diff --git a/packages/agent/src/bin/with_subnet_key.bin b/packages/agent/src/bin/with_subnet_key.bin new file mode 100644 index 00000000..770d6ba0 Binary files /dev/null and b/packages/agent/src/bin/with_subnet_key.bin differ diff --git a/packages/agent/src/certificate.test.ts b/packages/agent/src/certificate.test.ts index 06b8f2fe..2ff710e6 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -9,6 +9,7 @@ import { fromHex, toHex } from './utils/buffer'; import { Principal } from '@dfinity/principal'; import { decodeTime } from './utils/leb'; import { lookupResultToBuffer, lookup_path } from './certificate'; +import { readFileSync } from 'fs'; function label(str: string): ArrayBuffer { return new TextEncoder().encode(str); @@ -258,3 +259,27 @@ test('certificate verification fails if the time of the certificate is > 5 minut }), ).rejects.toThrow('Invalid certificate: Certificate is signed more than 5 minutes in the future'); }); + +test('certificate verification fails on nested delegations', async () => { + // This is a recorded certificate from a read_state request to the II + // subnet, with the /subnet tree included. Thus, it could be used as its + // own delegation, according to the old interface spec definition. + const withSubnetSubtree = readFileSync('packages/agent/src/bin/with_subnet_key.bin'); + const canisterId = Principal.fromText("rdmx6-jaaaa-aaaaa-aaadq-cai"); + const subnetId = Principal.fromText("uzr34-akd3s-xrdag-3ql62-ocgoh-ld2ao-tamcv-54e7j-krwgb-2gm4z-oqe"); + jest.setSystemTime(new Date(Date.parse('2023-12-12T10:40:00.652Z'))); + let cert: Cert.Cert = cbor.decode(withSubnetSubtree); + const overlyNested = cbor.encode({ + tree: cert.tree, + signature: cert.signature, + delegation: { + subnet_id: subnetId.toUint8Array(), + certificate: withSubnetSubtree, + }, + }) + await expect(Cert.Certificate.create({ + certificate: overlyNested, + rootKey: fromHex(IC_ROOT_KEY), + canisterId: canisterId, + })).rejects.toThrow('Invalid certificate: Delegation certificates cannot be nested'); +}); diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index 376483c6..af156aa5 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -170,20 +170,24 @@ export class Certificate { * @throws {CertificateVerificationError} */ public static async create(options: CreateCertificateOptions): Promise { + const cert = Certificate.createUnverified(options); + + await cert.verify(); + return cert; + } + + private static createUnverified(options: CreateCertificateOptions): Certificate { let blsVerify = options.blsVerify; if (!blsVerify) { blsVerify = bls.blsVerify; } - const cert = new Certificate( + return new Certificate( options.certificate, options.rootKey, options.canisterId, blsVerify, options.maxAgeInMinutes, ); - - await cert.verify(); - return cert; } private constructor( @@ -259,7 +263,7 @@ export class Certificate { return this._rootKey; } - const cert: Certificate = await Certificate.create({ + const cert: Certificate = await Certificate.createUnverified({ certificate: d.certificate, rootKey: this._rootKey, canisterId: this._canisterId, @@ -268,6 +272,12 @@ export class Certificate { maxAgeInMinutes: Infinity, }); + if (cert.cert.delegation) { + throw new CertificateVerificationError('Delegation certificates cannot be nested'); + } + + await cert.verify(); + const canisterInRange = check_canister_ranges({ canisterId: this._canisterId, subnetId: Principal.fromUint8Array(new Uint8Array(d.subnet_id)),