Skip to content

Commit

Permalink
default max age for certificates is 5 minutes
Browse files Browse the repository at this point in the history
provides mechanism for verifying the max age so that delegations can be signed up to 30 days in advance and will still validate correctly.
  • Loading branch information
krpeacock committed Sep 15, 2023
1 parent 5a91fe7 commit db7584a
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 44 deletions.
51 changes: 20 additions & 31 deletions packages/agent/src/certificate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand Down
41 changes: 30 additions & 11 deletions packages/agent/src/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Certificate> {
Expand All @@ -146,6 +154,7 @@ export class Certificate {
options.rootKey,
options.canisterId,
blsVerify,
options.maxAgeInMinutes,
);
await cert.verify();
return cert;
Expand All @@ -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));
}
Expand All @@ -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(),
);
}

Expand All @@ -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']);
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/utils/leb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
4 changes: 3 additions & 1 deletion packages/bls-verify/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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),
Expand Down

0 comments on commit db7584a

Please sign in to comment.