diff --git a/.changeset/mighty-jars-pay.md b/.changeset/mighty-jars-pay.md new file mode 100644 index 000000000..58039ad0c --- /dev/null +++ b/.changeset/mighty-jars-pay.md @@ -0,0 +1,6 @@ +--- +'@credo-ts/core': minor +--- + +- Included `CRLDistributionPoints` as extension +- Access to extensions and adding them made easier diff --git a/packages/core/src/modules/x509/X509Certificate.ts b/packages/core/src/modules/x509/X509Certificate.ts index ae9cc65e4..1fd3413b8 100644 --- a/packages/core/src/modules/x509/X509Certificate.ts +++ b/packages/core/src/modules/x509/X509Certificate.ts @@ -1,10 +1,12 @@ import type { X509CreateCertificateOptions } from './X509ServiceOptions' +import type { IssuerAlternativeNameExtension } from './extensions' import type { AgentContext } from '../../agent' import { AsnParser } from '@peculiar/asn1-schema' import { id_ce_authorityKeyIdentifier, id_ce_extKeyUsage, + id_ce_issuerAltName, id_ce_keyUsage, id_ce_subjectAltName, id_ce_subjectKeyIdentifier, @@ -22,6 +24,7 @@ import { convertName, createAuthorityKeyIdentifierExtension, createBasicConstraintsExtension, + createCrlDistributionPointsExtension, createExtendedKeyUsagesExtension, createIssuerAlternativeNameExtension, createKeyUsagesExtension, @@ -29,24 +32,6 @@ import { createSubjectKeyIdentifierExtension, } from './utils' -type ExtensionObjectIdentifier = string -type CanBeCritical = T & { critical?: boolean } - -type SubjectAlternativeNameExtension = CanBeCritical<{ name: Array<{ type: 'url' | 'dns'; value: string }> }> -type AuthorityKeyIdentifierExtension = CanBeCritical<{ keyId: string }> -type SubjectKeyIdentifierExtension = CanBeCritical<{ keyId: string }> -type KeyUsageExtension = CanBeCritical<{ usage: number }> -type ExtendedKeyUsageExtension = CanBeCritical<{ usage: Array }> - -type ExtensionValues = - | SubjectAlternativeNameExtension - | AuthorityKeyIdentifierExtension - | SubjectKeyIdentifierExtension - | KeyUsageExtension - | ExtendedKeyUsageExtension - -type Extension = Record - export enum X509KeyUsage { DigitalSignature = 1, NonRepudiation = 2, @@ -72,19 +57,19 @@ export enum X509ExtendedKeyUsage { export type X509CertificateOptions = { publicKey: Key privateKey?: Uint8Array - extensions?: Array + extensions?: Array rawCertificate: Uint8Array } export class X509Certificate { public publicKey: Key public privateKey?: Uint8Array - public extensions?: Array + private extensions: Array public readonly rawCertificate: Uint8Array public constructor(options: X509CertificateOptions) { - this.extensions = options.extensions + this.extensions = options.extensions ?? [] this.publicKey = options.publicKey this.privateKey = options.privateKey this.rawCertificate = options.rawCertificate @@ -109,65 +94,46 @@ export class X509Certificate { const key = new Key(publicKeyBytes, keyType) - const extensions = certificate.extensions - .map((e) => { - if (e instanceof x509.AuthorityKeyIdentifierExtension) { - return { [e.type]: { keyId: e.keyId as string, critical: e.critical } } - } else if (e instanceof x509.SubjectKeyIdentifierExtension) { - return { [e.type]: { keyId: e.keyId, critical: e.critical } } - } else if (e instanceof x509.SubjectAlternativeNameExtension) { - return { - [e.type]: { - name: JSON.parse(JSON.stringify(e.names)) as SubjectAlternativeNameExtension['name'], - critical: e.critical, - }, - } - } else if (e instanceof x509.KeyUsagesExtension) { - return { [e.type]: { usage: e.usages as number, critical: e.critical } } - } else if (e instanceof x509.ExtendedKeyUsageExtension) { - return { [e.type]: { usage: e.usages as Array, critical: e.critical } } - } - - // TODO: We could throw an error when we don't understand the extension? - // This will break everytime we do not understand an extension though - return undefined - }) - .filter((e): e is Exclude => e !== undefined) - return new X509Certificate({ publicKey: key, privateKey, - extensions: extensions.length > 0 ? extensions : undefined, + extensions: certificate.extensions, rawCertificate: new Uint8Array(certificate.rawData), }) } - private getMatchingExtensions(objectIdentifier: string): Array | undefined { - return this.extensions?.map((e) => e[objectIdentifier])?.filter(Boolean) as Array | undefined + private getMatchingExtensions(objectIdentifier: string): Array | undefined { + return this.extensions.filter((e) => e.type === objectIdentifier) as Array | undefined + } + + public get subjectAlternativeNames() { + const san = this.getMatchingExtensions(id_ce_subjectAltName) + return san?.flatMap((s) => s.names.items).map((i) => ({ type: i.type, value: i.value })) ?? [] + } + + public get issuerAlternativeNames() { + const ian = this.getMatchingExtensions(id_ce_issuerAltName) + return ian?.flatMap((i) => i.names.items).map((i) => ({ type: i.type, value: i.value })) ?? [] } public get sanDnsNames() { - const san = this.getMatchingExtensions(id_ce_subjectAltName) - return ( - san - ?.flatMap((e) => e.name) - ?.filter((e) => e.type === 'dns') - ?.map((e) => e.value) ?? [] - ) + return this.subjectAlternativeNames.filter((san) => san.type === 'dns').map((san) => san.value) } public get sanUriNames() { - const san = this.getMatchingExtensions(id_ce_subjectAltName) - return ( - san - ?.flatMap((e) => e.name) - ?.filter((e) => e.type === 'url') - ?.map((e) => e.value) ?? [] - ) + return this.subjectAlternativeNames.filter((ian) => ian.type === 'url').map((ian) => ian.value) + } + + public get ianDnsNames() { + return this.issuerAlternativeNames.filter((san) => san.type === 'dns').map((san) => san.value) + } + + public get ianUriNames() { + return this.issuerAlternativeNames.filter((ian) => ian.type === 'url').map((ian) => ian.value) } public get authorityKeyIdentifier() { - const keyIds = this.getMatchingExtensions(id_ce_authorityKeyIdentifier)?.map( + const keyIds = this.getMatchingExtensions(id_ce_authorityKeyIdentifier)?.map( (e) => e.keyId ) @@ -179,7 +145,7 @@ export class X509Certificate { } public get subjectKeyIdentifier() { - const keyIds = this.getMatchingExtensions(id_ce_subjectKeyIdentifier)?.map( + const keyIds = this.getMatchingExtensions(id_ce_subjectKeyIdentifier)?.map( (e) => e.keyId ) @@ -190,8 +156,8 @@ export class X509Certificate { return keyIds?.[0] } - public get keyUsage(): Array { - const keyUsages = this.getMatchingExtensions(id_ce_keyUsage)?.map((e) => e.usage) + public get keyUsage() { + const keyUsages = this.getMatchingExtensions(id_ce_keyUsage)?.map((e) => e.usages) if (keyUsages && keyUsages.length > 1) { throw new X509Error('Multiple Key Usages are not allowed') @@ -203,20 +169,18 @@ export class X509Certificate { .filter((flagValue) => (keyUsages[0] & flagValue) === flagValue) .map((flagValue) => flagValue as X509KeyUsage) } - - return [] } - public get extendedKeyUsage(): Array | undefined { - const extendedKeyUsages = this.getMatchingExtensions(id_ce_extKeyUsage)?.map( - (e) => e.usage + public get extendedKeyUsage() { + const extendedKeyUsages = this.getMatchingExtensions(id_ce_extKeyUsage)?.map( + (e) => e.usages ) if (extendedKeyUsages && extendedKeyUsages.length > 1) { throw new X509Error('Multiple Key Usages are not allowed') } - return extendedKeyUsages?.[0] + return extendedKeyUsages?.[0] as X509ExtendedKeyUsage | undefined } public isExtensionCritical(id: string): boolean { @@ -261,6 +225,7 @@ export class X509Certificate { extensions.push(createIssuerAlternativeNameExtension(options.extensions?.issuerAlternativeName)) extensions.push(createSubjectAlternativeNameExtension(options.extensions?.subjectAlternativeName)) extensions.push(createBasicConstraintsExtension(options.extensions?.basicConstraints)) + extensions.push(createCrlDistributionPointsExtension(options.extensions?.crlDistributionPoints)) if (isSelfSignedCertificate) { if (options.subject) { diff --git a/packages/core/src/modules/x509/X509ServiceOptions.ts b/packages/core/src/modules/x509/X509ServiceOptions.ts index 2febc5872..b3a328825 100644 --- a/packages/core/src/modules/x509/X509ServiceOptions.ts +++ b/packages/core/src/modules/x509/X509ServiceOptions.ts @@ -67,6 +67,9 @@ export type X509CertificateExtensionsOptions = AddMarkAsCritical<{ ca: boolean pathLenConstraint?: number } + crlDistributionPoints?: { + urls: Array + } }> export interface X509CertificateIssuerAndSubjectOptions { diff --git a/packages/core/src/modules/x509/__tests__/X509Service.test.ts b/packages/core/src/modules/x509/__tests__/X509Service.test.ts index a01057cf6..c6efc5b2e 100644 --- a/packages/core/src/modules/x509/__tests__/X509Service.test.ts +++ b/packages/core/src/modules/x509/__tests__/X509Service.test.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + import type { AgentContext } from '../../../agent' -import { id_ce_extKeyUsage, id_ce_keyUsage } from '@peculiar/asn1-x509' +import { id_ce_basicConstraints, id_ce_extKeyUsage, id_ce_keyUsage } from '@peculiar/asn1-x509' import * as x509 from '@peculiar/x509' import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' @@ -122,7 +124,6 @@ describe('X509Service', () => { expect(certificate.publicKey.keyType).toStrictEqual(KeyType.P256) expect(certificate.publicKey.publicKey.length).toStrictEqual(65) expect(certificate.subject).toStrictEqual('CN=credo') - expect(certificate.extensions).toBeUndefined() }) it('should create a valid self-signed certificate with a critical extension', async () => { @@ -179,6 +180,97 @@ describe('X509Service', () => { }) }) + it('should create a valid self-signed certifcate as IACA Root + DCS for mDoc', async () => { + const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) + const documentSignerKey = await wallet.createKey({ keyType: KeyType.P256 }) + + const mdocRootCertificate = await X509Service.createCertificate(agentContext, { + authorityKey, + issuer: { commonName: 'credo', countryName: 'NL' }, + validity: { + notBefore: getLastMonth(), + notAfter: getNextMonth(), + }, + extensions: { + subjectKeyIdentifier: { + include: true, + }, + keyUsage: { + usages: [X509KeyUsage.KeyCertSign, X509KeyUsage.CrlSign], + markAsCritical: true, + }, + issuerAlternativeName: { + name: [{ type: 'url', value: 'animo.id' }], + }, + basicConstraints: { + ca: true, + pathLenConstraint: 0, + markAsCritical: true, + }, + crlDistributionPoints: { + urls: ['https://animo.id'], + }, + }, + }) + + expect(mdocRootCertificate.isExtensionCritical(id_ce_basicConstraints)).toStrictEqual(true) + expect(mdocRootCertificate.isExtensionCritical(id_ce_keyUsage)).toStrictEqual(true) + + expect(mdocRootCertificate).toMatchObject({ + ianUriNames: expect.arrayContaining(['animo.id']), + keyUsage: expect.arrayContaining([X509KeyUsage.KeyCertSign, X509KeyUsage.CrlSign]), + subjectKeyIdentifier: TypedArrayEncoder.toHex(authorityKey.publicKey), + }) + + const mdocDocumentSignerCertificate = await X509Service.createCertificate(agentContext, { + authorityKey, + subjectPublicKey: new Key(documentSignerKey.publicKey, KeyType.P256), + issuer: mdocRootCertificate.issuer, + subject: { commonName: 'credo dcs', countryName: 'NL' }, + validity: { + notBefore: getLastMonth(), + notAfter: getNextMonth(), + }, + extensions: { + authorityKeyIdentifier: { + include: true, + }, + subjectKeyIdentifier: { + include: true, + }, + keyUsage: { + usages: [X509KeyUsage.DigitalSignature], + markAsCritical: true, + }, + subjectAlternativeName: { + name: [{ type: 'url', value: 'paradym.id' }], + }, + issuerAlternativeName: { + name: mdocRootCertificate.issuerAlternativeNames!, + }, + extendedKeyUsage: { + usages: [X509ExtendedKeyUsage.MdlDs], + markAsCritical: true, + }, + crlDistributionPoints: { + urls: ['https://animo.id'], + }, + }, + }) + + expect(mdocDocumentSignerCertificate.isExtensionCritical(id_ce_keyUsage)).toStrictEqual(true) + expect(mdocDocumentSignerCertificate.isExtensionCritical(id_ce_extKeyUsage)).toStrictEqual(true) + + expect(mdocDocumentSignerCertificate).toMatchObject({ + ianUriNames: expect.arrayContaining(['animo.id']), + sanUriNames: expect.arrayContaining(['paradym.id']), + keyUsage: expect.arrayContaining([X509KeyUsage.DigitalSignature]), + extendedKeyUsage: expect.arrayContaining([X509ExtendedKeyUsage.MdlDs]), + subjectKeyIdentifier: TypedArrayEncoder.toHex(documentSignerKey.publicKey), + authorityKeyIdentifier: TypedArrayEncoder.toHex(authorityKey.publicKey), + }) + }) + it('should create a valid leaf certificate', async () => { const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) const subjectKey = await wallet.createKey({ keyType: KeyType.P256 }) diff --git a/packages/core/src/modules/x509/extensions/IssuerAlternativeNameExtension.ts b/packages/core/src/modules/x509/extensions/IssuerAlternativeNameExtension.ts index db65e3285..66a0d93bc 100644 --- a/packages/core/src/modules/x509/extensions/IssuerAlternativeNameExtension.ts +++ b/packages/core/src/modules/x509/extensions/IssuerAlternativeNameExtension.ts @@ -1,6 +1,8 @@ -import type { TextObject } from '@peculiar/x509' +import type { Extension as AsnExtension } from '@peculiar/asn1-x509' +import type { JsonGeneralNames, TextObject } from '@peculiar/x509' -import { id_ce_issuerAltName, id_ce_subjectAltName } from '@peculiar/asn1-x509' +import { AsnConvert } from '@peculiar/asn1-schema' +import { id_ce_issuerAltName, IssueAlternativeName } from '@peculiar/asn1-x509' import { Extension, GeneralNames, ExtensionFactory } from '@peculiar/x509' export class IssuerAlternativeNameExtension extends Extension { @@ -8,8 +10,18 @@ export class IssuerAlternativeNameExtension extends Extension { public static override NAME = 'Issuer Alternative Name' - public constructor(...args: unknown[]) { - super(id_ce_subjectAltName, args[1] as boolean, new GeneralNames(args[0] || []).rawData) + public constructor(data: JsonGeneralNames | ArrayBufferLike, critical?: boolean) { + if (data instanceof ArrayBuffer) { + super(data) + } else { + super(id_ce_issuerAltName, !!critical, new GeneralNames(data).rawData) + } + } + + public onInit(asn: AsnExtension) { + super.onInit(asn) + const value = AsnConvert.parse(asn.extnValue, IssueAlternativeName) + this.names = new GeneralNames(value) } public override toTextObject(): TextObject { diff --git a/packages/core/src/modules/x509/utils/extensions.ts b/packages/core/src/modules/x509/utils/extensions.ts index 29597e9a4..c250957c5 100644 --- a/packages/core/src/modules/x509/utils/extensions.ts +++ b/packages/core/src/modules/x509/utils/extensions.ts @@ -8,6 +8,7 @@ import { SubjectKeyIdentifierExtension, SubjectAlternativeNameExtension, BasicConstraintsExtension, + CRLDistributionPointsExtension, } from '@peculiar/x509' import { TypedArrayEncoder } from '../../../utils' @@ -69,3 +70,11 @@ export const createBasicConstraintsExtension = (options: X509CertificateExtensio return new BasicConstraintsExtension(options.ca, options.pathLenConstraint, options.markAsCritical) } + +export const createCrlDistributionPointsExtension = ( + options: X509CertificateExtensionsOptions['crlDistributionPoints'] +) => { + if (!options) return + + return new CRLDistributionPointsExtension(options.urls, options.markAsCritical) +}