diff --git a/packages/agent/src/certificate.test.ts b/packages/agent/src/certificate.test.ts index 51d9f5e73..6c846ec36 100644 --- a/packages/agent/src/certificate.test.ts +++ b/packages/agent/src/certificate.test.ts @@ -170,31 +170,21 @@ test('delegation works for canisters within the subnet range', async () => { // to // 0x00000000002FFFFF0101 const rangeStart = Principal.fromHex('00000000002000000101'); - jest.setSystemTime(new Date(1645093069668)); - - expect( - Cert.Certificate.create({ - certificate: fromHex(SAMPLE_CERT), - rootKey: fromHex(IC_ROOT_KEY), - canisterId: rangeStart, - }), - ).rejects.toThrow(); - - // expect( - // Cert.Certificate.create({ - // certificate: fromHex(SAMPLE_CERT), - // rootKey: fromHex(IC_ROOT_KEY), - // canisterId: rangeInterior, - // }), - // ).resolves.not.toThrow(); - - // expect( - // Cert.Certificate.create({ - // certificate: fromHex(SAMPLE_CERT), - // rootKey: fromHex(IC_ROOT_KEY), - // canisterId: rangeEnd, - // }), - // ).resolves.not.toThrow(); + const rangeInterior = Principal.fromHex('000000000020000C0101'); + const rangeEnd = Principal.fromHex('00000000002FFFFF0101'); + async function verifies(canisterId) { + jest.setSystemTime(new Date(Date.parse('2022-02-23T07:38:00.652Z'))); + 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); }); test('delegation check fails for canisters outside of the subnet range', async () => { @@ -240,10 +230,9 @@ test('certificate verification fails for an invalid signature', async () => { test('certificate verification fails if the time of the certificate is > 5 minutes in the past', async () => { const badCert: FakeCert = cbor.decode(fromHex(SAMPLE_CERT)); const badCertEncoded = cbor.encode(badCert); - const sevenMinutesFuture = Date.parse( - 'Thu Feb 17 2022 02:24:00 GMT-0800 (Pacific Standard Time)', - ); - jest.setSystemTime(sevenMinutesFuture); + + const tenMinutesFuture = Date.parse('2022-02-23T07:48:00.652Z'); + jest.setSystemTime(tenMinutesFuture); await expect( Cert.Certificate.create({ certificate: badCertEncoded, @@ -256,8 +245,8 @@ test('certificate verification fails if the time of the certificate is > 5 minut test('certificate verification fails if the time of the certificate is > 5 minutes in the future', async () => { const badCert: FakeCert = cbor.decode(fromHex(SAMPLE_CERT)); const badCertEncoded = cbor.encode(badCert); - const sevenMinutesPast = Date.parse('Thu Feb 17 2022 02:10:00 GMT-0800 (Pacific Standard Time)'); - jest.setSystemTime(sevenMinutesPast); + const tenMinutesPast = Date.parse('2022-02-23T07:28:00.652Z'); + jest.setSystemTime(tenMinutesPast); await expect( Cert.Certificate.create({ diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index 38c53df90..0c79ca14a 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -120,6 +120,14 @@ export interface CreateCertificateOptions { * BLS Verification strategy. Default strategy uses wasm for performance, but that may not be available in all contexts. */ blsVerify?: VerifyFunc; + + /** + * The maximum age of the certificate in minutes. Default is 5 minutes. + * @default 5 + * This is used to verify the time the certificate was signed, particularly for validating Delegation certificates, which can live for longer than the default window of +/- 5 minutes. If the certificate is + * older than the specified age, it will fail verification. + */ + maxAgeInMinutes?: number; } export class Certificate { @@ -128,12 +136,12 @@ export class 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} + * @constructs Certificate + * @param {CreateCertificateOptions} options {@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 + * @param {number} options.maxAgeInMinutes The maximum age of the certificate in minutes. Default is 5 minutes. * @throws {CertificateVerificationError} */ public static async create(options: CreateCertificateOptions): Promise { @@ -146,6 +154,7 @@ export class Certificate { options.rootKey, options.canisterId, blsVerify, + options.maxAgeInMinutes, ); await cert.verify(); return cert; @@ -156,6 +165,8 @@ export class Certificate { private _rootKey: ArrayBuffer, private _canisterId: Principal, private _blsVerify: VerifyFunc, + // Default to 5 minutes + private _maxAgeInMinutes: number = 5, ) { this.cert = cbor.decode(new Uint8Array(certificate)); } @@ -177,24 +188,28 @@ export class Certificate { // Should never happen - time is always present in IC certificates throw new CertificateVerificationError('Certificate does not contain a time'); } - const certTime = decodeTime(lookupTime); - const now = new Date(Date.now()); const FIVE_MINUTES_IN_MSEC = 5 * 60 * 1000; + const MAX_AGE_IN_MSEC = this._maxAgeInMinutes * 60 * 1000; + const now = Date.now(); + const earliestCertificateTime = now - MAX_AGE_IN_MSEC; + const fiveMinutesFromNow = now + FIVE_MINUTES_IN_MSEC; + + const certTime = decodeTime(lookupTime); - if (certTime.getTime() - now.getTime() > FIVE_MINUTES_IN_MSEC) { + if (certTime.getTime() < earliestCertificateTime) { throw new CertificateVerificationError( - 'Certificate is signed more than 5 minutes in the future. Certificate time: ' + + 'Certificate is signed more than 5 minutes in the past. Certificate time: ' + certTime.toISOString() + ' Current time: ' + - now.toISOString(), + new Date(now).toISOString(), ); - } else if (certTime.getTime() - now.getTime() < -FIVE_MINUTES_IN_MSEC) { + } else if (certTime.getTime() > fiveMinutesFromNow) { throw new CertificateVerificationError( - 'Certificate is signed more than 5 minutes in the past. Certificate time: ' + + 'Certificate is signed more than 5 minutes in the future. Certificate time: ' + certTime.toISOString() + ' Current time: ' + - now.toISOString(), + new Date(now).toISOString(), ); } @@ -212,10 +227,14 @@ export class Certificate { if (!d) { return this._rootKey; } + const cert: Certificate = await Certificate.create({ certificate: d.certificate, rootKey: this._rootKey, canisterId: this._canisterId, + blsVerify: this._blsVerify, + // Maximum age of 30 days for delegation certificates + maxAgeInMinutes: 60 * 24 * 30, }); const rangeLookup = cert.lookup(['subnet', d.subnet_id, 'canister_ranges']); diff --git a/packages/agent/src/utils/leb.ts b/packages/agent/src/utils/leb.ts index 5f04cabc4..d7653f2b0 100644 --- a/packages/agent/src/utils/leb.ts +++ b/packages/agent/src/utils/leb.ts @@ -9,5 +9,5 @@ export const decodeTime = (buf: ArrayBuffer): Date => { const decoded = decodeLeb128(buf); // nanoseconds to milliseconds - return new Date(Number(decoded / BigInt(1_000_000))); + return new Date(Number(decoded) / 1_000_000); }; diff --git a/packages/bls-verify/src/index.test.ts b/packages/bls-verify/src/index.test.ts index 77e9635b4..fa835156f 100644 --- a/packages/bls-verify/src/index.test.ts +++ b/packages/bls-verify/src/index.test.ts @@ -1,7 +1,7 @@ import { blsVerify } from './index'; import * as Cert from '../../agent/src/certificate'; import * as cbor from '../../agent/src/cbor'; -import { fromHex, toHex } from '../../agent/src/utils/buffer'; +import { fromHex } from '../../agent/src/utils/buffer'; import { Principal } from '@dfinity/principal'; // Root public key for the IC main net, encoded as hex @@ -27,7 +27,9 @@ test('delegation works for canisters within the subnet range', async () => { const rangeStart = Principal.fromHex('00000000002000000101'); const rangeInterior = Principal.fromHex('000000000020000C0101'); const rangeEnd = Principal.fromHex('00000000002FFFFF0101'); + jest.useFakeTimers(); async function verifies(canisterId) { + jest.setSystemTime(new Date(Date.parse('2022-02-23T07:38:00.652Z'))); await expect( Cert.Certificate.create({ certificate: fromHex(SAMPLE_CERT),