From d67164e8f85d2e444d87859351cf44988023736f Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Mon, 30 May 2022 18:38:37 +0200 Subject: [PATCH 01/15] Verify the canister subnet ranges for a certificate --- packages/agent/src/canisterStatus/index.ts | 4 +- packages/agent/src/certificate.test.ts | 34 +++++++++++++++ packages/agent/src/certificate.ts | 49 +++++++++++++--------- packages/agent/src/polling/index.ts | 4 +- packages/principal/src/index.test.ts | 17 ++++++++ packages/principal/src/index.ts | 21 ++++++++++ 6 files changed, 105 insertions(+), 24 deletions(-) diff --git a/packages/agent/src/canisterStatus/index.ts b/packages/agent/src/canisterStatus/index.ts index 10ff779cb..370f15cb3 100644 --- a/packages/agent/src/canisterStatus/index.ts +++ b/packages/agent/src/canisterStatus/index.ts @@ -87,8 +87,8 @@ 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(); + const cert = new Certificate(response.certificate, agent.fetchRootKey()); + const verified = await cert.verify(canisterId); 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', diff --git a/packages/agent/src/certificate.test.ts b/packages/agent/src/certificate.test.ts index e24ba702d..d61b13a08 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -3,9 +3,11 @@ * an instance of ArrayBuffer). * @jest-environment node */ +import { fromHexString } from '@dfinity/candid'; import * as cbor from './cbor'; import * as Cert from './certificate'; import { fromHex, toHex } from './utils/buffer'; +import { Principal } from '@dfinity/principal'; 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,28 @@ test('lookup', () => { expect(toText(Cert.lookup_path([fromText('d')], tree))).toEqual('morning'); expect(Cert.lookup_path([fromText('e')], tree)).toEqual(undefined); }); + +test('delegation works for canisters within the subnet range', async () => { + // Certificate is the same one used in agent-rs tests, where they were taken + // from an interaction with the IC mainnet + const certHex = + 'd9d9f7a364747265658301830182045820250f5e26868d9c1ea7ab29cbe9c15bf1c47c0d7605e803e39e375a7fe09c6ebb830183024e726571756573745f7374617475738301820458204b268227774ec77ff2b37ecb12157329d54cf376694bdd59ded7803efd82386f83025820edad510eaaa08ed2acd4781324e6446269da6753ec17760f206bbe81c465ff528301830183024b72656a6563745f636f64658203410383024e72656a6563745f6d6573736167658203584443616e69737465722069766733372d71696161612d61616161622d61616167612d63616920686173206e6f20757064617465206d6574686f64202772656769737465722783024673746174757382034872656a65637465648204582097232f31f6ab7ca4fe53eb6568fc3e02bc22fe94ab31d010e5fb3c642301f1608301820458203a48d1fc213d49307103104f7d72c2b5930edba8787b90631f343b3aa68a5f0a83024474696d65820349e2dc939091c696eb16697369676e6174757265583089a2be21b5fa8ac9fab1527e041327ce899d7da971436a1f2165393947b4d942365bfe5488710e61a619ba48388a21b16a64656c65676174696f6ea2697375626e65745f6964581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac4026b6365727469666963617465590231d9d9f7a26474726565830182045820ae023f28c3b9d966c8fb09f9ed755c828aadb5152e00aaf700b18c9c067294b483018302467375626e6574830182045820e83bb025f6574c8f31233dc0fe289ff546dfa1e49bd6116dd6e8896d90a4946e830182045820e782619092d69d5bebf0924138bd4116b0156b5a95e25c358ea8cf7e7161a661830183018204582062513fa926c9a9ef803ac284d620f303189588e1d3904349ab63b6470856fc4883018204582060e9a344ced2c9c4a96a0197fd585f2d259dbd193e4eada56239cac26087f9c58302581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac402830183024f63616e69737465725f72616e6765738203581bd9d9f781824a000000000020000001014a00000000002fffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361009933e1f89e8a3c4d7fdcccdbd518089e2bd4d8180a261f18d9c247a52768ebce98dc7328a39814a8f911086a1dd50cbe015e2a53b7bf78b55288893daa15c346640e8831d72a12bdedd979d28470c34823b8d1c3f4795d9c3984a247132e94fe82045820996f17bb926be3315745dea7282005a793b58e76afeb5d43d1a28ce29d2d158583024474696d6582034995b8aac0e4eda2ea16697369676e61747572655830ace9fcdd9bc977e05d6328f889dc4e7c99114c737a494653cb27a1f55c06f4555e0f160980af5ead098acc195010b2f7'; + const canisterId = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'); + const cert = new Cert.Certificate(fromHex(certHex), Promise.resolve(fromHex(IC_ROOT_KEY))); + const result = await cert.verify(canisterId); + expect(result).toEqual(true); +}); + +test('delegation check fails for canisters outside of the subnet range', async () => { + // Same certificate as in the happy path, but a different canister ID + const certHex = + 'd9d9f7a364747265658301830182045820250f5e26868d9c1ea7ab29cbe9c15bf1c47c0d7605e803e39e375a7fe09c6ebb830183024e726571756573745f7374617475738301820458204b268227774ec77ff2b37ecb12157329d54cf376694bdd59ded7803efd82386f83025820edad510eaaa08ed2acd4781324e6446269da6753ec17760f206bbe81c465ff528301830183024b72656a6563745f636f64658203410383024e72656a6563745f6d6573736167658203584443616e69737465722069766733372d71696161612d61616161622d61616167612d63616920686173206e6f20757064617465206d6574686f64202772656769737465722783024673746174757382034872656a65637465648204582097232f31f6ab7ca4fe53eb6568fc3e02bc22fe94ab31d010e5fb3c642301f1608301820458203a48d1fc213d49307103104f7d72c2b5930edba8787b90631f343b3aa68a5f0a83024474696d65820349e2dc939091c696eb16697369676e6174757265583089a2be21b5fa8ac9fab1527e041327ce899d7da971436a1f2165393947b4d942365bfe5488710e61a619ba48388a21b16a64656c65676174696f6ea2697375626e65745f6964581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac4026b6365727469666963617465590231d9d9f7a26474726565830182045820ae023f28c3b9d966c8fb09f9ed755c828aadb5152e00aaf700b18c9c067294b483018302467375626e6574830182045820e83bb025f6574c8f31233dc0fe289ff546dfa1e49bd6116dd6e8896d90a4946e830182045820e782619092d69d5bebf0924138bd4116b0156b5a95e25c358ea8cf7e7161a661830183018204582062513fa926c9a9ef803ac284d620f303189588e1d3904349ab63b6470856fc4883018204582060e9a344ced2c9c4a96a0197fd585f2d259dbd193e4eada56239cac26087f9c58302581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac402830183024f63616e69737465725f72616e6765738203581bd9d9f781824a000000000020000001014a00000000002fffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361009933e1f89e8a3c4d7fdcccdbd518089e2bd4d8180a261f18d9c247a52768ebce98dc7328a39814a8f911086a1dd50cbe015e2a53b7bf78b55288893daa15c346640e8831d72a12bdedd979d28470c34823b8d1c3f4795d9c3984a247132e94fe82045820996f17bb926be3315745dea7282005a793b58e76afeb5d43d1a28ce29d2d158583024474696d6582034995b8aac0e4eda2ea16697369676e61747572655830ace9fcdd9bc977e05d6328f889dc4e7c99114c737a494653cb27a1f55c06f4555e0f160980af5ead098acc195010b2f7'; + const canisterId = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); + const cert = new Cert.Certificate(fromHex(certHex), Promise.resolve(fromHex(IC_ROOT_KEY))); + // This is a bit crufty; verify returns a boolean, but can also indicate verification errors + // by throwing exceptions + try { + const result = await cert.verify(canisterId); + expect(result).toEqual(false); + } catch (e) {} +}); diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index 11418c617..dfa890fe2 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -1,9 +1,9 @@ -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}) @@ -102,10 +102,9 @@ function isBufferEqual(a: ArrayBuffer, b: ArrayBuffer): boolean { 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)); + constructor(certificate: ArrayBuffer, private _rootKey: Promise) { + this.cert = cbor.decode(new Uint8Array(certificate)); } public lookup(path: Array): ArrayBuffer | undefined { @@ -113,9 +112,9 @@ export class Certificate { return lookup_path(path, this.cert.tree); } - public async verify(): Promise { + public async verify(canisterId: Principal): Promise { const rootHash = await reconstruct(this.cert.tree); - const derKey = await this._checkDelegation(this.cert.delegation); + const derKey = await this._checkDelegationAndGetKey(canisterId, this.cert.delegation); const sig = this.cert.signature; const key = extractDER(derKey); const msg = concat(domain_sep('ic-state-root'), rootHash); @@ -130,28 +129,38 @@ export class Certificate { } } - private async _checkDelegation(d?: Delegation): Promise { + private async _checkDelegationAndGetKey( + canisterId: Principal, + 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())) { + const cert: Certificate = new Certificate(d.certificate, this._rootKey); + if (!(await cert.verify(canisterId))) { throw new Error('fail to verify delegation certificate'); } + const range_lookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); + if (!range_lookup) { + throw new Error(`Could not find canister ranges for subnet 0x${toHex(d.subnet_id)}`); + } + const ranges_arr: Array<[Uint8Array, Uint8Array]> = cbor.decode(range_lookup); + 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 canister_in_range = ranges.some(r => r[0].ltEq(canisterId) && r[1].gtEq(canisterId)); + if (!canister_in_range) { + throw new Error( + `Canister ${canisterId} not in range of delegations for subnet 0x${toHex(d.subnet_id)}`, + ); + } + const public_key_lookup = cert.lookup(['subnet', d.subnet_id, 'public_key']); + if (!public_key_lookup) { throw new Error(`Could not find subnet key for subnet 0x${toHex(d.subnet_id)}`); } - return lookup; + return public_key_lookup; } } diff --git a/packages/agent/src/polling/index.ts b/packages/agent/src/polling/index.ts index 708096d91..a9e7dfe2f 100644 --- a/packages/agent/src/polling/index.ts +++ b/packages/agent/src/polling/index.ts @@ -29,8 +29,8 @@ 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(); + const cert = new Certificate(state.certificate, agent.fetchRootKey()); + const verified = await cert.verify(canisterId); if (!verified) { throw new Error('Fail to verify certificate'); } diff --git a/packages/principal/src/index.test.ts b/packages/principal/src/index.test.ts index a42b24be0..b6cbfe312 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 (let 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..3be744dbc 100644 --- a/packages/principal/src/index.ts +++ b/packages/principal/src/index.ts @@ -96,4 +96,25 @@ export class Principal { public toString(): string { return this.toText(); } + + 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'; + } + + public ltEq(other: Principal): boolean { + const cmp = this.compareTo(other); + return cmp == 'lt' || cmp == 'eq'; + } + + public gtEq(other: Principal): boolean { + const cmp = this.compareTo(other); + return cmp == 'gt' || cmp == 'eq'; + } } From 298b5b5d990050dbbbc878bf5aba54f2eeb35036 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Fri, 3 Jun 2022 16:04:02 +0200 Subject: [PATCH 02/15] Don't check subnet ranges for reading management canister state --- packages/agent/src/certificate.test.ts | 29 ++++++++++++++++-------- packages/agent/src/certificate.ts | 31 ++++++++++++++------------ packages/principal/src/index.ts | 6 +++++ 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/agent/src/certificate.test.ts b/packages/agent/src/certificate.test.ts index d61b13a08..cfc152a92 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -8,6 +8,7 @@ import * as cbor from './cbor'; import * as Cert from './certificate'; import { fromHex, toHex } from './utils/buffer'; import { Principal } from '@dfinity/principal'; +import { verify } from 'crypto'; function label(str: string): ArrayBuffer { return new TextEncoder().encode(str); @@ -134,23 +135,22 @@ test('lookup', () => { 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 () => { - // Certificate is the same one used in agent-rs tests, where they were taken - // from an interaction with the IC mainnet - const certHex = - 'd9d9f7a364747265658301830182045820250f5e26868d9c1ea7ab29cbe9c15bf1c47c0d7605e803e39e375a7fe09c6ebb830183024e726571756573745f7374617475738301820458204b268227774ec77ff2b37ecb12157329d54cf376694bdd59ded7803efd82386f83025820edad510eaaa08ed2acd4781324e6446269da6753ec17760f206bbe81c465ff528301830183024b72656a6563745f636f64658203410383024e72656a6563745f6d6573736167658203584443616e69737465722069766733372d71696161612d61616161622d61616167612d63616920686173206e6f20757064617465206d6574686f64202772656769737465722783024673746174757382034872656a65637465648204582097232f31f6ab7ca4fe53eb6568fc3e02bc22fe94ab31d010e5fb3c642301f1608301820458203a48d1fc213d49307103104f7d72c2b5930edba8787b90631f343b3aa68a5f0a83024474696d65820349e2dc939091c696eb16697369676e6174757265583089a2be21b5fa8ac9fab1527e041327ce899d7da971436a1f2165393947b4d942365bfe5488710e61a619ba48388a21b16a64656c65676174696f6ea2697375626e65745f6964581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac4026b6365727469666963617465590231d9d9f7a26474726565830182045820ae023f28c3b9d966c8fb09f9ed755c828aadb5152e00aaf700b18c9c067294b483018302467375626e6574830182045820e83bb025f6574c8f31233dc0fe289ff546dfa1e49bd6116dd6e8896d90a4946e830182045820e782619092d69d5bebf0924138bd4116b0156b5a95e25c358ea8cf7e7161a661830183018204582062513fa926c9a9ef803ac284d620f303189588e1d3904349ab63b6470856fc4883018204582060e9a344ced2c9c4a96a0197fd585f2d259dbd193e4eada56239cac26087f9c58302581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac402830183024f63616e69737465725f72616e6765738203581bd9d9f781824a000000000020000001014a00000000002fffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361009933e1f89e8a3c4d7fdcccdbd518089e2bd4d8180a261f18d9c247a52768ebce98dc7328a39814a8f911086a1dd50cbe015e2a53b7bf78b55288893daa15c346640e8831d72a12bdedd979d28470c34823b8d1c3f4795d9c3984a247132e94fe82045820996f17bb926be3315745dea7282005a793b58e76afeb5d43d1a28ce29d2d158583024474696d6582034995b8aac0e4eda2ea16697369676e61747572655830ace9fcdd9bc977e05d6328f889dc4e7c99114c737a494653cb27a1f55c06f4555e0f160980af5ead098acc195010b2f7'; const canisterId = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'); - const cert = new Cert.Certificate(fromHex(certHex), Promise.resolve(fromHex(IC_ROOT_KEY))); + const cert = new Cert.Certificate(fromHex(SAMPLE_CERT), Promise.resolve(fromHex(IC_ROOT_KEY))); const result = await cert.verify(canisterId); expect(result).toEqual(true); }); test('delegation check fails for canisters outside of the subnet range', async () => { - // Same certificate as in the happy path, but a different canister ID - const certHex = - 'd9d9f7a364747265658301830182045820250f5e26868d9c1ea7ab29cbe9c15bf1c47c0d7605e803e39e375a7fe09c6ebb830183024e726571756573745f7374617475738301820458204b268227774ec77ff2b37ecb12157329d54cf376694bdd59ded7803efd82386f83025820edad510eaaa08ed2acd4781324e6446269da6753ec17760f206bbe81c465ff528301830183024b72656a6563745f636f64658203410383024e72656a6563745f6d6573736167658203584443616e69737465722069766733372d71696161612d61616161622d61616167612d63616920686173206e6f20757064617465206d6574686f64202772656769737465722783024673746174757382034872656a65637465648204582097232f31f6ab7ca4fe53eb6568fc3e02bc22fe94ab31d010e5fb3c642301f1608301820458203a48d1fc213d49307103104f7d72c2b5930edba8787b90631f343b3aa68a5f0a83024474696d65820349e2dc939091c696eb16697369676e6174757265583089a2be21b5fa8ac9fab1527e041327ce899d7da971436a1f2165393947b4d942365bfe5488710e61a619ba48388a21b16a64656c65676174696f6ea2697375626e65745f6964581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac4026b6365727469666963617465590231d9d9f7a26474726565830182045820ae023f28c3b9d966c8fb09f9ed755c828aadb5152e00aaf700b18c9c067294b483018302467375626e6574830182045820e83bb025f6574c8f31233dc0fe289ff546dfa1e49bd6116dd6e8896d90a4946e830182045820e782619092d69d5bebf0924138bd4116b0156b5a95e25c358ea8cf7e7161a661830183018204582062513fa926c9a9ef803ac284d620f303189588e1d3904349ab63b6470856fc4883018204582060e9a344ced2c9c4a96a0197fd585f2d259dbd193e4eada56239cac26087f9c58302581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac402830183024f63616e69737465725f72616e6765738203581bd9d9f781824a000000000020000001014a00000000002fffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361009933e1f89e8a3c4d7fdcccdbd518089e2bd4d8180a261f18d9c247a52768ebce98dc7328a39814a8f911086a1dd50cbe015e2a53b7bf78b55288893daa15c346640e8831d72a12bdedd979d28470c34823b8d1c3f4795d9c3984a247132e94fe82045820996f17bb926be3315745dea7282005a793b58e76afeb5d43d1a28ce29d2d158583024474696d6582034995b8aac0e4eda2ea16697369676e61747572655830ace9fcdd9bc977e05d6328f889dc4e7c99114c737a494653cb27a1f55c06f4555e0f160980af5ead098acc195010b2f7'; + // Use a different principal than the happy path, which isn't in the delegation ranges. const canisterId = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); - const cert = new Cert.Certificate(fromHex(certHex), Promise.resolve(fromHex(IC_ROOT_KEY))); + const cert = new Cert.Certificate(fromHex(SAMPLE_CERT), Promise.resolve(fromHex(IC_ROOT_KEY))); // This is a bit crufty; verify returns a boolean, but can also indicate verification errors // by throwing exceptions try { @@ -158,3 +158,14 @@ test('delegation check fails for canisters outside of the subnet range', async ( expect(result).toEqual(false); } catch (e) {} }); + +// 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 () => { + const cert = new Cert.Certificate(fromHex(SAMPLE_CERT), Promise.resolve(fromHex(IC_ROOT_KEY))); + // This is a bit crufty; verify returns a boolean, but can also indicate verification errors + // by throwing exceptions + const result = await cert.verify(Principal.managementCanister()); + expect(result).toEqual(true); +}); diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index dfa890fe2..df2602b06 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -140,21 +140,24 @@ export class Certificate { if (!(await cert.verify(canisterId))) { throw new Error('fail to verify delegation certificate'); } - const range_lookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); - if (!range_lookup) { - throw new Error(`Could not find canister ranges for subnet 0x${toHex(d.subnet_id)}`); - } - const ranges_arr: Array<[Uint8Array, Uint8Array]> = cbor.decode(range_lookup); - const ranges: Array<[Principal, Principal]> = ranges_arr.map(v => [ - Principal.fromUint8Array(v[0]), - Principal.fromUint8Array(v[1]), - ]); - const canister_in_range = ranges.some(r => r[0].ltEq(canisterId) && r[1].gtEq(canisterId)); - if (!canister_in_range) { - throw new Error( - `Canister ${canisterId} not in range of delegations for subnet 0x${toHex(d.subnet_id)}`, - ); + if (canisterId.compareTo(Principal.managementCanister()) != 'eq') { + const range_lookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); + if (!range_lookup) { + throw new Error(`Could not find canister ranges for subnet 0x${toHex(d.subnet_id)}`); + } + const ranges_arr: Array<[Uint8Array, Uint8Array]> = cbor.decode(range_lookup); + const ranges: Array<[Principal, Principal]> = ranges_arr.map(v => [ + Principal.fromUint8Array(v[0]), + Principal.fromUint8Array(v[1]), + ]); + + const canister_in_range = ranges.some(r => r[0].ltEq(canisterId) && r[1].gtEq(canisterId)); + if (!canister_in_range) { + throw new Error( + `Canister ${canisterId} not in range of delegations for subnet 0x${toHex(d.subnet_id)}`, + ); + } } const public_key_lookup = cert.lookup(['subnet', d.subnet_id, 'public_key']); if (!public_key_lookup) { diff --git a/packages/principal/src/index.ts b/packages/principal/src/index.ts index 3be744dbc..3864b2127 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,10 @@ export class Principal { return new this(new Uint8Array([ANONYMOUS_SUFFIX])); } + 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])); From 2c360c82401f6e32cb83bf84012322a308010174 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Tue, 7 Jun 2022 11:17:37 +0200 Subject: [PATCH 03/15] Please the linter --- packages/principal/src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/principal/src/index.test.ts b/packages/principal/src/index.test.ts index b6cbfe312..e549a4417 100644 --- a/packages/principal/src/index.test.ts +++ b/packages/principal/src/index.test.ts @@ -29,7 +29,7 @@ describe('Principal', () => { const anonymous = Principal.anonymous(); const principal1 = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); const principal2 = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'); - for (let p of [anonymous, principal1, principal2]) { + 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); From 8adffe9e79361d959cc593a3ffea4eedf91c8533 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Tue, 7 Jun 2022 11:31:46 +0200 Subject: [PATCH 04/15] Update usage of Certificate.verify in e2e test --- e2e/node/basic/basic.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/e2e/node/basic/basic.test.ts b/e2e/node/basic/basic.test.ts index 4b21ed6ab..b918a8774 100644 --- a/e2e/node/basic/basic.test.ts +++ b/e2e/node/basic/basic.test.ts @@ -10,13 +10,14 @@ 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); + const cert = new Certificate(response.certificate, resolvedAgent.fetchRootKey()); expect(() => cert.lookup(path)).toThrow(/Cannot lookup unverified certificate/); - expect(await cert.verify()).toBe(true); + expect(await cert.verify(canisterId)).toBe(true); expect(cert.lookup([new TextEncoder().encode('Time')])).toBe(undefined); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const rawTime = cert.lookup(path)!; From 8212233a2df445ef488a6453f03771ea65c47e04 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Tue, 7 Jun 2022 11:54:44 +0200 Subject: [PATCH 05/15] snake_case to camelCase --- packages/agent/src/certificate.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index df2602b06..20e8ae10c 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -142,28 +142,28 @@ export class Certificate { } if (canisterId.compareTo(Principal.managementCanister()) != 'eq') { - const range_lookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); - if (!range_lookup) { + const rangeLookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); + if (!rangeLookup) { throw new Error(`Could not find canister ranges for subnet 0x${toHex(d.subnet_id)}`); } - const ranges_arr: Array<[Uint8Array, Uint8Array]> = cbor.decode(range_lookup); + 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 canister_in_range = ranges.some(r => r[0].ltEq(canisterId) && r[1].gtEq(canisterId)); - if (!canister_in_range) { + const canisterInRange = ranges.some(r => r[0].ltEq(canisterId) && r[1].gtEq(canisterId)); + if (!canisterInRange) { throw new Error( `Canister ${canisterId} not in range of delegations for subnet 0x${toHex(d.subnet_id)}`, ); } } - const public_key_lookup = cert.lookup(['subnet', d.subnet_id, 'public_key']); - if (!public_key_lookup) { + 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 public_key_lookup; + return publicKeyLookup; } } From d452ce7944c33a0a63dd6be5e16bf411f6e69c74 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Tue, 7 Jun 2022 12:25:59 +0200 Subject: [PATCH 06/15] Fix how root keys are determined --- e2e/node/basic/basic.test.ts | 4 ++++ packages/agent/src/canisterStatus/index.ts | 4 +++- packages/agent/src/polling/index.ts | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/e2e/node/basic/basic.test.ts b/e2e/node/basic/basic.test.ts index b918a8774..2e69fcd9e 100644 --- a/e2e/node/basic/basic.test.ts +++ b/e2e/node/basic/basic.test.ts @@ -14,6 +14,10 @@ test('read_state', async () => { const response = await resolvedAgent.readState(canisterId, { paths: [path], }); + const rootKey = + resolvedAgent.rootKey == null + ? resolvedAgent.fetchRootKey() + : Promise.resolve(resolvedAgent.rootKey); const cert = new Certificate(response.certificate, resolvedAgent.fetchRootKey()); expect(() => cert.lookup(path)).toThrow(/Cannot lookup unverified certificate/); diff --git a/packages/agent/src/canisterStatus/index.ts b/packages/agent/src/canisterStatus/index.ts index 370f15cb3..d539fba81 100644 --- a/packages/agent/src/canisterStatus/index.ts +++ b/packages/agent/src/canisterStatus/index.ts @@ -87,7 +87,9 @@ export const request = async (options: { const response = await agent.readState(canisterId, { paths: [encodedPaths[index]], }); - const cert = new Certificate(response.certificate, agent.fetchRootKey()); + const rootKey = + agent.rootKey == null ? agent.fetchRootKey() : Promise.resolve(agent.rootKey); + const cert = new Certificate(response.certificate, rootKey); const verified = await cert.verify(canisterId); if (!verified) { throw new Error( diff --git a/packages/agent/src/polling/index.ts b/packages/agent/src/polling/index.ts index a9e7dfe2f..987ad3d8e 100644 --- a/packages/agent/src/polling/index.ts +++ b/packages/agent/src/polling/index.ts @@ -29,7 +29,8 @@ 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.certificate, agent.fetchRootKey()); + const rootKey = agent.rootKey == null ? agent.fetchRootKey() : Promise.resolve(agent.rootKey); + const cert = new Certificate(state.certificate, rootKey); const verified = await cert.verify(canisterId); if (!verified) { throw new Error('Fail to verify certificate'); From 76ac41cef6e2701c1aa3c48a56f96ee5c11c40be Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Tue, 7 Jun 2022 12:47:39 +0200 Subject: [PATCH 07/15] Bump version up --- demos/ledgerhq/package.json | 2 +- demos/sample-javascript/package.json | 2 +- docs/generated/changelog.html | 8 ++++++++ e2e/node/package.json | 10 +++++----- package.json | 2 +- packages/agent/package.json | 6 +++--- packages/auth-client/package.json | 10 +++++----- packages/authentication/package.json | 8 ++++---- packages/candid/package.json | 2 +- packages/identity-ledgerhq/package.json | 8 ++++---- packages/identity/package.json | 6 +++--- packages/principal/package.json | 2 +- 12 files changed, 37 insertions(+), 29 deletions(-) diff --git a/demos/ledgerhq/package.json b/demos/ledgerhq/package.json index 8869a5aa2..b03f9a972 100644 --- a/demos/ledgerhq/package.json +++ b/demos/ledgerhq/package.json @@ -27,5 +27,5 @@ "test:coverage": "", "test": "" }, - "version": "0.11.3" + "version": "0.12.0" } diff --git a/demos/sample-javascript/package.json b/demos/sample-javascript/package.json index 70155651b..5c8f12a70 100644 --- a/demos/sample-javascript/package.json +++ b/demos/sample-javascript/package.json @@ -24,5 +24,5 @@ "test:coverage": "", "test": "" }, - "version": "0.11.3" + "version": "0.12.0" } diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html index f212bd2d4..e48cec12b 100644 --- a/docs/generated/changelog.html +++ b/docs/generated/changelog.html @@ -10,6 +10,14 @@

Agent-JS Changelog

+

Version 0.12.0

+
    +
  • + Changes the certificate verification interface and fixed its logic. The constructor now + takes a root key, and verification takes a canister ID. Additionally, verification now + checks that the delegation is authoritative for the given canister ID. +
  • +

Version 0.11.2

  • diff --git a/e2e/node/package.json b/e2e/node/package.json index 4cb4bf7f3..26793797c 100644 --- a/e2e/node/package.json +++ b/e2e/node/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@do-not-publish/ic-node-e2e-tests", - "version": "0.11.3", + "version": "0.12.0", "scripts": { "ci": "npm run e2e", "e2e": "jest --verbose", @@ -16,10 +16,10 @@ "test": "" }, "dependencies": { - "@dfinity/agent": "^0.11.3", - "@dfinity/authentication": "^0.11.3", - "@dfinity/identity": "^0.11.3", - "@dfinity/principal": "^0.11.3", + "@dfinity/agent": "^0.12.0", + "@dfinity/authentication": "^0.12.0", + "@dfinity/identity": "^0.12.0", + "@dfinity/principal": "^0.12.0", "@trust/webcrypto": "^0.9.2", "@types/base64-js": "^1.2.5", "@types/jest": "^26.0.23", diff --git a/package.json b/package.json index 1535ad9de..106b6b53c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/agent-monorepo", - "version": "0.11.3", + "version": "0.12.0", "private": true, "description": "Use an Agent to interact with the Internet Computer from your JavaScript program.", "dependencies": { diff --git a/packages/agent/package.json b/packages/agent/package.json index 4db353aae..17f7cac26 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/agent", - "version": "0.11.3", + "version": "0.12.0", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to interact with the Internet Computer", @@ -49,8 +49,8 @@ "tslint": "tslint --project tsconfig.json --config tslint.json" }, "peerDependencies": { - "@dfinity/candid": "^0.11.3", - "@dfinity/principal": "^0.11.3" + "@dfinity/candid": "^0.12.0", + "@dfinity/principal": "^0.12.0" }, "dependencies": { "base64-arraybuffer": "^0.2.0", diff --git a/packages/auth-client/package.json b/packages/auth-client/package.json index e152a7486..d3a963d6e 100644 --- a/packages/auth-client/package.json +++ b/packages/auth-client/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/auth-client", - "version": "0.11.3", + "version": "0.12.0", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to provide a simple integration with an IC Internet Identity", @@ -45,10 +45,10 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.11.3", - "@dfinity/authentication": "^0.11.3", - "@dfinity/identity": "^0.11.3", - "@dfinity/principal": "^0.11.3" + "@dfinity/agent": "^0.12.0", + "@dfinity/authentication": "^0.12.0", + "@dfinity/identity": "^0.12.0", + "@dfinity/principal": "^0.12.0" }, "devDependencies": { "@trust/webcrypto": "^0.9.2", diff --git a/packages/authentication/package.json b/packages/authentication/package.json index deb47478b..588ba1de7 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/authentication", - "version": "0.11.3", + "version": "0.12.0", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to manage identity and authentication with the Internet Computer", @@ -45,9 +45,9 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.11.3", - "@dfinity/identity": "^0.11.3", - "@dfinity/principal": "^0.11.3" + "@dfinity/agent": "^0.12.0", + "@dfinity/identity": "^0.12.0", + "@dfinity/principal": "^0.12.0" }, "devDependencies": { "@trust/webcrypto": "^0.9.2", diff --git a/packages/candid/package.json b/packages/candid/package.json index 76c9b1dc9..1a4a71d91 100644 --- a/packages/candid/package.json +++ b/packages/candid/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/candid", - "version": "0.11.3", + "version": "0.12.0", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to work with candid interfaces", diff --git a/packages/identity-ledgerhq/package.json b/packages/identity-ledgerhq/package.json index 31116b418..fc1c7d9eb 100644 --- a/packages/identity-ledgerhq/package.json +++ b/packages/identity-ledgerhq/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/identity-ledgerhq", - "version": "0.11.3", + "version": "0.12.0", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to manage identity and authentication with the Internet Computer", @@ -47,9 +47,9 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.11.3", - "@dfinity/identity": "^0.11.3", - "@dfinity/principal": "^0.11.3" + "@dfinity/agent": "^0.12.0", + "@dfinity/identity": "^0.12.0", + "@dfinity/principal": "^0.12.0" }, "dependencies": { "@ledgerhq/hw-transport": "^5.49.0", diff --git a/packages/identity/package.json b/packages/identity/package.json index 9f8628885..8ed90bb5b 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/identity", - "version": "0.11.3", + "version": "0.12.0", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to manage identity with the Internet Computer", @@ -44,8 +44,8 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.11.3", - "@dfinity/principal": "^0.11.3" + "@dfinity/agent": "^0.12.0", + "@dfinity/principal": "^0.12.0" }, "dependencies": { "@types/webappsec-credential-management": "^0.6.2", diff --git a/packages/principal/package.json b/packages/principal/package.json index 9a8eee96e..6bd86de0a 100644 --- a/packages/principal/package.json +++ b/packages/principal/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/principal", - "version": "0.11.3", + "version": "0.12.0", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to work with Internet Computer principals", From af999e47d093823fdee290d549c0c1600464062f Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Wed, 8 Jun 2022 12:04:14 +0200 Subject: [PATCH 08/15] Switch to a static constructor with automatic verification --- docs/generated/changelog.html | 8 ++- e2e/node/basic/basic.test.ts | 10 +--- packages/agent/src/canisterStatus/index.ts | 10 +--- packages/agent/src/certificate.test.ts | 45 +++++++++----- packages/agent/src/certificate.ts | 70 +++++++++++++--------- packages/agent/src/polling/index.ts | 8 +-- 6 files changed, 81 insertions(+), 70 deletions(-) diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html index e48cec12b..84aa5df4b 100644 --- a/docs/generated/changelog.html +++ b/docs/generated/changelog.html @@ -13,9 +13,11 @@

    Agent-JS Changelog

    Version 0.12.0

    • - Changes the certificate verification interface and fixed its logic. The constructor now - takes a root key, and verification takes a canister ID. Additionally, verification now - checks that the delegation is authoritative for the given canister ID. + 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 2e69fcd9e..be1a03587 100644 --- a/e2e/node/basic/basic.test.ts +++ b/e2e/node/basic/basic.test.ts @@ -14,14 +14,8 @@ test('read_state', async () => { const response = await resolvedAgent.readState(canisterId, { paths: [path], }); - const rootKey = - resolvedAgent.rootKey == null - ? resolvedAgent.fetchRootKey() - : Promise.resolve(resolvedAgent.rootKey); - const cert = new Certificate(response.certificate, resolvedAgent.fetchRootKey()); - - expect(() => cert.lookup(path)).toThrow(/Cannot lookup unverified certificate/); - expect(await cert.verify(canisterId)).toBe(true); + if (resolvedAgent.rootKey == null) throw new Error(`The agent doesn't have a root key yet`); + const cert = await Certificate.create(response.certificate, resolvedAgent.rootKey, 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/packages/agent/src/canisterStatus/index.ts b/packages/agent/src/canisterStatus/index.ts index d539fba81..97e63cc18 100644 --- a/packages/agent/src/canisterStatus/index.ts +++ b/packages/agent/src/canisterStatus/index.ts @@ -87,15 +87,7 @@ export const request = async (options: { const response = await agent.readState(canisterId, { paths: [encodedPaths[index]], }); - const rootKey = - agent.rootKey == null ? agent.fetchRootKey() : Promise.resolve(agent.rootKey); - const cert = new Certificate(response.certificate, rootKey); - const verified = await cert.verify(canisterId); - 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(response.certificate, agent.rootKey, 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 cfc152a92..077db05a1 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -3,12 +3,11 @@ * an instance of ArrayBuffer). * @jest-environment node */ -import { fromHexString } from '@dfinity/candid'; import * as cbor from './cbor'; import * as Cert from './certificate'; import { fromHex, toHex } from './utils/buffer'; import { Principal } from '@dfinity/principal'; -import { verify } from 'crypto'; +import { AgentError } from './errors'; function label(str: string): ArrayBuffer { return new TextEncoder().encode(str); @@ -142,30 +141,44 @@ const SAMPLE_CERT: string = test('delegation works for canisters within the subnet range', async () => { const canisterId = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'); - const cert = new Cert.Certificate(fromHex(SAMPLE_CERT), Promise.resolve(fromHex(IC_ROOT_KEY))); - const result = await cert.verify(canisterId); - expect(result).toEqual(true); + // Test that create doesn't throw + const cert = await Cert.Certificate.create( + fromHex(SAMPLE_CERT), + fromHex(IC_ROOT_KEY), + canisterId, + ); }); +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. const canisterId = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); - const cert = new Cert.Certificate(fromHex(SAMPLE_CERT), Promise.resolve(fromHex(IC_ROOT_KEY))); - // This is a bit crufty; verify returns a boolean, but can also indicate verification errors - // by throwing exceptions try { - const result = await cert.verify(canisterId); - expect(result).toEqual(false); - } catch (e) {} + const cert = await Cert.Certificate.create( + fromHex(SAMPLE_CERT), + fromHex(IC_ROOT_KEY), + canisterId, + ); + fail('The create method should throw on an invalid certificate'); + } catch (err) { + if (!(err instanceof AgentError) || !err.message.includes('Invalid certificate')) { + fail( + `The create method should throw an ${AgentError.name} mentioning an invalid certificate`, + ); + } + } }); // 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 () => { - const cert = new Cert.Certificate(fromHex(SAMPLE_CERT), Promise.resolve(fromHex(IC_ROOT_KEY))); - // This is a bit crufty; verify returns a boolean, but can also indicate verification errors - // by throwing exceptions - const result = await cert.verify(Principal.managementCanister()); - expect(result).toEqual(true); + const cert = await Cert.Certificate.create( + fromHex(SAMPLE_CERT), + fromHex(IC_ROOT_KEY), + Principal.managementCanister(), + ); }); diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index 20e8ae10c..05cd92d0c 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -6,12 +6,11 @@ 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}`); } } @@ -103,18 +102,35 @@ export class Certificate { private readonly cert: Cert; private verified = false; - constructor(certificate: ArrayBuffer, private _rootKey: Promise) { + /** + * Create a new instance of a certificate, automatically verifying it. + * @throws {CertificateVerificationError} + */ + public static async create( + certificate: ArrayBuffer, + rootKey: ArrayBuffer, + canisterId: Principal, + ): Promise { + const cert = new Certificate(certificate, rootKey, 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(canisterId: Principal): Promise { + private async verify(): Promise { const rootHash = await reconstruct(this.cert.tree); - const derKey = await this._checkDelegationAndGetKey(canisterId, 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); @@ -123,28 +139,22 @@ export class Certificate { return res; } - protected checkState(): void { - if (!this.verified) { - throw new UnverifiedCertificateError(); - } - } - - private async _checkDelegationAndGetKey( - canisterId: Principal, - d?: Delegation, - ): Promise { + private async _checkDelegationAndGetKey(d?: Delegation): Promise { if (!d) { return this._rootKey; } - const cert: Certificate = new Certificate(d.certificate, this._rootKey); - if (!(await cert.verify(canisterId))) { - throw new Error('fail to verify delegation certificate'); - } + const cert: Certificate = await Certificate.create( + d.certificate, + this._rootKey, + this._canisterId, + ); - if (canisterId.compareTo(Principal.managementCanister()) != 'eq') { + if (this._canisterId.compareTo(Principal.managementCanister()) != 'eq') { const rangeLookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); if (!rangeLookup) { - throw new Error(`Could not find canister ranges for subnet 0x${toHex(d.subnet_id)}`); + 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 => [ @@ -152,10 +162,14 @@ export class Certificate { Principal.fromUint8Array(v[1]), ]); - const canisterInRange = ranges.some(r => r[0].ltEq(canisterId) && r[1].gtEq(canisterId)); + const canisterInRange = ranges.some( + r => r[0].ltEq(this._canisterId) && r[1].gtEq(this._canisterId), + ); if (!canisterInRange) { - throw new Error( - `Canister ${canisterId} not in range of delegations for subnet 0x${toHex(d.subnet_id)}`, + throw new CertificateVerificationError( + `Canister ${this._canisterId} not in range of delegations for subnet 0x${toHex( + d.subnet_id, + )}`, ); } } diff --git a/packages/agent/src/polling/index.ts b/packages/agent/src/polling/index.ts index 987ad3d8e..bde895810 100644 --- a/packages/agent/src/polling/index.ts +++ b/packages/agent/src/polling/index.ts @@ -29,12 +29,8 @@ export async function pollForResponse( ): Promise { const path = [new TextEncoder().encode('request_status'), requestId]; const state = await agent.readState(canisterId, { paths: [path] }); - const rootKey = agent.rootKey == null ? agent.fetchRootKey() : Promise.resolve(agent.rootKey); - const cert = new Certificate(state.certificate, rootKey); - const verified = await cert.verify(canisterId); - 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(state.certificate, agent.rootKey, canisterId); const maybeBuf = cert.lookup([...path, new TextEncoder().encode('status')]); let status; if (typeof maybeBuf === 'undefined') { From 4203c2e85ca0d56df153321d8ced75512d8921dc Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Wed, 8 Jun 2022 13:21:59 +0200 Subject: [PATCH 09/15] Fail on wrong signature --- packages/agent/src/certificate.test.ts | 59 ++++++++++++++------------ packages/agent/src/certificate.ts | 15 ++++--- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/packages/agent/src/certificate.test.ts b/packages/agent/src/certificate.test.ts index 077db05a1..d11c9cbad 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -7,7 +7,7 @@ import * as cbor from './cbor'; import * as Cert from './certificate'; import { fromHex, toHex } from './utils/buffer'; import { Principal } from '@dfinity/principal'; -import { AgentError } from './errors'; +import { NodeBuilderFlags } from 'typescript'; function label(str: string): ArrayBuffer { return new TextEncoder().encode(str); @@ -141,12 +141,9 @@ const SAMPLE_CERT: string = test('delegation works for canisters within the subnet range', async () => { const canisterId = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'); - // Test that create doesn't throw - const cert = await Cert.Certificate.create( - fromHex(SAMPLE_CERT), - fromHex(IC_ROOT_KEY), - canisterId, - ); + await expect( + Cert.Certificate.create(fromHex(SAMPLE_CERT), fromHex(IC_ROOT_KEY), canisterId), + ).resolves.not.toThrow(); }); function fail(reason) { @@ -156,29 +153,39 @@ function fail(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. const canisterId = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); - try { - const cert = await Cert.Certificate.create( - fromHex(SAMPLE_CERT), - fromHex(IC_ROOT_KEY), - canisterId, - ); - fail('The create method should throw on an invalid certificate'); - } catch (err) { - if (!(err instanceof AgentError) || !err.message.includes('Invalid certificate')) { - fail( - `The create method should throw an ${AgentError.name} mentioning an invalid certificate`, - ); - } - } + await expect( + Cert.Certificate.create(fromHex(SAMPLE_CERT), fromHex(IC_ROOT_KEY), canisterId), + ).rejects.toThrow(/Invalid certificate/); }); // 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 () => { - const cert = await Cert.Certificate.create( - fromHex(SAMPLE_CERT), - fromHex(IC_ROOT_KEY), - Principal.managementCanister(), - ); + await expect( + Cert.Certificate.create( + fromHex(SAMPLE_CERT), + fromHex(IC_ROOT_KEY), + 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( + badCertEncoded, + fromHex(IC_ROOT_KEY), + 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 05cd92d0c..4686d8388 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -100,7 +100,6 @@ function isBufferEqual(a: ArrayBuffer, b: ArrayBuffer): boolean { export class Certificate { private readonly cert: Cert; - private verified = false; /** * Create a new instance of a certificate, automatically verifying it. @@ -128,15 +127,21 @@ export class Certificate { return lookup_path(path, this.cert.tree); } - private async verify(): Promise { + private async verify(): Promise { const rootHash = await reconstruct(this.cert.tree); 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; + 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 _checkDelegationAndGetKey(d?: Delegation): Promise { From d4d11dc15d6b139c0b3feefc551630138c231c3c Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Wed, 8 Jun 2022 13:38:25 +0200 Subject: [PATCH 10/15] Change the expected error in the MITM test --- e2e/node/basic/mitm.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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!'); }); From cd997dffae1d9cd0074400437d9049cf67e09e76 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Wed, 8 Jun 2022 13:42:36 +0200 Subject: [PATCH 11/15] Avoid any potential coercion --- packages/agent/src/certificate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index 4686d8388..0fc46b8f3 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -154,7 +154,7 @@ export class Certificate { this._canisterId, ); - if (this._canisterId.compareTo(Principal.managementCanister()) != 'eq') { + if (this._canisterId.compareTo(Principal.managementCanister()) !== 'eq') { const rangeLookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); if (!rangeLookup) { throw new CertificateVerificationError( From 5e98356646fa47bfae03426bf820fd38d599c013 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Tue, 14 Jun 2022 10:45:24 -0700 Subject: [PATCH 12/15] chore: reverts package versions --- demos/ledgerhq/package.json | 2 +- demos/sample-javascript/package.json | 2 +- e2e/node/package.json | 10 +-- package-lock.json | 113 ++++++++++++------------ package.json | 2 +- packages/agent/package.json | 6 +- packages/auth-client/package.json | 10 +-- packages/authentication/package.json | 8 +- packages/candid/package.json | 2 +- packages/identity-ledgerhq/package.json | 8 +- packages/identity/package.json | 6 +- packages/principal/package.json | 2 +- 12 files changed, 87 insertions(+), 84 deletions(-) diff --git a/demos/ledgerhq/package.json b/demos/ledgerhq/package.json index b03f9a972..8869a5aa2 100644 --- a/demos/ledgerhq/package.json +++ b/demos/ledgerhq/package.json @@ -27,5 +27,5 @@ "test:coverage": "", "test": "" }, - "version": "0.12.0" + "version": "0.11.3" } diff --git a/demos/sample-javascript/package.json b/demos/sample-javascript/package.json index 5c8f12a70..70155651b 100644 --- a/demos/sample-javascript/package.json +++ b/demos/sample-javascript/package.json @@ -24,5 +24,5 @@ "test:coverage": "", "test": "" }, - "version": "0.12.0" + "version": "0.11.3" } diff --git a/e2e/node/package.json b/e2e/node/package.json index 26793797c..4cb4bf7f3 100644 --- a/e2e/node/package.json +++ b/e2e/node/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@do-not-publish/ic-node-e2e-tests", - "version": "0.12.0", + "version": "0.11.3", "scripts": { "ci": "npm run e2e", "e2e": "jest --verbose", @@ -16,10 +16,10 @@ "test": "" }, "dependencies": { - "@dfinity/agent": "^0.12.0", - "@dfinity/authentication": "^0.12.0", - "@dfinity/identity": "^0.12.0", - "@dfinity/principal": "^0.12.0", + "@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", 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/package.json b/package.json index 106b6b53c..1535ad9de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/agent-monorepo", - "version": "0.12.0", + "version": "0.11.3", "private": true, "description": "Use an Agent to interact with the Internet Computer from your JavaScript program.", "dependencies": { diff --git a/packages/agent/package.json b/packages/agent/package.json index 17f7cac26..4db353aae 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/agent", - "version": "0.12.0", + "version": "0.11.3", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to interact with the Internet Computer", @@ -49,8 +49,8 @@ "tslint": "tslint --project tsconfig.json --config tslint.json" }, "peerDependencies": { - "@dfinity/candid": "^0.12.0", - "@dfinity/principal": "^0.12.0" + "@dfinity/candid": "^0.11.3", + "@dfinity/principal": "^0.11.3" }, "dependencies": { "base64-arraybuffer": "^0.2.0", diff --git a/packages/auth-client/package.json b/packages/auth-client/package.json index d3a963d6e..e152a7486 100644 --- a/packages/auth-client/package.json +++ b/packages/auth-client/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/auth-client", - "version": "0.12.0", + "version": "0.11.3", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to provide a simple integration with an IC Internet Identity", @@ -45,10 +45,10 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.12.0", - "@dfinity/authentication": "^0.12.0", - "@dfinity/identity": "^0.12.0", - "@dfinity/principal": "^0.12.0" + "@dfinity/agent": "^0.11.3", + "@dfinity/authentication": "^0.11.3", + "@dfinity/identity": "^0.11.3", + "@dfinity/principal": "^0.11.3" }, "devDependencies": { "@trust/webcrypto": "^0.9.2", diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 588ba1de7..deb47478b 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/authentication", - "version": "0.12.0", + "version": "0.11.3", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to manage identity and authentication with the Internet Computer", @@ -45,9 +45,9 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.12.0", - "@dfinity/identity": "^0.12.0", - "@dfinity/principal": "^0.12.0" + "@dfinity/agent": "^0.11.3", + "@dfinity/identity": "^0.11.3", + "@dfinity/principal": "^0.11.3" }, "devDependencies": { "@trust/webcrypto": "^0.9.2", diff --git a/packages/candid/package.json b/packages/candid/package.json index 1a4a71d91..76c9b1dc9 100644 --- a/packages/candid/package.json +++ b/packages/candid/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/candid", - "version": "0.12.0", + "version": "0.11.3", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to work with candid interfaces", diff --git a/packages/identity-ledgerhq/package.json b/packages/identity-ledgerhq/package.json index fc1c7d9eb..31116b418 100644 --- a/packages/identity-ledgerhq/package.json +++ b/packages/identity-ledgerhq/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/identity-ledgerhq", - "version": "0.12.0", + "version": "0.11.3", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to manage identity and authentication with the Internet Computer", @@ -47,9 +47,9 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.12.0", - "@dfinity/identity": "^0.12.0", - "@dfinity/principal": "^0.12.0" + "@dfinity/agent": "^0.11.3", + "@dfinity/identity": "^0.11.3", + "@dfinity/principal": "^0.11.3" }, "dependencies": { "@ledgerhq/hw-transport": "^5.49.0", diff --git a/packages/identity/package.json b/packages/identity/package.json index 8ed90bb5b..9f8628885 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/identity", - "version": "0.12.0", + "version": "0.11.3", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to manage identity with the Internet Computer", @@ -44,8 +44,8 @@ "test:coverage": "jest --verbose --collectCoverage" }, "peerDependencies": { - "@dfinity/agent": "^0.12.0", - "@dfinity/principal": "^0.12.0" + "@dfinity/agent": "^0.11.3", + "@dfinity/principal": "^0.11.3" }, "dependencies": { "@types/webappsec-credential-management": "^0.6.2", diff --git a/packages/principal/package.json b/packages/principal/package.json index 6bd86de0a..9a8eee96e 100644 --- a/packages/principal/package.json +++ b/packages/principal/package.json @@ -1,6 +1,6 @@ { "name": "@dfinity/principal", - "version": "0.12.0", + "version": "0.11.3", "author": "DFINITY Stiftung ", "license": "Apache-2.0", "description": "JavaScript and TypeScript library to work with Internet Computer principals", From 6889f724bc9641a41b88c85a41c7bde319c6373d Mon Sep 17 00:00:00 2001 From: oggy-dfin <89794951+oggy-dfin@users.noreply.github.com> Date: Fri, 17 Jun 2022 15:03:36 +0200 Subject: [PATCH 13/15] Apply suggestions from code review Co-authored-by: Kyle Peacock --- packages/principal/src/index.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/principal/src/index.ts b/packages/principal/src/index.ts index 3864b2127..857282a60 100644 --- a/packages/principal/src/index.ts +++ b/packages/principal/src/index.ts @@ -18,6 +18,10 @@ 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); } @@ -103,6 +107,11 @@ export class Principal { 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'; @@ -114,11 +123,21 @@ export class Principal { 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'; From 839a6ea5cf975a06b6fe0e18d67dccd405a25a97 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Fri, 17 Jun 2022 16:29:17 +0200 Subject: [PATCH 14/15] Use an options object for Certificate.create --- e2e/node/basic/basic.test.ts | 6 ++- packages/agent/src/canisterStatus/index.ts | 6 ++- packages/agent/src/certificate.test.ts | 32 ++++++++++------ packages/agent/src/certificate.ts | 43 ++++++++++++++++------ packages/agent/src/polling/index.ts | 6 ++- 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/e2e/node/basic/basic.test.ts b/e2e/node/basic/basic.test.ts index be1a03587..a085873f4 100644 --- a/e2e/node/basic/basic.test.ts +++ b/e2e/node/basic/basic.test.ts @@ -15,7 +15,11 @@ test('read_state', async () => { paths: [path], }); if (resolvedAgent.rootKey == null) throw new Error(`The agent doesn't have a root key yet`); - const cert = await Certificate.create(response.certificate, resolvedAgent.rootKey, canisterId); + 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/packages/agent/src/canisterStatus/index.ts b/packages/agent/src/canisterStatus/index.ts index 97e63cc18..35c85aabd 100644 --- a/packages/agent/src/canisterStatus/index.ts +++ b/packages/agent/src/canisterStatus/index.ts @@ -87,7 +87,11 @@ export const request = async (options: { const response = await agent.readState(canisterId, { paths: [encodedPaths[index]], }); - const cert = await Certificate.create(response.certificate, agent.rootKey, canisterId); + 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 d11c9cbad..c51545282 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -142,7 +142,11 @@ const SAMPLE_CERT: string = test('delegation works for canisters within the subnet range', async () => { const canisterId = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'); await expect( - Cert.Certificate.create(fromHex(SAMPLE_CERT), fromHex(IC_ROOT_KEY), canisterId), + Cert.Certificate.create({ + certificate: fromHex(SAMPLE_CERT), + rootKey: fromHex(IC_ROOT_KEY), + canisterId: canisterId, + }), ).resolves.not.toThrow(); }); @@ -154,7 +158,11 @@ 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. const canisterId = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); await expect( - Cert.Certificate.create(fromHex(SAMPLE_CERT), fromHex(IC_ROOT_KEY), canisterId), + Cert.Certificate.create({ + certificate: fromHex(SAMPLE_CERT), + rootKey: fromHex(IC_ROOT_KEY), + canisterId: canisterId, + }), ).rejects.toThrow(/Invalid certificate/); }); @@ -163,11 +171,11 @@ test('delegation check fails for canisters outside of the subnet range', async ( // we shouldn't check the delegations. test('delegation check succeeds for the management canister', async () => { await expect( - Cert.Certificate.create( - fromHex(SAMPLE_CERT), - fromHex(IC_ROOT_KEY), - Principal.managementCanister(), - ), + Cert.Certificate.create({ + certificate: fromHex(SAMPLE_CERT), + rootKey: fromHex(IC_ROOT_KEY), + canisterId: Principal.managementCanister(), + }), ).resolves.not.toThrow(); }); @@ -182,10 +190,10 @@ test('certificate verification fails for an invalid signature', async () => { badCert.signature = new ArrayBuffer(badCert.signature.byteLength); const badCertEncoded = cbor.encode(badCert); await expect( - Cert.Certificate.create( - badCertEncoded, - fromHex(IC_ROOT_KEY), - Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'), - ), + 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 0fc46b8f3..d305d1695 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -98,19 +98,38 @@ 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 that generated the certificate. + */ + canisterId: Principal; +} + export class Certificate { private readonly cert: Cert; /** - * Create a new instance of a certificate, automatically verifying it. + * 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 root key to verify against * @throws {CertificateVerificationError} */ - public static async create( - certificate: ArrayBuffer, - rootKey: ArrayBuffer, - canisterId: Principal, - ): Promise { - const cert = new Certificate(certificate, rootKey, canisterId); + public static async create(options: CreateCertificateOptions): Promise { + const cert = new Certificate(options.certificate, options.rootKey, options.canisterId); await cert.verify(); return cert; } @@ -148,11 +167,11 @@ export class Certificate { if (!d) { return this._rootKey; } - const cert: Certificate = await Certificate.create( - d.certificate, - this._rootKey, - this._canisterId, - ); + 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']); diff --git a/packages/agent/src/polling/index.ts b/packages/agent/src/polling/index.ts index bde895810..f71123a90 100644 --- a/packages/agent/src/polling/index.ts +++ b/packages/agent/src/polling/index.ts @@ -30,7 +30,11 @@ export async function pollForResponse( const path = [new TextEncoder().encode('request_status'), requestId]; const state = await agent.readState(canisterId, { paths: [path] }); if (agent.rootKey == null) throw new Error('Agent root key not initialized before polling'); - const cert = await Certificate.create(state.certificate, agent.rootKey, canisterId); + 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') { From e7f9eb6ffbfd40d30766259d3f103a6992bf9194 Mon Sep 17 00:00:00 2001 From: Ognjen Maric Date: Tue, 28 Jun 2022 11:27:03 +0200 Subject: [PATCH 15/15] Robin's review comments --- packages/agent/src/certificate.test.ts | 52 ++++++++++++++++++-------- packages/agent/src/certificate.ts | 5 ++- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/packages/agent/src/certificate.test.ts b/packages/agent/src/certificate.test.ts index c51545282..3bbea0ca8 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -140,14 +140,25 @@ const SAMPLE_CERT: string = 'd9d9f7a364747265658301830182045820250f5e26868d9c1ea7ab29cbe9c15bf1c47c0d7605e803e39e375a7fe09c6ebb830183024e726571756573745f7374617475738301820458204b268227774ec77ff2b37ecb12157329d54cf376694bdd59ded7803efd82386f83025820edad510eaaa08ed2acd4781324e6446269da6753ec17760f206bbe81c465ff528301830183024b72656a6563745f636f64658203410383024e72656a6563745f6d6573736167658203584443616e69737465722069766733372d71696161612d61616161622d61616167612d63616920686173206e6f20757064617465206d6574686f64202772656769737465722783024673746174757382034872656a65637465648204582097232f31f6ab7ca4fe53eb6568fc3e02bc22fe94ab31d010e5fb3c642301f1608301820458203a48d1fc213d49307103104f7d72c2b5930edba8787b90631f343b3aa68a5f0a83024474696d65820349e2dc939091c696eb16697369676e6174757265583089a2be21b5fa8ac9fab1527e041327ce899d7da971436a1f2165393947b4d942365bfe5488710e61a619ba48388a21b16a64656c65676174696f6ea2697375626e65745f6964581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac4026b6365727469666963617465590231d9d9f7a26474726565830182045820ae023f28c3b9d966c8fb09f9ed755c828aadb5152e00aaf700b18c9c067294b483018302467375626e6574830182045820e83bb025f6574c8f31233dc0fe289ff546dfa1e49bd6116dd6e8896d90a4946e830182045820e782619092d69d5bebf0924138bd4116b0156b5a95e25c358ea8cf7e7161a661830183018204582062513fa926c9a9ef803ac284d620f303189588e1d3904349ab63b6470856fc4883018204582060e9a344ced2c9c4a96a0197fd585f2d259dbd193e4eada56239cac26087f9c58302581dd77b2a2f7199b9a8aec93fe6fb588661358cf12223e9a3af7b4ebac402830183024f63616e69737465725f72616e6765738203581bd9d9f781824a000000000020000001014a00000000002fffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361009933e1f89e8a3c4d7fdcccdbd518089e2bd4d8180a261f18d9c247a52768ebce98dc7328a39814a8f911086a1dd50cbe015e2a53b7bf78b55288893daa15c346640e8831d72a12bdedd979d28470c34823b8d1c3f4795d9c3984a247132e94fe82045820996f17bb926be3315745dea7282005a793b58e76afeb5d43d1a28ce29d2d158583024474696d6582034995b8aac0e4eda2ea16697369676e61747572655830ace9fcdd9bc977e05d6328f889dc4e7c99114c737a494653cb27a1f55c06f4555e0f160980af5ead098acc195010b2f7'; test('delegation works for canisters within the subnet range', async () => { - const canisterId = Principal.fromText('ivg37-qiaaa-aaaab-aaaga-cai'); - await expect( - Cert.Certificate.create({ - certificate: fromHex(SAMPLE_CERT), - rootKey: fromHex(IC_ROOT_KEY), - canisterId: canisterId, - }), - ).resolves.not.toThrow(); + // 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) { @@ -156,14 +167,23 @@ function fail(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. - const canisterId = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); - await expect( - Cert.Certificate.create({ - certificate: fromHex(SAMPLE_CERT), - rootKey: fromHex(IC_ROOT_KEY), - canisterId: canisterId, - }), - ).rejects.toThrow(/Invalid certificate/); + // 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 diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index d305d1695..a92824cd1 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -109,7 +109,8 @@ export interface CreateCertificateOptions { */ rootKey: ArrayBuffer; /** - * The effective canister ID of the request that generated the certificate. + * The effective canister ID of the request when verifying a response, or + * the signing canister ID when verifying a certified variable. */ canisterId: Principal; } @@ -125,7 +126,7 @@ export class Certificate { * @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 root key to verify against + * @param {Principal} options.canisterId The effective or signing canister ID * @throws {CertificateVerificationError} */ public static async create(options: CreateCertificateOptions): Promise {