diff --git a/package-lock.json b/package-lock.json index 33eeaaecb..5c2345d86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2363,8 +2363,7 @@ "date-fns": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", - "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==", - "dev": true + "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" }, "debug": { "version": "4.3.3", diff --git a/package.json b/package.json index 48de72dd1..16fc930f4 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@types/verror": "^1.10.5", "asn1js": "^2.2.0", "buffer-to-arraybuffer": "0.0.6", + "date-fns": "^2.28.0", "dohdec": "^3.1.0", "is-valid-domain": "^0.1.6", "moment": "^2.29.1", @@ -59,7 +60,6 @@ "@types/asn1js": "2.0.2", "@types/jest": "^27.4.1", "@types/pkijs": "0.0.12", - "date-fns": "^2.28.0", "del-cli": "^4.0.1", "jest": "^27.5.1", "jest-date-mock": "^1.0.8", diff --git a/src/lib/_test_utils.ts b/src/lib/_test_utils.ts index 64139826a..b663c96c7 100644 --- a/src/lib/_test_utils.ts +++ b/src/lib/_test_utils.ts @@ -42,6 +42,9 @@ interface StubCertConfig { readonly subjectPublicKey: CryptoKey; } +/** + * @deprecated Use {Certificate.issue} instead + */ export async function generateStubCert(config: Partial = {}): Promise { const keyPair = await generateRSAKeyPair(); const futureDate = new Date(); diff --git a/src/lib/crypto_wrappers/x509/Certificate.spec.ts b/src/lib/crypto_wrappers/x509/Certificate.spec.ts index dac9b6585..97275c523 100644 --- a/src/lib/crypto_wrappers/x509/Certificate.spec.ts +++ b/src/lib/crypto_wrappers/x509/Certificate.spec.ts @@ -1,5 +1,6 @@ import * as asn1js from 'asn1js'; import bufferToArrayBuffer from 'buffer-to-arraybuffer'; +import { addDays, addSeconds, setMilliseconds, subSeconds } from 'date-fns'; import * as jestDateMock from 'jest-date-mock'; import * as pkijs from 'pkijs'; @@ -19,11 +20,28 @@ import { import Certificate from './Certificate'; import CertificateError from './CertificateError'; -const futureDate = new Date(); -futureDate.setDate(futureDate.getDate() + 1); - const pkijsCrypto = getPkijsCrypto(); +const baseCertificateOptions = { + commonName: 'the CN', + validityEndDate: addDays(new Date(), 1), +}; + +let issuerKeyPair: CryptoKeyPair; +let issuerCertificate: Certificate; +let subjectKeyPair: CryptoKeyPair; +beforeAll(async () => { + issuerKeyPair = await generateRSAKeyPair(); + issuerCertificate = await Certificate.issue({ + ...baseCertificateOptions, + isCA: true, + issuerPrivateKey: issuerKeyPair.privateKey, + subjectPublicKey: issuerKeyPair.publicKey, + }); + + subjectKeyPair = await generateRSAKeyPair(); +}); + afterEach(() => { jest.restoreAllMocks(); jestDateMock.clear(); @@ -55,22 +73,11 @@ describe('deserialize()', () => { }); describe('issue()', () => { - const baseCertificateOptions = { - commonName: 'the CN', - validityEndDate: futureDate, - }; - - // tslint:disable-next-line:no-let - let keyPair: CryptoKeyPair; - beforeAll(async () => { - keyPair = await generateRSAKeyPair(); - }); - test('should create an X.509 v3 certificate', async () => { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); // v3 is serialized as integer 2 @@ -81,26 +88,26 @@ describe('issue()', () => { jest.spyOn(pkijs.PublicKeyInfo.prototype, 'importKey'); await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); expect(pkijs.PublicKeyInfo.prototype.importKey).toBeCalledTimes(1); - expect(pkijs.PublicKeyInfo.prototype.importKey).toBeCalledWith(keyPair.publicKey); + expect(pkijs.PublicKeyInfo.prototype.importKey).toBeCalledWith(subjectKeyPair.publicKey); }); test('should be signed with the specified private key', async () => { jest.spyOn(pkijs.Certificate.prototype, 'sign'); await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); expect(pkijs.Certificate.prototype.sign).toBeCalledTimes(1); expect(pkijs.Certificate.prototype.sign).toBeCalledWith( - keyPair.privateKey, - ((keyPair.privateKey.algorithm as RsaHashedKeyGenParams).hash as Algorithm).name, + subjectKeyPair.privateKey, + ((subjectKeyPair.privateKey.algorithm as RsaHashedKeyGenParams).hash as Algorithm).name, ); }); @@ -109,8 +116,8 @@ describe('issue()', () => { for (let index = 0; index < 10; index++) { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const serialNumberSerialized = new Uint8Array( cert.pkijsCertificate.serialNumber.valueBlock.valueHex, @@ -135,8 +142,8 @@ describe('issue()', () => { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const expectedDate = new Date(now.getTime()); @@ -149,8 +156,8 @@ describe('issue()', () => { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, validityStartDate: startDate, }); @@ -159,43 +166,86 @@ describe('issue()', () => { expect(cert.startDate).toEqual(expectedDate); }); - test('should honor a custom end validity date', async () => { - const endDate = new Date(futureDate); - endDate.setDate(futureDate.getDate() + 1); - const cert = await Certificate.issue({ - ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, - validityEndDate: endDate, - }); - - expect(cert.pkijsCertificate.notAfter.value).toBe(endDate); - }); - - test('should not accept an end date before the start date', async () => { - const startDate = new Date(2019, 1, 1); - const invalidEndDate = new Date(startDate); - invalidEndDate.setDate(startDate.getDate() - 1); + test('should refuse start date if after expiry date of issuer', async () => { + const startDate = addSeconds(issuerCertificate.expiryDate, 1); + const expiryDate = addSeconds(startDate, 1); await expect( Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, - validityEndDate: invalidEndDate, + issuerCertificate, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, + validityEndDate: expiryDate, validityStartDate: startDate, }), ).rejects.toThrow('The end date must be later than the start date'); }); + describe('Validity end date', () => { + test('should honor explicit one', async () => { + const endDate = setMilliseconds(addDays(new Date(), 1), 0); + + const cert = await Certificate.issue({ + ...baseCertificateOptions, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, + validityEndDate: endDate, + }); + + expect(cert.expiryDate).toEqual(endDate); + }); + + test('should be capped at that of issuer', async () => { + const endDate = addSeconds(issuerCertificate.expiryDate, 1); + + const cert = await Certificate.issue({ + ...baseCertificateOptions, + issuerCertificate, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, + validityEndDate: endDate, + }); + + expect(cert.expiryDate).toEqual(issuerCertificate.expiryDate); + }); + + test('should be rounded down to nearest second', async () => { + const endDate = addDays(issuerCertificate.expiryDate, 1); + + const cert = await Certificate.issue({ + ...baseCertificateOptions, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, + validityEndDate: endDate, + }); + + expect(cert.expiryDate).toEqual(setMilliseconds(endDate, 0)); + }); + + test('should be refused if before the start date', async () => { + const startDate = new Date(2019, 1, 1); + const invalidEndDate = subSeconds(new Date(startDate), 1); + + await expect( + Certificate.issue({ + ...baseCertificateOptions, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, + validityEndDate: invalidEndDate, + validityStartDate: startDate, + }), + ).rejects.toThrow('The end date must be later than the start date'); + }); + }); + test('should store the specified Common Name (CN) in the subject', async () => { - const { privateKey, publicKey } = await generateRSAKeyPair(); const commonName = 'this is the CN'; const cert = await Certificate.issue({ + ...baseCertificateOptions, commonName, - issuerPrivateKey: privateKey, - subjectPublicKey: publicKey, - validityEndDate: futureDate, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const subjectDnAttributes = cert.pkijsCertificate.subject.typesAndValues; @@ -205,7 +255,6 @@ describe('issue()', () => { }); test('should set issuer DN to that of subject when self-issuing certificates', async () => { - const subjectKeyPair = await generateRSAKeyPair(); const cert = await Certificate.issue({ ...baseCertificateOptions, issuerPrivateKey: subjectKeyPair.privateKey, @@ -220,7 +269,6 @@ describe('issue()', () => { }); test('should accept an issuer marked as CA', async () => { - const issuerKeyPair = await generateRSAKeyPair(); const issuerCert = await Certificate.issue({ ...baseCertificateOptions, isCA: true, @@ -233,28 +281,27 @@ describe('issue()', () => { ...baseCertificateOptions, issuerCertificate: issuerCert, issuerPrivateKey: issuerKeyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + subjectPublicKey: subjectKeyPair.publicKey, }), ).toResolve(); }); test('should refuse an issuer certificate without extensions', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCert = await Certificate.issue({ + const invalidIssuerCertificate = await Certificate.issue({ ...baseCertificateOptions, isCA: false, issuerPrivateKey: issuerKeyPair.privateKey, subjectPublicKey: issuerKeyPair.publicKey, }); // tslint:disable-next-line:no-object-mutation - issuerCert.pkijsCertificate.extensions = undefined; + invalidIssuerCertificate.pkijsCertificate.extensions = undefined; await expect( Certificate.issue({ ...baseCertificateOptions, - issuerCertificate: issuerCert, + issuerCertificate: invalidIssuerCertificate, issuerPrivateKey: issuerKeyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + subjectPublicKey: subjectKeyPair.publicKey, }), ).rejects.toEqual( new CertificateError('Basic constraints extension is missing from issuer certificate'), @@ -262,22 +309,21 @@ describe('issue()', () => { }); test('should refuse an issuer certificate with an empty set of extensions', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCert = await Certificate.issue({ + const invalidIssuerCertificate = await Certificate.issue({ ...baseCertificateOptions, isCA: false, issuerPrivateKey: issuerKeyPair.privateKey, subjectPublicKey: issuerKeyPair.publicKey, }); // tslint:disable-next-line:no-object-mutation - issuerCert.pkijsCertificate.extensions = []; + invalidIssuerCertificate.pkijsCertificate.extensions = []; await expect( Certificate.issue({ ...baseCertificateOptions, - issuerCertificate: issuerCert, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerCertificate: invalidIssuerCertificate, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }), ).rejects.toEqual( new CertificateError('Basic constraints extension is missing from issuer certificate'), @@ -285,24 +331,23 @@ describe('issue()', () => { }); test('should refuse an issuer certificate without basic constraints extension', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCert = await Certificate.issue({ + const invalidIssuerCertificate = await Certificate.issue({ ...baseCertificateOptions, isCA: false, - issuerPrivateKey: keyPair.privateKey, + issuerPrivateKey: subjectKeyPair.privateKey, subjectPublicKey: issuerKeyPair.publicKey, }); // tslint:disable-next-line:no-object-mutation - issuerCert.pkijsCertificate.extensions = ( - issuerCert.pkijsCertificate.extensions as ReadonlyArray + invalidIssuerCertificate.pkijsCertificate.extensions = ( + invalidIssuerCertificate.pkijsCertificate.extensions as ReadonlyArray ).filter((e) => e.extnID !== oids.BASIC_CONSTRAINTS); await expect( Certificate.issue({ ...baseCertificateOptions, - issuerCertificate: issuerCert, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerCertificate: invalidIssuerCertificate, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }), ).rejects.toEqual( new CertificateError('Basic constraints extension is missing from issuer certificate'), @@ -310,8 +355,7 @@ describe('issue()', () => { }); test('should refuse an issuer not marked as CA', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCert = await Certificate.issue({ + const invalidIssuerCertificate = await Certificate.issue({ ...baseCertificateOptions, isCA: false, issuerPrivateKey: issuerKeyPair.privateKey, @@ -321,34 +365,26 @@ describe('issue()', () => { await expect( Certificate.issue({ ...baseCertificateOptions, - issuerCertificate: issuerCert, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerCertificate: invalidIssuerCertificate, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }), ).rejects.toEqual(new CertificateError('Issuer is not a CA')); }); test('should set issuer DN to that of CA', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCert = await Certificate.issue({ - ...baseCertificateOptions, - isCA: true, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: issuerKeyPair.publicKey, - }); - - const subjectKeyPair = await generateRSAKeyPair(); const subjectCert = await Certificate.issue({ ...baseCertificateOptions, - issuerCertificate: issuerCert, - issuerPrivateKey: keyPair.privateKey, + issuerCertificate, + issuerPrivateKey: issuerKeyPair.privateKey, subjectPublicKey: subjectKeyPair.publicKey, }); const subjectCertIssuerDn = subjectCert.pkijsCertificate.issuer.typesAndValues; expect(subjectCertIssuerDn.length).toBe(1); expect(subjectCertIssuerDn[0].type).toBe(oids.COMMON_NAME); - const issuerCn = issuerCert.pkijsCertificate.subject.typesAndValues[0].value.valueBlock.value; + const issuerCn = + issuerCertificate.pkijsCertificate.subject.typesAndValues[0].value.valueBlock.value; expect(subjectCertIssuerDn[0].value.valueBlock.value).toBe(issuerCn); }); @@ -356,8 +392,8 @@ describe('issue()', () => { test('Extension should be included and marked as critical', async () => { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const extensions = cert.pkijsCertificate.extensions as ReadonlyArray; @@ -369,8 +405,8 @@ describe('issue()', () => { test('CA flag should be false by default', async () => { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const basicConstraints = getBasicConstraintsExtension(cert); @@ -381,8 +417,8 @@ describe('issue()', () => { const cert = await Certificate.issue({ ...baseCertificateOptions, isCA: true, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const basicConstraints = getBasicConstraintsExtension(cert); expect(basicConstraints).toHaveProperty('cA', true); @@ -391,8 +427,8 @@ describe('issue()', () => { test('pathLenConstraint should be 0 by default', async () => { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const basicConstraints = getBasicConstraintsExtension(cert); @@ -403,9 +439,9 @@ describe('issue()', () => { const pathLenConstraint = 2; const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, + issuerPrivateKey: subjectKeyPair.privateKey, pathLenConstraint, - subjectPublicKey: keyPair.publicKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const basicConstraints = getBasicConstraintsExtension(cert); @@ -416,9 +452,9 @@ describe('issue()', () => { await expect( Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, + issuerPrivateKey: subjectKeyPair.privateKey, pathLenConstraint: 3, - subjectPublicKey: keyPair.publicKey, + subjectPublicKey: subjectKeyPair.publicKey, }), ).rejects.toEqual(new CertificateError('pathLenConstraint must be between 0 and 2 (got 3)')); }); @@ -427,9 +463,9 @@ describe('issue()', () => { await expect( Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, + issuerPrivateKey: subjectKeyPair.privateKey, pathLenConstraint: -1, - subjectPublicKey: keyPair.publicKey, + subjectPublicKey: subjectKeyPair.publicKey, }), ).rejects.toEqual(new CertificateError('pathLenConstraint must be between 0 and 2 (got -1)')); }); @@ -439,8 +475,8 @@ describe('issue()', () => { test('should correspond to subject when self-issued', async () => { const cert = await Certificate.issue({ ...baseCertificateOptions, - issuerPrivateKey: keyPair.privateKey, - subjectPublicKey: keyPair.publicKey, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, }); const extensions = cert.pkijsCertificate.extensions || []; @@ -453,22 +489,15 @@ describe('issue()', () => { schema: akiExtensionAsn1, }); const keyIdBuffer = Buffer.from(akiExtensionRestored.keyIdentifier.valueBlock.valueHex); - expect(keyIdBuffer.toString('hex')).toEqual(await getPublicKeyDigest(keyPair.publicKey)); + expect(keyIdBuffer.toString('hex')).toEqual( + await getPublicKeyDigest(subjectKeyPair.publicKey), + ); }); test('should correspond to issuer key when different from subject', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCert = await Certificate.issue({ - ...baseCertificateOptions, - isCA: true, - issuerPrivateKey: issuerKeyPair.privateKey, - subjectPublicKey: issuerKeyPair.publicKey, - }); - - const subjectKeyPair = await generateRSAKeyPair(); const subjectCert = await Certificate.issue({ ...baseCertificateOptions, - issuerCertificate: issuerCert, + issuerCertificate, issuerPrivateKey: subjectKeyPair.privateKey, subjectPublicKey: subjectKeyPair.publicKey, }); @@ -490,18 +519,9 @@ describe('issue()', () => { }); test('Subject Key Identifier extension should correspond to subject key', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCert = await Certificate.issue({ - ...baseCertificateOptions, - isCA: true, - issuerPrivateKey: issuerKeyPair.privateKey, - subjectPublicKey: issuerKeyPair.publicKey, - }); - - const subjectKeyPair = await generateRSAKeyPair(); const subjectCert = await Certificate.issue({ ...baseCertificateOptions, - issuerCertificate: issuerCert, + issuerCertificate, issuerPrivateKey: subjectKeyPair.privateKey, subjectPublicKey: subjectKeyPair.publicKey, }); @@ -545,11 +565,30 @@ test('startDate should return the start date', async () => { expect(cert.startDate).toEqual(expectedStartDate); }); -test('expiryDate should return the expiry date', async () => { - const cert = await generateStubCert(); +describe('expiryDate', () => { + test('should return the expiry date', async () => { + const expiryDate = setMilliseconds(new Date(), 0); + const cert = await Certificate.issue({ + ...baseCertificateOptions, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, + validityEndDate: expiryDate, + }); + + expect(cert.expiryDate).toEqual(expiryDate); + }); - const expectedExpiryDate = cert.pkijsCertificate.notAfter.value; - expect(cert.expiryDate).toEqual(expectedExpiryDate); + test('should round down to the nearest second', async () => { + const expiryDate = setMilliseconds(addSeconds(new Date(), 10), 50); + const cert = await Certificate.issue({ + ...baseCertificateOptions, + issuerPrivateKey: subjectKeyPair.privateKey, + subjectPublicKey: subjectKeyPair.publicKey, + validityEndDate: expiryDate, + }); + + expect(cert.expiryDate).toEqual(setMilliseconds(expiryDate, 0)); + }); }); test('getSerialNumber() should return the serial number as a buffer', async () => { @@ -638,14 +677,6 @@ describe('getIssuerPrivateAddress', () => { }); test('Private address of issuer should be output if extension is present', async () => { - const issuerKeyPair = await generateRSAKeyPair(); - const issuerCertificate = reSerializeCertificate( - await generateStubCert({ - attributes: { isCA: true }, - issuerPrivateKey: issuerKeyPair.privateKey, - subjectPublicKey: issuerKeyPair.publicKey, - }), - ); const certificate = await generateStubCert({ issuerCertificate, issuerPrivateKey: issuerKeyPair.privateKey, @@ -876,7 +907,6 @@ describe('getCertificationPath', () => { }); test('getPublicKey should return the subject public key', async () => { - const subjectKeyPair = await generateRSAKeyPair(); const cert = await generateStubCert({ issuerPrivateKey: subjectKeyPair.privateKey, subjectPublicKey: subjectKeyPair.publicKey, diff --git a/src/lib/crypto_wrappers/x509/Certificate.ts b/src/lib/crypto_wrappers/x509/Certificate.ts index b644ee40f..6a3eb7905 100644 --- a/src/lib/crypto_wrappers/x509/Certificate.ts +++ b/src/lib/crypto_wrappers/x509/Certificate.ts @@ -1,6 +1,6 @@ import { BmpString, Integer, LocalBaseBlock, OctetString } from 'asn1js'; +import { min, setMilliseconds } from 'date-fns'; import * as pkijs from 'pkijs'; -import { makeDateWithSecondPrecision } from '../../_utils'; import * as oids from '../../oids'; import { derDeserialize, generateRandom64BitValue } from '../_utils'; @@ -47,20 +47,28 @@ export default class Certificate { * @param options */ public static async issue(options: FullCertificateIssuanceOptions): Promise { - const validityStartDate = makeDateWithSecondPrecision(options.validityStartDate); + // PKI.js should round down to the nearest second per X.509. We should do it ourselves to + // avoid discrepancies when the validity dates of a freshly-issued certificate are used. + const validityStartDate = setMilliseconds(options.validityStartDate ?? new Date(), 0); + const issuerCertificate = options.issuerCertificate; + const validityEndDate = setMilliseconds( + issuerCertificate + ? min([issuerCertificate.expiryDate, options.validityEndDate]) + : options.validityEndDate, + 0, + ); //region Validation - if (options.validityEndDate < validityStartDate) { + if (validityEndDate < validityStartDate) { throw new CertificateError('The end date must be later than the start date'); } - - if (options.issuerCertificate) { - validateIssuerCertificate(options.issuerCertificate); + if (issuerCertificate) { + validateIssuerCertificate(issuerCertificate); } //endregion - const issuerPublicKey = options.issuerCertificate - ? await options.issuerCertificate.pkijsCertificate.getPublicKey() + const issuerPublicKey = issuerCertificate + ? await issuerCertificate.pkijsCertificate.getPublicKey() : options.subjectPublicKey; const pkijsCert = new pkijs.Certificate({ extensions: [ @@ -75,7 +83,7 @@ export default class Certificate { // tslint:disable-next-line:no-object-mutation pkijsCert.notBefore.value = validityStartDate; // tslint:disable-next-line:no-object-mutation - pkijsCert.notAfter.value = options.validityEndDate; + pkijsCert.notAfter.value = validityEndDate; pkijsCert.subject.typesAndValues.push( new pkijs.AttributeTypeAndValue({ @@ -84,8 +92,8 @@ export default class Certificate { }), ); - const issuerDn = options.issuerCertificate - ? options.issuerCertificate.pkijsCertificate.subject.typesAndValues + const issuerDn = issuerCertificate + ? issuerCertificate.pkijsCertificate.subject.typesAndValues : pkijsCert.subject.typesAndValues; // tslint:disable-next-line:no-object-mutation pkijsCert.issuer.typesAndValues = issuerDn.map( diff --git a/src/lib/keyStores/CertificateStore.spec.ts b/src/lib/keyStores/CertificateStore.spec.ts index 534ec36da..2b5dabbcd 100644 --- a/src/lib/keyStores/CertificateStore.spec.ts +++ b/src/lib/keyStores/CertificateStore.spec.ts @@ -1,4 +1,4 @@ -import { addSeconds, subSeconds } from 'date-fns'; +import { addSeconds, setMilliseconds, subSeconds } from 'date-fns'; import { generateRSAKeyPair, getPrivateAddressFromIdentityKey } from '../crypto_wrappers/keys'; import Certificate from '../crypto_wrappers/x509/Certificate'; @@ -44,7 +44,9 @@ describe('save', () => { await store.save(certificate, privateAddress); expect(store.dataByPrivateAddress).toHaveProperty(privateAddress); - expect(store.dataByPrivateAddress[privateAddress][0].expiryDate).toEqual(expiryDate); + expect(store.dataByPrivateAddress[privateAddress][0].expiryDate).toEqual( + setMilliseconds(expiryDate, 0), + ); }); test('Specified issuer private address should be honoured', async () => { diff --git a/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts b/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts index 801b133ee..e9541c19b 100644 --- a/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts +++ b/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts @@ -24,7 +24,7 @@ let privateGatewayPrivateAddress: string; let privateGatewayKeyPair: CryptoKeyPair; let privateGatewayPDCCertificate: Certificate; beforeAll(async () => { - const tomorrow = setMilliseconds(addDays(new Date(), 1), 0); + const nextYear = setMilliseconds(addDays(new Date(), 360), 0); // Public gateway const publicGatewayKeyPair = await generateRSAKeyPair(); @@ -33,7 +33,7 @@ beforeAll(async () => { publicGatewayCertificate = await issueGatewayCertificate({ issuerPrivateKey: publicGatewayKeyPair.privateKey, subjectPublicKey: publicGatewayPublicKey, - validityEndDate: tomorrow, + validityEndDate: nextYear, }); // Private gateway @@ -43,7 +43,7 @@ beforeAll(async () => { issuerCertificate: publicGatewayCertificate, issuerPrivateKey: publicGatewayKeyPair.privateKey, subjectPublicKey: privateGatewayKeyPair.publicKey, - validityEndDate: tomorrow, + validityEndDate: nextYear, }), ); privateGatewayPrivateAddress = await getPrivateAddressFromIdentityKey(