diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html
index f212bd2d4..84aa5df4b 100644
--- a/docs/generated/changelog.html
+++ b/docs/generated/changelog.html
@@ -10,6 +10,16 @@
Agent-JS Changelog
+ Version 0.12.0
+
+ -
+ Changed the certificate verification interface and fixed its logic. The public constructor
+ is now static and asynchronous. There is no separate verification method, the check is
+ done automatically in the constructor and newly also checks that the delegation is
+ authoritative for the given canister ID, as required by the Internet Computer interface
+ specification.
+
+
Version 0.11.2
-
diff --git a/e2e/node/basic/basic.test.ts b/e2e/node/basic/basic.test.ts
index 4b21ed6ab..a085873f4 100644
--- a/e2e/node/basic/basic.test.ts
+++ b/e2e/node/basic/basic.test.ts
@@ -10,13 +10,16 @@ test('read_state', async () => {
const resolvedAgent = await agent;
const now = Date.now() / 1000;
const path = [new TextEncoder().encode('time')];
- const response = await resolvedAgent.readState(Principal.fromHex('00000000000000000001'), {
+ const canisterId = Principal.fromHex('00000000000000000001');
+ const response = await resolvedAgent.readState(canisterId, {
paths: [path],
});
- const cert = new Certificate(response, resolvedAgent);
-
- expect(() => cert.lookup(path)).toThrow(/Cannot lookup unverified certificate/);
- expect(await cert.verify()).toBe(true);
+ if (resolvedAgent.rootKey == null) throw new Error(`The agent doesn't have a root key yet`);
+ const cert = await Certificate.create({
+ certificate: response.certificate,
+ rootKey: resolvedAgent.rootKey,
+ canisterId: canisterId,
+ });
expect(cert.lookup([new TextEncoder().encode('Time')])).toBe(undefined);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const rawTime = cert.lookup(path)!;
diff --git a/e2e/node/basic/mitm.test.ts b/e2e/node/basic/mitm.test.ts
index 092d9dc25..1cae8e3dc 100644
--- a/e2e/node/basic/mitm.test.ts
+++ b/e2e/node/basic/mitm.test.ts
@@ -11,6 +11,6 @@ if (!process.env['MITM']) {
jest.setTimeout(30000);
mitmTest('mitm greet', async () => {
const { actor: counter } = await counterCanister();
- await expect(counter.greet('counter')).rejects.toThrow(/Fail to verify certificate/);
+ await expect(counter.greet('counter')).rejects.toThrow(/Invalid certificate/);
expect(await counter.queryGreet('counter')).toEqual('Hullo, counter!');
});
diff --git a/package-lock.json b/package-lock.json
index 1d454f337..f0b27c96e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@dfinity/agent-monorepo",
- "version": "0.11.2",
+ "version": "0.11.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@dfinity/agent-monorepo",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"dependencies": {
"jest": "^27.3.1",
@@ -52,13 +52,13 @@
},
"demos/ledgerhq": {
"name": "ic-agent-ledgerhq-app",
- "version": "0.11.2",
+ "version": "0.11.3",
"dependencies": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/authentication": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/identity-ledgerhq": "^0.11.2",
- "@dfinity/principal": "^0.11.2",
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/authentication": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/identity-ledgerhq": "^0.11.3",
+ "@dfinity/principal": "^0.11.3",
"assert": "^2.0.0",
"buffer": "^6.0.3",
"events": "^3.2.0",
@@ -74,12 +74,12 @@
},
"demos/sample-javascript": {
"name": "ic-agent-sample-javascript-app",
- "version": "0.11.2",
+ "version": "0.11.3",
"dependencies": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/authentication": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/principal": "^0.11.2",
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/authentication": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/principal": "^0.11.3",
"assert": "^2.0.0",
"events": "^3.2.0",
"html-webpack-plugin": "^5.1.0",
@@ -93,12 +93,12 @@
},
"e2e/node": {
"name": "@do-not-publish/ic-node-e2e-tests",
- "version": "0.11.2",
+ "version": "0.11.3",
"dependencies": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/authentication": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/principal": "^0.11.2",
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/authentication": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/principal": "^0.11.3",
"@trust/webcrypto": "^0.9.2",
"@types/base64-js": "^1.2.5",
"@types/jest": "^26.0.23",
@@ -10257,9 +10257,10 @@
}
},
"node_modules/protobufjs": {
- "version": "6.11.2",
+ "version": "6.11.3",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
+ "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
"hasInstallScript": true,
- "license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
@@ -12451,7 +12452,7 @@
},
"packages/agent": {
"name": "@dfinity/agent",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"dependencies": {
"base64-arraybuffer": "^0.2.0",
@@ -12477,8 +12478,8 @@
"whatwg-fetch": "^3.0.0"
},
"peerDependencies": {
- "@dfinity/candid": "^0.11.2",
- "@dfinity/principal": "^0.11.2"
+ "@dfinity/candid": "^0.11.3",
+ "@dfinity/principal": "^0.11.3"
}
},
"packages/agent/node_modules/@types/jest": {
@@ -12504,7 +12505,7 @@
},
"packages/auth-client": {
"name": "@dfinity/auth-client",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"devDependencies": {
"@trust/webcrypto": "^0.9.2",
@@ -12523,15 +12524,15 @@
"whatwg-fetch": "^3.0.0"
},
"peerDependencies": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/authentication": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/principal": "^0.11.2"
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/authentication": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/principal": "^0.11.3"
}
},
"packages/authentication": {
"name": "@dfinity/authentication",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"devDependencies": {
"@trust/webcrypto": "^0.9.2",
@@ -12550,14 +12551,14 @@
"whatwg-fetch": "^3.0.0"
},
"peerDependencies": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/principal": "^0.11.2"
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/principal": "^0.11.3"
}
},
"packages/candid": {
"name": "@dfinity/candid",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"devDependencies": {
"@types/jest": "^27.0.2",
@@ -12697,7 +12698,7 @@
},
"packages/identity": {
"name": "@dfinity/identity",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"dependencies": {
"@types/webappsec-credential-management": "^0.6.2",
@@ -12723,13 +12724,13 @@
"whatwg-fetch": "^3.0.0"
},
"peerDependencies": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/principal": "^0.11.2"
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/principal": "^0.11.3"
}
},
"packages/identity-ledgerhq": {
"name": "@dfinity/identity-ledgerhq",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"dependencies": {
"@ledgerhq/hw-transport": "^5.49.0",
@@ -12755,14 +12756,14 @@
"whatwg-fetch": "^3.0.0"
},
"peerDependencies": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/principal": "^0.11.2"
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/principal": "^0.11.3"
}
},
"packages/principal": {
"name": "@dfinity/principal",
- "version": "0.11.2",
+ "version": "0.11.3",
"license": "Apache-2.0",
"devDependencies": {
"@types/jest": "^27.0.2",
@@ -13850,10 +13851,10 @@
"@do-not-publish/ic-node-e2e-tests": {
"version": "file:e2e/node",
"requires": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/authentication": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/principal": "^0.11.2",
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/authentication": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/principal": "^0.11.3",
"@trust/webcrypto": "^0.9.2",
"@types/base64-js": "^1.2.5",
"@types/jest": "^26.0.23",
@@ -16828,11 +16829,11 @@
"ic-agent-ledgerhq-app": {
"version": "file:demos/ledgerhq",
"requires": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/authentication": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/identity-ledgerhq": "^0.11.2",
- "@dfinity/principal": "^0.11.2",
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/authentication": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/identity-ledgerhq": "^0.11.3",
+ "@dfinity/principal": "^0.11.3",
"assert": "^2.0.0",
"buffer": "^6.0.3",
"events": "^3.2.0",
@@ -16849,10 +16850,10 @@
"ic-agent-sample-javascript-app": {
"version": "file:demos/sample-javascript",
"requires": {
- "@dfinity/agent": "^0.11.2",
- "@dfinity/authentication": "^0.11.2",
- "@dfinity/identity": "^0.11.2",
- "@dfinity/principal": "^0.11.2",
+ "@dfinity/agent": "^0.11.3",
+ "@dfinity/authentication": "^0.11.3",
+ "@dfinity/identity": "^0.11.3",
+ "@dfinity/principal": "^0.11.3",
"assert": "^2.0.0",
"events": "^3.2.0",
"html-webpack-plugin": "^5.1.0",
@@ -19518,7 +19519,9 @@
}
},
"protobufjs": {
- "version": "6.11.2",
+ "version": "6.11.3",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
+ "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
diff --git a/packages/agent/src/canisterStatus/index.ts b/packages/agent/src/canisterStatus/index.ts
index aab8b68f1..0f321b75b 100644
--- a/packages/agent/src/canisterStatus/index.ts
+++ b/packages/agent/src/canisterStatus/index.ts
@@ -87,13 +87,11 @@ export const request = async (options: {
const response = await agent.readState(canisterId, {
paths: [encodedPaths[index]],
});
- const cert = new Certificate(response, agent);
- const verified = await cert.verify();
- if (!verified) {
- throw new Error(
- 'There was a problem certifying the response data. Please verify your connection to the mainnet, or be sure to call fetchRootKey on your agent if you are developing locally',
- );
- }
+ const cert = await Certificate.create({
+ certificate: response.certificate,
+ rootKey: agent.rootKey,
+ canisterId: canisterId,
+ });
const data = cert.lookup(encodePath(uniquePaths[index], canisterId));
if (!data) {
diff --git a/packages/agent/src/certificate.test.ts b/packages/agent/src/certificate.test.ts
index e24ba702d..3bbea0ca8 100644
--- a/packages/agent/src/certificate.test.ts
+++ b/packages/agent/src/certificate.test.ts
@@ -6,6 +6,8 @@
import * as cbor from './cbor';
import * as Cert from './certificate';
import { fromHex, toHex } from './utils/buffer';
+import { Principal } from '@dfinity/principal';
+import { NodeBuilderFlags } from 'typescript';
function label(str: string): ArrayBuffer {
return new TextEncoder().encode(str);
@@ -15,6 +17,13 @@ function pruned(str: string): ArrayBuffer {
return fromHex(str);
}
+// Root public key for the IC main net, encoded as hex
+const IC_ROOT_KEY =
+ '308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100814' +
+ 'c0e6ec71fab583b08bd81373c255c3c371b2e84863c98a4f1e08b74235d14fb5d9c0cd546d968' +
+ '5f913a0c0b2cc5341583bf4b4392e467db96d65b9bb4cb717112f8472e0d5a4d14505ffd7484' +
+ 'b01291091c5f87b98883463f98091a0baaae';
+
test('hash tree', async () => {
const cborEncode = fromHex(
'8301830183024161830183018302417882034568656c6c6f810083024179820345776f726c64' +
@@ -124,3 +133,87 @@ test('lookup', () => {
expect(toText(Cert.lookup_path([fromText('d')], tree))).toEqual('morning');
expect(Cert.lookup_path([fromText('e')], tree)).toEqual(undefined);
});
+
+// The sample certificate for testing delegation is extracted from the response used in agent-rs tests, where they were taken
+// from an interaction with the IC mainnet.
+const SAMPLE_CERT: string =
+ 'd9d9f7a364747265658301830182045820250f5e26868d9c1ea7ab29cbe9c15bf1c47c0d7605e803e39e375a7fe09c6ebb830183024e726571756573745f7374617475738301820458204b268227774ec77ff2b37ecb12157329d54cf376694bdd59ded7803efd82386f83025820edad510eaaa08ed2acd4781324e6446269da6753ec17760f206bbe81c465ff528301830183024b72656a6563745f636f64658203410383024e72656a6563745f6d6573736167658203584443616e69737465722069766733372d71696161612d61616161622d61616167612d63616920686173206e6f20757064617465206d6574686f64202772656769737465722783024673746174757382034872656a65637465648204582097232f31f6ab7ca4fe53eb6568fc3e02bc22fe94ab31d010e5fb3c642301f1608301820458203a48d1fc213d49307103104f7d72c2b5930edba8787b90631f343b3aa68a5f0a83024474696d65820349e2dc939091c696eb16697369676e6174757265583089a2be21b5fa8ac9fab1527e041327ce899d7da971436a1f2165393947b4d942365bfe5488710e61a619ba48388a21b16a64656c65676174696f6ea2697375626e65745f6964581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac4026b6365727469666963617465590231d9d9f7a26474726565830182045820ae023f28c3b9d966c8fb09f9ed755c828aadb5152e00aaf700b18c9c067294b483018302467375626e6574830182045820e83bb025f6574c8f31233dc0fe289ff546dfa1e49bd6116dd6e8896d90a4946e830182045820e782619092d69d5bebf0924138bd4116b0156b5a95e25c358ea8cf7e7161a661830183018204582062513fa926c9a9ef803ac284d620f303189588e1d3904349ab63b6470856fc4883018204582060e9a344ced2c9c4a96a0197fd585f2d259dbd193e4eada56239cac26087f9c58302581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac402830183024f63616e69737465725f72616e6765738203581bd9d9f781824a000000000020000001014a00000000002fffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361009933e1f89e8a3c4d7fdcccdbd518089e2bd4d8180a261f18d9c247a52768ebce98dc7328a39814a8f911086a1dd50cbe015e2a53b7bf78b55288893daa15c346640e8831d72a12bdedd979d28470c34823b8d1c3f4795d9c3984a247132e94fe82045820996f17bb926be3315745dea7282005a793b58e76afeb5d43d1a28ce29d2d158583024474696d6582034995b8aac0e4eda2ea16697369676e61747572655830ace9fcdd9bc977e05d6328f889dc4e7c99114c737a494653cb27a1f55c06f4555e0f160980af5ead098acc195010b2f7';
+
+test('delegation works for canisters within the subnet range', async () => {
+ // The certificate specifies the range from
+ // 0x00000000002000000101
+ // to
+ // 0x00000000002FFFFF0101
+ const rangeStart = Principal.fromHex('00000000002000000101');
+ const rangeInterior = Principal.fromHex('000000000020000C0101');
+ const rangeEnd = Principal.fromHex('00000000002FFFFF0101');
+ async function verifies(canisterId) {
+ await expect(
+ Cert.Certificate.create({
+ certificate: fromHex(SAMPLE_CERT),
+ rootKey: fromHex(IC_ROOT_KEY),
+ canisterId: canisterId,
+ }),
+ ).resolves.not.toThrow();
+ }
+ await verifies(rangeStart);
+ await verifies(rangeInterior);
+ await verifies(rangeEnd);
+});
+
+function fail(reason) {
+ throw new Error(reason);
+}
+
+test('delegation check fails for canisters outside of the subnet range', async () => {
+ // Use a different principal than the happy path, which isn't in the delegation ranges.
+ // The certificate specifies the range from
+ // 0x00000000002000000101
+ // to
+ // 0x00000000002FFFFF0101
+ const beforeRange = Principal.fromHex('00000000000000020101');
+ const afterRange = Principal.fromHex('00000000003000020101');
+ async function certificateFails(canisterId) {
+ await expect(
+ Cert.Certificate.create({
+ certificate: fromHex(SAMPLE_CERT),
+ rootKey: fromHex(IC_ROOT_KEY),
+ canisterId: canisterId,
+ }),
+ ).rejects.toThrow(/Invalid certificate/);
+ }
+ await certificateFails(beforeRange);
+ await certificateFails(afterRange);
+});
+
+// The only situation in which one can read state of the IC management canister
+// is when the user calls provisional_create_canister_with_cycles. In this case,
+// we shouldn't check the delegations.
+test('delegation check succeeds for the management canister', async () => {
+ await expect(
+ Cert.Certificate.create({
+ certificate: fromHex(SAMPLE_CERT),
+ rootKey: fromHex(IC_ROOT_KEY),
+ canisterId: Principal.managementCanister(),
+ }),
+ ).resolves.not.toThrow();
+});
+
+type FakeCert = {
+ tree: Cert.HashTree;
+ signature: ArrayBuffer;
+ delegation?: { subnet_id: ArrayBuffer; certificate: ArrayBuffer };
+};
+
+test('certificate verification fails for an invalid signature', async () => {
+ let badCert: FakeCert = cbor.decode(fromHex(SAMPLE_CERT));
+ badCert.signature = new ArrayBuffer(badCert.signature.byteLength);
+ const badCertEncoded = cbor.encode(badCert);
+ await expect(
+ Cert.Certificate.create({
+ certificate: badCertEncoded,
+ rootKey: fromHex(IC_ROOT_KEY),
+ canisterId: Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'),
+ }),
+ ).rejects.toThrow('Invalid certificate');
+});
diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts
index 11418c617..a92824cd1 100644
--- a/packages/agent/src/certificate.ts
+++ b/packages/agent/src/certificate.ts
@@ -1,17 +1,16 @@
-import { Agent, getDefaultAgent, ReadStateResponse } from './agent';
import * as cbor from './cbor';
import { AgentError } from './errors';
import { hash } from './request_id';
import { blsVerify } from './utils/bls';
import { concat, fromHex, toHex } from './utils/buffer';
+import { Principal } from '@dfinity/principal';
/**
- * A certificate needs to be verified (using {@link Certificate.prototype.verify})
- * before it can be used.
+ * A certificate may fail verification with respect to the provided public key
*/
-export class UnverifiedCertificateError extends AgentError {
- constructor() {
- super(`Cannot lookup unverified certificate. Call 'verify()' first.`);
+export class CertificateVerificationError extends AgentError {
+ constructor(reason: string) {
+ super(`Invalid certificate: ${reason}`);
}
}
@@ -99,59 +98,111 @@ function isBufferEqual(a: ArrayBuffer, b: ArrayBuffer): boolean {
return true;
}
+export interface CreateCertificateOptions {
+ /**
+ * The bytes encoding the certificate to be verified
+ */
+ certificate: ArrayBuffer;
+ /**
+ * The root key against which to verify the certificate
+ * (normally, the root key of the IC main network)
+ */
+ rootKey: ArrayBuffer;
+ /**
+ * The effective canister ID of the request when verifying a response, or
+ * the signing canister ID when verifying a certified variable.
+ */
+ canisterId: Principal;
+}
+
export class Certificate {
private readonly cert: Cert;
- private verified = false;
- private _rootKey: ArrayBuffer | null = null;
- constructor(response: ReadStateResponse, private _agent: Agent = getDefaultAgent()) {
- this.cert = cbor.decode(new Uint8Array(response.certificate));
+ /**
+ * Create a new instance of a certificate, automatically verifying it. Throws a
+ * CertificateVerificationError if the certificate cannot be verified.
+ * @constructs {@link AuthClient}
+ * @param {CreateCertificateOptions} options
+ * @see {@link CreateCertificateOptions}
+ * @param {ArrayBuffer} options.certificate The bytes of the certificate
+ * @param {ArrayBuffer} options.rootKey The root key to verify against
+ * @param {Principal} options.canisterId The effective or signing canister ID
+ * @throws {CertificateVerificationError}
+ */
+ public static async create(options: CreateCertificateOptions): Promise {
+ const cert = new Certificate(options.certificate, options.rootKey, options.canisterId);
+ await cert.verify();
+ return cert;
+ }
+
+ private constructor(
+ certificate: ArrayBuffer,
+ private _rootKey: ArrayBuffer,
+ private _canisterId: Principal,
+ ) {
+ this.cert = cbor.decode(new Uint8Array(certificate));
}
public lookup(path: Array): ArrayBuffer | undefined {
- this.checkState();
return lookup_path(path, this.cert.tree);
}
- public async verify(): Promise {
+ private async verify(): Promise {
const rootHash = await reconstruct(this.cert.tree);
- const derKey = await this._checkDelegation(this.cert.delegation);
+ const derKey = await this._checkDelegationAndGetKey(this.cert.delegation);
const sig = this.cert.signature;
const key = extractDER(derKey);
const msg = concat(domain_sep('ic-state-root'), rootHash);
- const res = await blsVerify(new Uint8Array(key), new Uint8Array(sig), new Uint8Array(msg));
- this.verified = res;
- return res;
- }
-
- protected checkState(): void {
- if (!this.verified) {
- throw new UnverifiedCertificateError();
+ let sigVer = false;
+ try {
+ sigVer = await blsVerify(new Uint8Array(key), new Uint8Array(sig), new Uint8Array(msg));
+ } catch (err) {
+ sigVer = false;
+ }
+ if (!sigVer) {
+ throw new CertificateVerificationError('Signature verification failed');
}
}
- private async _checkDelegation(d?: Delegation): Promise {
+ private async _checkDelegationAndGetKey(d?: Delegation): Promise {
if (!d) {
- if (!this._rootKey) {
- if (this._agent.rootKey) {
- this._rootKey = this._agent.rootKey;
- return this._rootKey;
- }
-
- throw new Error(`Agent does not have a rootKey. Do you need to call 'fetchRootKey'?`);
- }
return this._rootKey;
}
- const cert: Certificate = new Certificate(d as any, this._agent);
- if (!(await cert.verify())) {
- throw new Error('fail to verify delegation certificate');
- }
+ const cert: Certificate = await Certificate.create({
+ certificate: d.certificate,
+ rootKey: this._rootKey,
+ canisterId: this._canisterId,
+ });
+
+ if (this._canisterId.compareTo(Principal.managementCanister()) !== 'eq') {
+ const rangeLookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']);
+ if (!rangeLookup) {
+ throw new CertificateVerificationError(
+ `Could not find canister ranges for subnet 0x${toHex(d.subnet_id)}`,
+ );
+ }
+ const ranges_arr: Array<[Uint8Array, Uint8Array]> = cbor.decode(rangeLookup);
+ const ranges: Array<[Principal, Principal]> = ranges_arr.map(v => [
+ Principal.fromUint8Array(v[0]),
+ Principal.fromUint8Array(v[1]),
+ ]);
- const lookup = cert.lookup(['subnet', d.subnet_id, 'public_key']);
- if (!lookup) {
+ const canisterInRange = ranges.some(
+ r => r[0].ltEq(this._canisterId) && r[1].gtEq(this._canisterId),
+ );
+ if (!canisterInRange) {
+ throw new CertificateVerificationError(
+ `Canister ${this._canisterId} not in range of delegations for subnet 0x${toHex(
+ d.subnet_id,
+ )}`,
+ );
+ }
+ }
+ const publicKeyLookup = cert.lookup(['subnet', d.subnet_id, 'public_key']);
+ if (!publicKeyLookup) {
throw new Error(`Could not find subnet key for subnet 0x${toHex(d.subnet_id)}`);
}
- return lookup;
+ return publicKeyLookup;
}
}
diff --git a/packages/agent/src/polling/index.ts b/packages/agent/src/polling/index.ts
index 708096d91..f71123a90 100644
--- a/packages/agent/src/polling/index.ts
+++ b/packages/agent/src/polling/index.ts
@@ -29,11 +29,12 @@ export async function pollForResponse(
): Promise {
const path = [new TextEncoder().encode('request_status'), requestId];
const state = await agent.readState(canisterId, { paths: [path] });
- const cert = new Certificate(state, agent);
- const verified = await cert.verify();
- if (!verified) {
- throw new Error('Fail to verify certificate');
- }
+ if (agent.rootKey == null) throw new Error('Agent root key not initialized before polling');
+ const cert = await Certificate.create({
+ certificate: state.certificate,
+ rootKey: agent.rootKey,
+ canisterId: canisterId,
+ });
const maybeBuf = cert.lookup([...path, new TextEncoder().encode('status')]);
let status;
if (typeof maybeBuf === 'undefined') {
diff --git a/packages/principal/src/index.test.ts b/packages/principal/src/index.test.ts
index a42b24be0..e549a4417 100644
--- a/packages/principal/src/index.test.ts
+++ b/packages/principal/src/index.test.ts
@@ -24,4 +24,21 @@ describe('Principal', () => {
it('errors out on parsing invalid characters', () => {
expect(() => Principal.fromText('Hello world!')).toThrow();
});
+
+ it('compares principals correctly', () => {
+ const anonymous = Principal.anonymous();
+ const principal1 = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai');
+ const principal2 = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai');
+ for (const p of [anonymous, principal1, principal2]) {
+ expect(p.compareTo(p)).toBe('eq');
+ expect(p.ltEq(p)).toBe(true);
+ expect(p.gtEq(p)).toBe(true);
+ }
+ expect(principal1.compareTo(principal2)).toBe('lt');
+ expect(principal1.compareTo(anonymous)).toBe('lt');
+ expect(principal2.compareTo(principal1)).toBe('gt');
+ expect(principal2.compareTo(anonymous)).toBe('lt');
+ expect(anonymous.compareTo(principal1)).toBe('gt');
+ expect(anonymous.compareTo(principal2)).toBe('gt');
+ });
});
diff --git a/packages/principal/src/index.ts b/packages/principal/src/index.ts
index ca1e16bee..857282a60 100644
--- a/packages/principal/src/index.ts
+++ b/packages/principal/src/index.ts
@@ -5,6 +5,8 @@ import { sha224 } from './utils/sha224';
const SELF_AUTHENTICATING_SUFFIX = 2;
const ANONYMOUS_SUFFIX = 4;
+const MANAGEMENT_CANISTER_PRINCIPAL_HEX_STR = 'aaaaa-aa';
+
const fromHexString = (hexString: string) =>
new Uint8Array((hexString.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16)));
@@ -16,6 +18,14 @@ export class Principal {
return new this(new Uint8Array([ANONYMOUS_SUFFIX]));
}
+ /**
+ * Utility method, returning the principal representing the management canister, decoded from the hex string `'aaaaa-aa'`
+ * @returns {Principal} principal of the management canister
+ */
+ public static managementCanister(): Principal {
+ return this.fromHex(MANAGEMENT_CANISTER_PRINCIPAL_HEX_STR);
+ }
+
public static selfAuthenticating(publicKey: Uint8Array): Principal {
const sha = sha224(publicKey);
return new this(new Uint8Array([...sha, SELF_AUTHENTICATING_SUFFIX]));
@@ -96,4 +106,40 @@ export class Principal {
public toString(): string {
return this.toText();
}
+
+ /**
+ * Utility method taking a Principal to compare against. Used for determining canister ranges in certificate verification
+ * @param {Principal} other - a {@link Principal} to compare
+ * @returns {'lt' | 'eq' | 'gt'} `'lt' | 'eq' | 'gt'` a string, representing less than, equal to, or greater than
+ */
+ public compareTo(other: Principal): 'lt' | 'eq' | 'gt' {
+ for (let i = 0; i < Math.min(this._arr.length, other._arr.length); i++) {
+ if (this._arr[i] < other._arr[i]) return 'lt';
+ else if (this._arr[i] > other._arr[i]) return 'gt';
+ }
+ // Here, at least one principal is a prefix of the other principal (they could be the same)
+ if (this._arr.length < other._arr.length) return 'lt';
+ if (this._arr.length > other._arr.length) return 'gt';
+ return 'eq';
+ }
+
+ /**
+ * Utility method checking whether a provided Principal is less than or equal to the current one using the {@link Principal.compareTo} method
+ * @param other a {@link Principal} to compare
+ * @returns {boolean} boolean
+ */
+ public ltEq(other: Principal): boolean {
+ const cmp = this.compareTo(other);
+ return cmp == 'lt' || cmp == 'eq';
+ }
+
+ /**
+ * Utility method checking whether a provided Principal is greater than or equal to the current one using the {@link Principal.compareTo} method
+ * @param other a {@link Principal} to compare
+ * @returns {boolean} boolean
+ */
+ public gtEq(other: Principal): boolean {
+ const cmp = this.compareTo(other);
+ return cmp == 'gt' || cmp == 'eq';
+ }
}