diff --git a/.changeset/great-cases-wear.md b/.changeset/great-cases-wear.md new file mode 100644 index 0000000000..1a3b39d2d6 --- /dev/null +++ b/.changeset/great-cases-wear.md @@ -0,0 +1,6 @@ +--- +'@credo-ts/core': minor +--- + +- X.509 self-signed certificate creation is now done via the `agent.x509.createCertificate` API where the `subjectPublicKey` is not supplied or equal to the `authorityKey` +- allow to create more complex X.509 certificates diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index dee9959051..e2d8477069 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -267,18 +267,24 @@ export class Issuer extends BaseAgent<{ const issuer = new Issuer(ISSUER_HOST, 2000, 'OpenId4VcIssuer ' + Math.random().toString()) await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(issuer.agent.context, { - key: await issuer.agent.context.wallet.createKey({ + const certificate = await X509Service.createCertificate(issuer.agent.context, { + authorityKey: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256, seed: TypedArrayEncoder.fromString('e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962239bbcf42f'), }), - notBefore: new Date('2000-01-01'), - notAfter: new Date('2050-01-01'), - extensions: [[{ type: 'dns', value: ISSUER_HOST.replace('https://', '').replace('http://', '') }]], - name: 'C=DE', + validity: { + notBefore: new Date('2000-01-01'), + notAfter: new Date('2050-01-01'), + }, + extensions: { + subjectAlternativeName: { + name: [{ type: 'dns', value: ISSUER_HOST.replace('https://', '').replace('http://', '') }], + }, + }, + issuer: 'C=DE', }) - const issuerCertficicate = selfSignedCertificate.toString('base64url') + const issuerCertficicate = certificate.toString('base64url') await issuer.agent.x509.setTrustedCertificates([issuerCertficicate]) console.log('Set the following certficate for the holder to verify mdoc credentials.') console.log(issuerCertficicate) diff --git a/packages/core/src/modules/mdoc/MdocContext.ts b/packages/core/src/modules/mdoc/MdocContext.ts index b2e124291f..2d85c70fe3 100644 --- a/packages/core/src/modules/mdoc/MdocContext.ts +++ b/packages/core/src/modules/mdoc/MdocContext.ts @@ -107,7 +107,7 @@ export const getMdocContext = (agentContext: AgentContext): MdocContext => { const x509Certificate = X509Certificate.fromRawCertificate(certificate) return { ...x509Certificate.data, - thumbprint: await x509Certificate.getThumprint(agentContext), + thumbprint: await x509Certificate.getThumprintInHex(agentContext), } }, } satisfies X509Context, diff --git a/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts index 404b819d4c..39aaad578c 100644 --- a/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts +++ b/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts @@ -26,14 +26,16 @@ describe('mdoc device-response test', () => { const nextDay = new Date(currentDate) nextDay.setDate(currentDate.getDate() + 2) - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(agent.context, { - key: issuerKey, - notBefore: currentDate, - notAfter: nextDay, - extensions: [], + const certificate = await X509Service.createCertificate(agent.context, { + issuer: 'CN=credo', + authorityKey: issuerKey, + validity: { + notBefore: currentDate, + notAfter: nextDay, + }, }) - const issuerCertificate = selfSignedCertificate.toString('pem') + const issuerCertificate = certificate.toString('pem') const mdoc = await Mdoc.sign(agent.context, { docType: 'org.iso.18013.5.1.mDL', diff --git a/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts index 6433cb3aa3..b208e9b409 100644 --- a/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts +++ b/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts @@ -305,11 +305,13 @@ describe('mdoc device-response openid4vp test', () => { keyType: KeyType.Ed25519, }) - const issuerCertificate = await agent.x509.createSelfSignedCertificate({ - key: issuerKey, - name: 'C=US,ST=New York', - notBefore: new Date('2020-01-01'), - notAfter: new Date(Date.now() + 1000 * 3600), + const issuerCertificate = await agent.x509.createCertificate({ + authorityKey: issuerKey, + issuer: 'C=US,ST=New York', + validity: { + notBefore: new Date('2020-01-01'), + notAfter: new Date(Date.now() + 1000 * 3600), + }, }) const mdoc = await Mdoc.sign(agent.context, { diff --git a/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts index c57bba7755..716898cbc9 100644 --- a/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts +++ b/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts @@ -44,15 +44,16 @@ describe('mdoc service test', () => { const nextDay = new Date(currentDate) nextDay.setDate(currentDate.getDate() + 2) - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(agentContext, { - key: issuerKey, - notBefore: currentDate, - notAfter: nextDay, - extensions: [], - name: 'C=DE', + const certificate = await X509Service.createCertificate(agentContext, { + authorityKey: issuerKey, + validity: { + notBefore: currentDate, + notAfter: nextDay, + }, + issuer: 'C=DE', }) - const issuerCertificate = selfSignedCertificate.toString('pem') + const issuerCertificate = certificate.toString('pem') const mdoc = await Mdoc.sign(agentContext, { docType: 'org.iso.18013.5.1.mDL', @@ -78,7 +79,7 @@ describe('mdoc service test', () => { expect(() => mdoc.deviceSignedNamespaces).toThrow() const { isValid } = await mdoc.verify(agentContext, { - trustedCertificates: [selfSignedCertificate.toString('base64')], + trustedCertificates: [certificate.toString('base64')], }) expect(isValid).toBeTruthy() }) diff --git a/packages/core/src/modules/x509/X509Api.ts b/packages/core/src/modules/x509/X509Api.ts index 49d77e4cc7..cd74667633 100644 --- a/packages/core/src/modules/x509/X509Api.ts +++ b/packages/core/src/modules/x509/X509Api.ts @@ -3,7 +3,7 @@ import { injectable } from '../../plugins' import { X509ModuleConfig } from './X509ModuleConfig' import { X509Service } from './X509Service' -import { X509CreateSelfSignedCertificateOptions, X509ValidateCertificateChainOptions } from './X509ServiceOptions' +import { X509CreateCertificateOptions, X509ValidateCertificateChainOptions } from './X509ServiceOptions' /** * @public @@ -31,12 +31,12 @@ export class X509Api { } /** - * Creates a self-signed certificate. + * Creates a X.509 certificate. * - * @param options X509CreateSelfSignedCertificateOptions + * @param options X509CreateCertificateOptions */ - public async createSelfSignedCertificate(options: X509CreateSelfSignedCertificateOptions) { - return await X509Service.createSelfSignedCertificate(this.agentContext, options) + public async createCertificate(options: X509CreateCertificateOptions) { + return await X509Service.createCertificate(this.agentContext, options) } /** diff --git a/packages/core/src/modules/x509/X509Certificate.ts b/packages/core/src/modules/x509/X509Certificate.ts index 7a5a07191e..c0d0bb01f7 100644 --- a/packages/core/src/modules/x509/X509Certificate.ts +++ b/packages/core/src/modules/x509/X509Certificate.ts @@ -1,9 +1,10 @@ -import type { X509CreateSelfSignedCertificateOptions } from './X509ServiceOptions' +import type { X509CreateCertificateOptions } from './X509ServiceOptions' import type { AgentContext } from '../../agent' import { AsnParser } from '@peculiar/asn1-schema' import { id_ce_authorityKeyIdentifier, + id_ce_extKeyUsage, id_ce_keyUsage, id_ce_subjectAltName, id_ce_subjectKeyIdentifier, @@ -17,25 +18,36 @@ import { credoKeyTypeIntoCryptoKeyAlgorithm, spkiAlgorithmIntoCredoKeyType } fro import { TypedArrayEncoder } from '../../utils' import { X509Error } from './X509Error' +import { + convertName, + createAuthorityKeyIdentifierExtension, + createBasicConstraintsExtension, + createExtendedKeyUsagesExtension, + createIssuerAlternativeNameExtension, + createKeyUsagesExtension, + createSubjectAlternativeNameExtension, + createSubjectKeyIdentifierExtension, +} from './utils' type ExtensionObjectIdentifier = string +type CanBeCritical = T & { critical?: boolean } -type SubjectAlternativeNameExtension = Array<{ type: 'url' | 'dns'; value: string }> -type AuthorityKeyIdentifierExtension = { keyId: string } -type SubjectKeyIdentifierExtension = { keyId: string } -type KeyUsageExtension = { usage: number } +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 type ExtensionInput = Array> - -export enum KeyUsage { +export enum X509KeyUsage { DigitalSignature = 1, NonRepudiation = 2, KeyEncipherment = 4, @@ -47,6 +59,16 @@ export enum KeyUsage { DecipherOnly = 256, } +export enum X509ExtendedKeyUsage { + ServerAuth = '1.3.6.1.5.5.7.3.1', + ClientAuth = '1.3.6.1.5.5.7.3.2', + CodeSigning = '1.3.6.1.5.5.7.3.3', + EmailProtection = '1.3.6.1.5.5.7.3.4', + TimeStamping = '1.3.6.1.5.5.7.3.8', + OcspSigning = '1.3.6.1.5.5.7.3.9', + MdlDs = '1.0.18013.5.1.2', +} + export type X509CertificateOptions = { publicKey: Key privateKey?: Uint8Array @@ -90,13 +112,20 @@ export class X509Certificate { const extensions = certificate.extensions .map((e) => { if (e instanceof x509.AuthorityKeyIdentifierExtension) { - return { [e.type]: { keyId: e.keyId as string } } + return { [e.type]: { keyId: e.keyId as string, critical: e.critical } } } else if (e instanceof x509.SubjectKeyIdentifierExtension) { - return { [e.type]: { keyId: e.keyId } } + return { [e.type]: { keyId: e.keyId, critical: e.critical } } } else if (e instanceof x509.SubjectAlternativeNameExtension) { - return { [e.type]: JSON.parse(JSON.stringify(e.names)) as 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 } } + 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? @@ -108,7 +137,7 @@ export class X509Certificate { return new X509Certificate({ publicKey: key, privateKey, - extensions, + extensions: extensions.length > 0 ? extensions : undefined, rawCertificate: new Uint8Array(certificate.rawData), }) } @@ -121,7 +150,7 @@ export class X509Certificate { const san = this.getMatchingExtensions(id_ce_subjectAltName) return ( san - ?.flatMap((e) => e) + ?.flatMap((e) => e.name) ?.filter((e) => e.type === 'dns') ?.map((e) => e.value) ?? [] ) @@ -131,7 +160,7 @@ export class X509Certificate { const san = this.getMatchingExtensions(id_ce_subjectAltName) return ( san - ?.flatMap((e) => e) + ?.flatMap((e) => e.name) ?.filter((e) => e.type === 'url') ?.map((e) => e.value) ?? [] ) @@ -161,7 +190,7 @@ export class X509Certificate { return keyIds?.[0] } - public get keyUsage(): Array { + public get keyUsage(): Array { const keyUsages = this.getMatchingExtensions(id_ce_keyUsage)?.map((e) => e.usage) if (keyUsages && keyUsages.length > 1) { @@ -169,53 +198,105 @@ export class X509Certificate { } if (keyUsages) { - return Object.values(KeyUsage) + return Object.values(X509KeyUsage) .filter((key): key is number => typeof key === 'number') .filter((flagValue) => (keyUsages[0] & flagValue) === flagValue) - .map((flagValue) => flagValue as KeyUsage) + .map((flagValue) => flagValue as X509KeyUsage) } return [] } - public static async createSelfSigned( - { - key, - extensions, - notAfter, - notBefore, - name, - includeAuthorityKeyIdentifier = true, - }: X509CreateSelfSignedCertificateOptions, - webCrypto: CredoWebCrypto - ) { - const cryptoKeyAlgorithm = credoKeyTypeIntoCryptoKeyAlgorithm(key.keyType) + public get extendedKeyUsage(): Array | undefined { + const extendedKeyUsages = this.getMatchingExtensions(id_ce_extKeyUsage)?.map( + (e) => e.usage + ) - const publicKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, true, 'public', ['verify']) - const privateKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, false, 'private', ['sign']) + if (extendedKeyUsages && extendedKeyUsages.length > 1) { + throw new X509Error('Multiple Key Usages are not allowed') + } + + return extendedKeyUsages?.[0] + } - const hexPublicKey = TypedArrayEncoder.toHex(key.publicKey) + public isExtensionCritical(id: string): boolean { + const extension = this.getMatchingExtensions(id) + if (!extension) { + throw new X509Error(`extension with id '${id}' is not found`) + } - const x509Extensions: Array = [ - new x509.SubjectKeyIdentifierExtension(hexPublicKey), - new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyCertSign), - ] + return !!extension[0].critical + } - if (includeAuthorityKeyIdentifier) { - x509Extensions.push(new x509.AuthorityKeyIdentifierExtension(hexPublicKey)) + public static async create(options: X509CreateCertificateOptions, webCrypto: CredoWebCrypto) { + const subjectPublicKey = options.subjectPublicKey ?? options.authorityKey + const isSelfSignedCertificate = options.authorityKey.publicKeyBase58 === subjectPublicKey.publicKeyBase58 + + const signingKey = new CredoWebCryptoKey( + options.authorityKey, + credoKeyTypeIntoCryptoKeyAlgorithm(options.authorityKey.keyType), + false, + 'private', + ['sign'] + ) + const publicKey = new CredoWebCryptoKey( + subjectPublicKey, + credoKeyTypeIntoCryptoKeyAlgorithm(options.authorityKey.keyType), + true, + 'public', + ['verify'] + ) + + const issuerName = convertName(options.issuer) + + const extensions: Array = [] + extensions.push( + createSubjectKeyIdentifierExtension(options.extensions?.subjectKeyIdentifier, { key: subjectPublicKey }) + ) + extensions.push(createKeyUsagesExtension(options.extensions?.keyUsage)) + extensions.push(createExtendedKeyUsagesExtension(options.extensions?.extendedKeyUsage)) + extensions.push( + createAuthorityKeyIdentifierExtension(options.extensions?.authorityKeyIdentifier, { key: options.authorityKey }) + ) + extensions.push(createIssuerAlternativeNameExtension(options.extensions?.issuerAlternativeName)) + extensions.push(createSubjectAlternativeNameExtension(options.extensions?.subjectAlternativeName)) + extensions.push(createBasicConstraintsExtension(options.extensions?.basicConstraints)) + + if (isSelfSignedCertificate) { + if (options.subject) { + throw new X509Error('Do not provide a subject name when the certificate is supposed to be self signed') + } + + const certificate = await x509.X509CertificateGenerator.createSelfSigned( + { + keys: { publicKey, privateKey: signingKey }, + name: issuerName, + notBefore: options.validity?.notBefore, + notAfter: options.validity?.notAfter, + extensions: extensions.filter((e) => e !== undefined), + serialNumber: options.serialNumber, + }, + webCrypto + ) + + return X509Certificate.parseCertificate(certificate) } - for (const extension of extensions ?? []) { - x509Extensions.push(new x509.SubjectAlternativeNameExtension(extension)) + if (!options.subject) { + throw new X509Error('Provide a subject name when the certificate is not supposed to be self signed') } - const certificate = await x509.X509CertificateGenerator.createSelfSigned( + const subjectName = convertName(options.subject) + + const certificate = await x509.X509CertificateGenerator.create( { - keys: { publicKey, privateKey }, - name, - extensions: x509Extensions, - notAfter, - notBefore, + signingKey, + publicKey, + issuer: issuerName, + subject: subjectName, + notBefore: options.validity?.notBefore, + notAfter: options.validity?.notAfter, + extensions: extensions.filter((e) => e !== undefined), }, webCrypto ) @@ -228,6 +309,11 @@ export class X509Certificate { return certificate.subject } + public get issuer() { + const certificate = new x509.X509Certificate(this.rawCertificate) + return certificate.issuer + } + public async verify( { verificationDate = new Date(), publicKey }: { verificationDate: Date; publicKey?: Key }, webCrypto: CredoWebCrypto @@ -250,6 +336,7 @@ export class X509Certificate { if (!isSignatureValid) { throw new X509Error(`Certificate: '${certificate.subject}' has an invalid signature`) } + if (!isNotBeforeValid) { throw new X509Error(`Certificate: '${certificate.subject}' used before it is allowed`) } @@ -262,7 +349,7 @@ export class X509Certificate { /** * Get the thumprint of the X509 certificate in hex format. */ - public async getThumprint(agentContext: AgentContext) { + public async getThumprintInHex(agentContext: AgentContext) { const certificate = new x509.X509Certificate(this.rawCertificate) const thumbprint = await certificate.getThumbprint(new CredoWebCrypto(agentContext)) diff --git a/packages/core/src/modules/x509/X509Service.ts b/packages/core/src/modules/x509/X509Service.ts index 6ed5f07289..6064c46d56 100644 --- a/packages/core/src/modules/x509/X509Service.ts +++ b/packages/core/src/modules/x509/X509Service.ts @@ -1,8 +1,8 @@ import type { X509ValidateCertificateChainOptions, - X509CreateSelfSignedCertificateOptions, - X509GetLefCertificateOptions, + X509GetLeafCertificateOptions, X509ParseCertificateOptions, + X509CreateCertificateOptions, } from './X509ServiceOptions' import * as x509 from '@peculiar/x509' @@ -101,7 +101,7 @@ export class X509Service { public static getLeafCertificate( _agentContext: AgentContext, - { certificateChain }: X509GetLefCertificateOptions + { certificateChain }: X509GetLeafCertificateOptions ): X509Certificate { if (certificateChain.length === 0) throw new X509Error('Certificate chain is empty') @@ -110,13 +110,10 @@ export class X509Service { return certificate } - public static async createSelfSignedCertificate( - agentContext: AgentContext, - options: X509CreateSelfSignedCertificateOptions - ) { + public static async createCertificate(agentContext: AgentContext, options: X509CreateCertificateOptions) { const webCrypto = new CredoWebCrypto(agentContext) - const certificate = await X509Certificate.createSelfSigned(options, webCrypto) + const certificate = await X509Certificate.create(options, webCrypto) return certificate } diff --git a/packages/core/src/modules/x509/X509ServiceOptions.ts b/packages/core/src/modules/x509/X509ServiceOptions.ts index 84068af570..2febc58729 100644 --- a/packages/core/src/modules/x509/X509ServiceOptions.ts +++ b/packages/core/src/modules/x509/X509ServiceOptions.ts @@ -1,5 +1,12 @@ -import type { ExtensionInput } from './X509Certificate' +import type { X509Certificate, X509ExtendedKeyUsage, X509KeyUsage } from './X509Certificate' import type { Key } from '../../crypto/Key' +import type { GeneralNameType } from '@peculiar/x509' + +type AddMarkAsCritical>> = T & { + [K in keyof T]: T[K] & { + markAsCritical?: boolean + } +} /** * Base64 or PEM @@ -9,7 +16,8 @@ export type EncodedX509Certificate = string export interface X509ValidateCertificateChainOptions { certificateChain: Array - certificate?: string + certificate?: EncodedX509Certificate + /** * The date for which the certificate chain should be valid * @default new Date() @@ -20,22 +28,121 @@ export interface X509ValidateCertificateChainOptions { */ verificationDate?: Date - trustedCertificates?: EncodedX509Certificate[] + trustedCertificates?: Array } -export interface X509CreateSelfSignedCertificateOptions { - key: Key - extensions?: ExtensionInput - includeAuthorityKeyIdentifier?: boolean - notBefore?: Date - notAfter?: Date - name?: string -} - -export interface X509GetLefCertificateOptions { +export interface X509GetLeafCertificateOptions { certificateChain: Array } export interface X509ParseCertificateOptions { encodedCertificate: string } + +export interface X509CreateCertificateChainOptions { + certificates: Array + outputFormat?: 'pem' | 'base64' +} + +export type X509CertificateExtensionsOptions = AddMarkAsCritical<{ + subjectKeyIdentifier?: { + include: boolean + } + keyUsage?: { + usages: Array + } + extendedKeyUsage?: { + usages: Array + } + authorityKeyIdentifier?: { + include: boolean + } + issuerAlternativeName?: { + name: Array<{ type: GeneralNameType; value: string }> + } + subjectAlternativeName?: { + name: Array<{ type: GeneralNameType; value: string }> + } + basicConstraints?: { + ca: boolean + pathLenConstraint?: number + } +}> + +export interface X509CertificateIssuerAndSubjectOptions { + countryName?: string + stateOrProvinceName?: string + organizationalUnit?: string + commonName?: string +} + +export interface X509CreateCertificateOptions { + /** + * + * Serial number of the X.509 certificate + * + */ + serialNumber?: string + + /** + * + * The Key that will be used to sign the X.509 Certificate + * + */ + authorityKey: Key + + /** + * + * The key that is the subject of the X.509 Certificate + * + * If the `subjectPublicKey` is not included, the `authorityKey` will be used. + * This means that the certificate is self-signed + * + */ + subjectPublicKey?: Key + + /** + * + * The issuer information of the X.509 Certificate + * + */ + issuer: string | X509CertificateIssuerAndSubjectOptions + + /** + * + * The subject information of the X.509 Certificate + * + * If the `subject` is not included, the `issuer` will be used + * + * + */ + subject?: string | X509CertificateIssuerAndSubjectOptions + + /** + * + * Date range for when the X.509 Certificate is valid + * + */ + validity?: { + /** + * + * Certificate is not valid before this date + * + */ + notBefore?: Date + + /** + * + * Certificate is not valid after this date + * + */ + notAfter?: Date + } + + /** + * + * X.509 v3 Extensions to be added to the certificate + * + */ + extensions?: X509CertificateExtensionsOptions +} diff --git a/packages/core/src/modules/x509/__tests__/X509Service.test.ts b/packages/core/src/modules/x509/__tests__/X509Service.test.ts index fea2fa6c4e..a01057cf62 100644 --- a/packages/core/src/modules/x509/__tests__/X509Service.test.ts +++ b/packages/core/src/modules/x509/__tests__/X509Service.test.ts @@ -1,17 +1,17 @@ import type { AgentContext } from '../../../agent' -import type { KeyGenAlgorithm, KeySignParams } from '../../../crypto/webcrypto/types' +import { id_ce_extKeyUsage, id_ce_keyUsage } from '@peculiar/asn1-x509' import * as x509 from '@peculiar/x509' import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../tests' import { KeyType } from '../../../crypto/KeyType' import { getJwkFromKey, P256Jwk } from '../../../crypto/jose/jwk' -import { CredoWebCrypto, CredoWebCryptoKey } from '../../../crypto/webcrypto' +import { CredoWebCrypto } from '../../../crypto/webcrypto' import { X509Error } from '../X509Error' import { X509Service } from '../X509Service' -import { KeyUsage, TypedArrayEncoder } from '@credo-ts/core' +import { X509KeyUsage, TypedArrayEncoder, X509ExtendedKeyUsage, Key } from '@credo-ts/core' /** * @@ -45,7 +45,7 @@ const getLastMonth = () => { describe('X509Service', () => { let wallet: InMemoryWallet let agentContext: AgentContext - let x5c: Array + let certificateChain: Array beforeAll(async () => { const agentConfig = getAgentConfig('X509Service') @@ -55,65 +55,55 @@ describe('X509Service', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await wallet.createAndOpen(agentConfig.walletConfig!) - const algorithm: KeyGenAlgorithm = { name: 'ECDSA', namedCurve: 'P-256' } - const signingAlgorithm: KeySignParams = { name: 'ECDSA', hash: 'SHA-256' } - const rootKey = await wallet.createKey({ keyType: KeyType.P256 }) - const webCryptoRootKeys = { - publicKey: new CredoWebCryptoKey(rootKey, algorithm, true, 'public', ['verify']), - privateKey: new CredoWebCryptoKey(rootKey, algorithm, false, 'private', ['sign']), - } - const intermediateKey = await wallet.createKey({ keyType: KeyType.P256 }) - const webCryptoIntermediateKeys = { - publicKey: new CredoWebCryptoKey(intermediateKey, algorithm, true, 'public', ['verify']), - privateKey: new CredoWebCryptoKey(intermediateKey, algorithm, false, 'private', ['sign']), - } - const leafKey = await wallet.createKey({ keyType: KeyType.P256 }) - const webCryptoLeafKeys = { - publicKey: new CredoWebCryptoKey(leafKey, algorithm, true, 'public', ['verify']), - privateKey: new CredoWebCryptoKey(leafKey, algorithm, false, 'private', ['sign']), - } x509.cryptoProvider.set(new CredoWebCrypto(agentContext)) - const rootCert = await x509.X509CertificateGenerator.createSelfSigned({ + const rootCert = await X509Service.createCertificate(agentContext, { serialNumber: '01', - name: 'CN=Root', - notBefore: getLastMonth(), - notAfter: getNextMonth(), - keys: webCryptoRootKeys, - signingAlgorithm, + issuer: { commonName: 'Root' }, + authorityKey: rootKey, + validity: { + notBefore: getLastMonth(), + notAfter: getNextMonth(), + }, }) - const intermediateCert = await x509.X509CertificateGenerator.create({ + const intermediateCert = await X509Service.createCertificate(agentContext, { serialNumber: '02', - subject: 'CN=Intermediate', issuer: rootCert.subject, - notBefore: getLastMonth(), - notAfter: getNextMonth(), - signingKey: webCryptoRootKeys.privateKey, - publicKey: webCryptoIntermediateKeys.publicKey, - signingAlgorithm, + authorityKey: rootKey, + subject: { commonName: 'Intermediate' }, + subjectPublicKey: intermediateKey, + validity: { + notBefore: getLastMonth(), + notAfter: getNextMonth(), + }, }) - const leafCert = await x509.X509CertificateGenerator.create({ + const leafCert = await X509Service.createCertificate(agentContext, { serialNumber: '03', - subject: 'CN=Leaf', issuer: intermediateCert.subject, - notBefore: getLastMonth(), - notAfter: getNextMonth(), - signingKey: webCryptoIntermediateKeys.privateKey, - publicKey: webCryptoLeafKeys.publicKey, - signingAlgorithm, + authorityKey: intermediateKey, + subject: { commonName: 'Leaf' }, + subjectPublicKey: leafKey, + validity: { + notBefore: getLastMonth(), + notAfter: getNextMonth(), + }, }) - const chain = new x509.X509ChainBuilder({ - certificates: [rootCert, intermediateCert, leafCert], + const builder = new x509.X509ChainBuilder({ + certificates: [ + new x509.X509Certificate(rootCert.rawCertificate), + new x509.X509Certificate(intermediateCert.rawCertificate), + ], }) - - x5c = (await chain.build(leafCert)).map((cert) => cert.toString('base64')) + certificateChain = (await builder.build(new x509.X509Certificate(leafCert.rawCertificate))).map((c) => + c.toString('base64') + ) x509.cryptoProvider.clear() }) @@ -122,6 +112,99 @@ describe('X509Service', () => { await wallet.close() }) + it('should create a valid self-signed certificate', async () => { + const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) + const certificate = await X509Service.createCertificate(agentContext, { + authorityKey, + issuer: { commonName: 'credo' }, + }) + + 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 () => { + const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) + const certificate = await X509Service.createCertificate(agentContext, { + authorityKey, + issuer: { commonName: 'credo' }, + extensions: { + keyUsage: { + usages: [X509KeyUsage.CrlSign, X509KeyUsage.KeyCertSign], + markAsCritical: true, + }, + extendedKeyUsage: { + usages: [X509ExtendedKeyUsage.MdlDs], + markAsCritical: false, + }, + }, + }) + + expect(certificate.isExtensionCritical(id_ce_keyUsage)).toStrictEqual(true) + expect(certificate.isExtensionCritical(id_ce_extKeyUsage)).toStrictEqual(false) + }) + + it('should create a valid self-signed certifcate with extensions', async () => { + const authorityKey = await wallet.createKey({ keyType: KeyType.P256 }) + const certificate = await X509Service.createCertificate(agentContext, { + authorityKey, + issuer: { commonName: 'credo' }, + extensions: { + subjectAlternativeName: { + name: [ + { type: 'url', value: 'animo.id' }, + { type: 'dns', value: 'paradym.id' }, + ], + }, + keyUsage: { + usages: [X509KeyUsage.DigitalSignature], + }, + extendedKeyUsage: { + usages: [X509ExtendedKeyUsage.MdlDs], + }, + subjectKeyIdentifier: { + include: true, + }, + }, + }) + + expect(certificate).toMatchObject({ + sanDnsNames: expect.arrayContaining(['paradym.id']), + sanUriNames: expect.arrayContaining(['animo.id']), + keyUsage: expect.arrayContaining([X509KeyUsage.DigitalSignature]), + extendedKeyUsage: expect.arrayContaining([X509ExtendedKeyUsage.MdlDs]), + subjectKeyIdentifier: 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 }) + + const certificate = await X509Service.createCertificate(agentContext, { + authorityKey, + subjectPublicKey: new Key(subjectKey.publicKey, KeyType.P256), + issuer: { commonName: 'credo' }, + subject: { commonName: 'DCS credo' }, + extensions: { + subjectKeyIdentifier: { include: true }, + authorityKeyIdentifier: { include: true }, + }, + }) + + expect(certificate.subjectKeyIdentifier).toStrictEqual(TypedArrayEncoder.toHex(subjectKey.publicKey)) + expect(certificate.authorityKeyIdentifier).toStrictEqual(TypedArrayEncoder.toHex(authorityKey.publicKey)) + expect(certificate.publicKey.keyType).toStrictEqual(KeyType.P256) + expect(certificate.publicKey.publicKey.length).toStrictEqual(65) + expect(certificate.subject).toStrictEqual('CN=DCS credo') + expect(certificate).toMatchObject({ + subjectKeyIdentifier: TypedArrayEncoder.toHex(subjectKey.publicKey), + authorityKeyIdentifier: TypedArrayEncoder.toHex(authorityKey.publicKey), + }) + }) + it('should correctly parse an X.509 certificate with an uncompressed key to a JWK', async () => { const encodedCertificate = 'MIICKjCCAdCgAwIBAgIUV8bM0wi95D7KN0TyqHE42ru4hOgwCgYIKoZIzj0EAwIwUzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQHDAZBbGJhbnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UECwwGTlkgRE1WMB4XDTIzMDkxNDE0NTUxOFoXDTMzMDkxMTE0NTUxOFowUzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQHDAZBbGJhbnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UECwwGTlkgRE1WMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiTwtg0eQbcbNabf2Nq9L/VM/lhhPCq2s0Qgw2kRx29tgrBcNHPxTT64tnc1Ij3dH/fl42SXqMenpCDw4K6ntU6OBgTB/MB0GA1UdDgQWBBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAfBgNVHSMEGDAWgBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAPBgNVHRMBAf8EBTADAQH/MCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAKBggqhkjOPQQDAgNIADBFAiAJ/Qyrl7A+ePZOdNfc7ohmjEdqCvxaos6//gfTvncuqQIhANo4q8mKCA9J8k/+zh//yKbN1bLAtdqPx7dnrDqV3Lg+' @@ -143,77 +226,20 @@ describe('X509Service', () => { }) }) - it('should parse a valid X.509 certificate', async () => { - const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) - const certificate = await X509Service.createSelfSignedCertificate(agentContext, { - key, - extensions: [ - [ - { type: 'url', value: 'animo.id' }, - { type: 'dns', value: 'paradym.id' }, - ], - [ - { type: 'dns', value: 'wallet.paradym.id' }, - { type: 'dns', value: 'animo.id' }, - ], - ], - }) - const encodedCertificate = certificate.toString('base64') - - const x509Certificate = X509Service.parseCertificate(agentContext, { encodedCertificate }) - - expect(x509Certificate).toMatchObject({ - sanDnsNames: expect.arrayContaining(['paradym.id', 'wallet.paradym.id', 'animo.id']), - sanUriNames: expect.arrayContaining(['animo.id']), - }) - - expect(x509Certificate.publicKey.publicKey.length).toStrictEqual(65) - }) - it('should correctly parse x5c chain provided as a test-vector', async () => { - const x5c = [ + const certificateChain = [ 'MIICaTCCAg+gAwIBAgIUShyxcIZGiPV3wBRp4YOlNp1I13YwCgYIKoZIzj0EAwIwgYkxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZiZHIuZGUxDzANBgNVBAcMBkJlcmxpbjEMMAoGA1UECgwDQkRSMQ8wDQYDVQQLDAZNYXVyZXIxHTAbBgNVBAMMFGlzc3VhbmNlLXRlc3QuYmRyLmRlMRowGAYJKoZIhvcNAQkBFgt0ZXN0QGJkci5kZTAeFw0yNDA1MjgwODIyMjdaFw0zNDA0MDYwODIyMjdaMIGJMQswCQYDVQQGEwJERTEPMA0GA1UECAwGYmRyLmRlMQ8wDQYDVQQHDAZCZXJsaW4xDDAKBgNVBAoMA0JEUjEPMA0GA1UECwwGTWF1cmVyMR0wGwYDVQQDDBRpc3N1YW5jZS10ZXN0LmJkci5kZTEaMBgGCSqGSIb3DQEJARYLdGVzdEBiZHIuZGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASygZ1Ma0m9uif4n8g3CiCP+E1r2KWFxVmS6LRWqUBMgn5fODKIBftdzVSbv/38gujy5qxh/q5bLcT+yLilazCao1MwUTAdBgNVHQ4EFgQUMGdPNMIdo3iHfqt2jlTnBNCfRNAwHwYDVR0jBBgwFoAUMGdPNMIdo3iHfqt2jlTnBNCfRNAwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiAu2h5xulXReb5IhgpkYiYR1BONTtsjT7nfzQAhL4ISOQIhAK6jKwwf6fTTSZwvJUOAu7dz1Dy/DmH19Lef0zqaNNht', ] - const chain = await X509Service.validateCertificateChain(agentContext, { certificateChain: x5c }) + const chain = await X509Service.validateCertificateChain(agentContext, { certificateChain }) expect(chain.length).toStrictEqual(1) expect(chain[0].sanDnsNames).toStrictEqual([]) expect(chain[0].sanUriNames).toStrictEqual([]) }) - it('should parse a valid X.509 certificate', async () => { - const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) - const certificate = await X509Service.createSelfSignedCertificate(agentContext, { - key, - extensions: [ - [ - { type: 'url', value: 'animo.id' }, - { type: 'dns', value: 'paradym.id' }, - ], - [ - { type: 'dns', value: 'wallet.paradym.id' }, - { type: 'dns', value: 'animo.id' }, - ], - ], - }) - const encodedCertificate = certificate.toString('base64') - - const x509Certificate = X509Service.parseCertificate(agentContext, { encodedCertificate }) - - expect(x509Certificate).toMatchObject({ - sanDnsNames: expect.arrayContaining(['paradym.id', 'wallet.paradym.id', 'animo.id']), - sanUriNames: expect.arrayContaining(['animo.id']), - authorityKeyIdentifier: TypedArrayEncoder.toHex(key.publicKey), - subjectKeyIdentifier: TypedArrayEncoder.toHex(key.publicKey), - keyUsage: [KeyUsage.DigitalSignature, KeyUsage.KeyCertSign], - }) - - expect(x509Certificate.publicKey.publicKey.length).toStrictEqual(65) - }) - it('should validate a valid certificate chain', async () => { - const validatedChain = await X509Service.validateCertificateChain(agentContext, { certificateChain: x5c }) + const validatedChain = await X509Service.validateCertificateChain(agentContext, { certificateChain }) expect(validatedChain.length).toStrictEqual(3) @@ -227,108 +253,53 @@ describe('X509Service', () => { }) }) - it('should generate a self-signed X509 Certificate with Ed25519', async () => { - const key = await agentContext.wallet.createKey({ keyType: KeyType.Ed25519 }) - - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(agentContext, { - key, - }) - - expect(selfSignedCertificate.publicKey.publicKeyBase58).toStrictEqual(key.publicKeyBase58) - expect(selfSignedCertificate.sanDnsNames).toStrictEqual([]) - expect(selfSignedCertificate.sanUriNames).toStrictEqual([]) - - const pemCertificate = selfSignedCertificate.toString('pem') - - expect(pemCertificate.startsWith('-----BEGIN CERTIFICATE-----\n')).toBeTruthy() - expect(pemCertificate.endsWith('\n-----END CERTIFICATE-----')).toBeTruthy() - }) - - it('should generate a self-signed X509 Certificate with P256', async () => { - const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) - - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(agentContext, { - key, - }) - - expect(selfSignedCertificate.publicKey.publicKeyBase58).toStrictEqual(key.publicKeyBase58) - expect(selfSignedCertificate.sanDnsNames).toStrictEqual([]) - expect(selfSignedCertificate.sanUriNames).toStrictEqual([]) - - const pemCertificate = selfSignedCertificate.toString('pem') - - expect(pemCertificate.startsWith('-----BEGIN CERTIFICATE-----\n')).toBeTruthy() - expect(pemCertificate.endsWith('\n-----END CERTIFICATE-----')).toBeTruthy() - }) - - it('should generate a self-signed X509 Certificate with extensions', async () => { - const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) - - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(agentContext, { - key, - name: 'C=DOO', - extensions: [ - [ - { type: 'dns', value: 'dns:me' }, - { type: 'url', value: 'some://scheme' }, - ], - ], - includeAuthorityKeyIdentifier: true, - }) - - expect(selfSignedCertificate.publicKey).toMatchObject({ - publicKeyBase58: key.publicKeyBase58, - }) - - expect(selfSignedCertificate).toMatchObject({ - sanDnsNames: expect.arrayContaining(['dns:me']), - sanUriNames: expect.arrayContaining(['some://scheme']), - }) - }) - it('should not validate a certificate with a `notBefore` of > Date.now', async () => { - const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) - - const selfSignedCertificate = ( - await X509Service.createSelfSignedCertificate(agentContext, { - key, - notBefore: getNextMonth(), + const authorityKey = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) + + const certificate = ( + await X509Service.createCertificate(agentContext, { + authorityKey, + issuer: 'CN=credo', + validity: { + notBefore: getNextMonth(), + }, }) ).toString('base64') expect( async () => await X509Service.validateCertificateChain(agentContext, { - certificateChain: [selfSignedCertificate], + certificateChain: [certificate], }) ).rejects.toThrow(X509Error) }) it('should not validate a certificate with a `notAfter` of < Date.now', async () => { - const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) - - const selfSignedCertificate = ( - await X509Service.createSelfSignedCertificate(agentContext, { - key, - notAfter: getLastMonth(), + const authorityKey = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) + + const certificate = ( + await X509Service.createCertificate(agentContext, { + authorityKey, + issuer: 'CN=credo', + validity: { + notAfter: getLastMonth(), + }, }) ).toString('base64') expect( async () => await X509Service.validateCertificateChain(agentContext, { - certificateChain: [selfSignedCertificate], + certificateChain: [certificate], }) ).rejects.toThrow(X509Error) }) it('should not validate a certificate chain if incorrect signing order', async () => { - const certificateChain = [x5c[1], x5c[2], x5c[0]] - expect( async () => await X509Service.validateCertificateChain(agentContext, { - certificateChain, + certificateChain: [certificateChain[1], certificateChain[2], certificateChain[0]], }) ).rejects.toThrow(X509Error) }) diff --git a/packages/core/src/modules/x509/extensions/IssuerAlternativeNameExtension.ts b/packages/core/src/modules/x509/extensions/IssuerAlternativeNameExtension.ts new file mode 100644 index 0000000000..db65e3285c --- /dev/null +++ b/packages/core/src/modules/x509/extensions/IssuerAlternativeNameExtension.ts @@ -0,0 +1,27 @@ +import type { TextObject } from '@peculiar/x509' + +import { id_ce_issuerAltName, id_ce_subjectAltName } from '@peculiar/asn1-x509' +import { Extension, GeneralNames, ExtensionFactory } from '@peculiar/x509' + +export class IssuerAlternativeNameExtension extends Extension { + public names!: GeneralNames + + 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 override toTextObject(): TextObject { + const obj = this.toTextObjectWithoutValue() + + const namesObj = this.names.toTextObject() + for (const key in namesObj) { + obj[key] = namesObj[key] + } + + return obj + } +} + +ExtensionFactory.register(id_ce_issuerAltName, IssuerAlternativeNameExtension) diff --git a/packages/core/src/modules/x509/extensions/index.ts b/packages/core/src/modules/x509/extensions/index.ts new file mode 100644 index 0000000000..86f0db6bb1 --- /dev/null +++ b/packages/core/src/modules/x509/extensions/index.ts @@ -0,0 +1 @@ +export * from './IssuerAlternativeNameExtension' diff --git a/packages/core/src/modules/x509/index.ts b/packages/core/src/modules/x509/index.ts index abe5a67c0d..e6ad771a46 100644 --- a/packages/core/src/modules/x509/index.ts +++ b/packages/core/src/modules/x509/index.ts @@ -5,4 +5,5 @@ export * from './X509Api' export * from './X509Module' export * from './X509ModuleConfig' export * from './X509ServiceOptions' +export * from './utils' export * from './extraction' diff --git a/packages/core/src/modules/x509/utils/extensions.ts b/packages/core/src/modules/x509/utils/extensions.ts new file mode 100644 index 0000000000..29597e9a4e --- /dev/null +++ b/packages/core/src/modules/x509/utils/extensions.ts @@ -0,0 +1,71 @@ +import type { Key } from '../../../crypto' +import type { X509CertificateExtensionsOptions } from '../X509ServiceOptions' + +import { + AuthorityKeyIdentifierExtension, + ExtendedKeyUsageExtension, + KeyUsagesExtension, + SubjectKeyIdentifierExtension, + SubjectAlternativeNameExtension, + BasicConstraintsExtension, +} from '@peculiar/x509' + +import { TypedArrayEncoder } from '../../../utils' +import { IssuerAlternativeNameExtension } from '../extensions' + +export const createSubjectKeyIdentifierExtension = ( + options: X509CertificateExtensionsOptions['subjectKeyIdentifier'], + additionalOptions: { key: Key } +) => { + if (!options || !options.include) return + + return new SubjectKeyIdentifierExtension(TypedArrayEncoder.toHex(additionalOptions.key.publicKey)) +} + +export const createKeyUsagesExtension = (options: X509CertificateExtensionsOptions['keyUsage']) => { + if (!options) return + + const flags = options.usages.reduce((prev, curr) => prev | curr, 0) + + return new KeyUsagesExtension(flags, options.markAsCritical) +} + +export const createExtendedKeyUsagesExtension = (options: X509CertificateExtensionsOptions['extendedKeyUsage']) => { + if (!options) return + + return new ExtendedKeyUsageExtension(options.usages, options.markAsCritical) +} + +export const createAuthorityKeyIdentifierExtension = ( + options: X509CertificateExtensionsOptions['authorityKeyIdentifier'], + additionalOptions: { key: Key } +) => { + if (!options) return + + return new AuthorityKeyIdentifierExtension( + TypedArrayEncoder.toHex(additionalOptions.key.publicKey), + options.markAsCritical + ) +} + +export const createIssuerAlternativeNameExtension = ( + options: X509CertificateExtensionsOptions['issuerAlternativeName'] +) => { + if (!options) return + + return new IssuerAlternativeNameExtension(options.name, options.markAsCritical) +} + +export const createSubjectAlternativeNameExtension = ( + options: X509CertificateExtensionsOptions['subjectAlternativeName'] +) => { + if (!options) return + + return new SubjectAlternativeNameExtension(options.name, options.markAsCritical) +} + +export const createBasicConstraintsExtension = (options: X509CertificateExtensionsOptions['basicConstraints']) => { + if (!options) return + + return new BasicConstraintsExtension(options.ca, options.pathLenConstraint, options.markAsCritical) +} diff --git a/packages/core/src/modules/x509/utils/index.ts b/packages/core/src/modules/x509/utils/index.ts new file mode 100644 index 0000000000..d9c24bdb10 --- /dev/null +++ b/packages/core/src/modules/x509/utils/index.ts @@ -0,0 +1,2 @@ +export * from './nameConversion' +export * from './extensions' diff --git a/packages/core/src/modules/x509/utils/nameConversion.ts b/packages/core/src/modules/x509/utils/nameConversion.ts new file mode 100644 index 0000000000..bce46f4204 --- /dev/null +++ b/packages/core/src/modules/x509/utils/nameConversion.ts @@ -0,0 +1,21 @@ +import type { X509CertificateIssuerAndSubjectOptions } from '../X509ServiceOptions' + +import { X509Error } from '../X509Error' + +export const convertName = (name: string | X509CertificateIssuerAndSubjectOptions) => { + if (typeof name === 'string') return name + + let nameBuilder = '' + + if (name.commonName) nameBuilder = nameBuilder.concat(`CN=${name.commonName}, `) + if (name.countryName) nameBuilder = nameBuilder.concat(`C=${name.countryName}, `) + if (name.organizationalUnit) nameBuilder = nameBuilder.concat(`OU=${name.organizationalUnit}, `) + if (name.stateOrProvinceName) nameBuilder = nameBuilder.concat(`S=${name.stateOrProvinceName}, `) + + if (nameBuilder.length === 0) { + throw new X509Error('Provided name object has no entries. Could not generate an issuer/subject name') + } + + // Remove the trailing `, ` + return nameBuilder.slice(0, nameBuilder.length - 2) +} diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index cc459d6d3d..15559b7012 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -803,14 +803,18 @@ export async function createDidKidVerificationMethod(agentContext: AgentContext, export async function createX509Certificate(agentContext: AgentContext, dns: string, key?: Key) { const x509 = agentContext.dependencyManager.resolve(X509Api) - const certificate = await x509.createSelfSignedCertificate({ - key: + const certificate = await x509.createCertificate({ + authorityKey: key ?? (await agentContext.wallet.createKey({ keyType: KeyType.Ed25519, })), - name: 'C=DE', - extensions: [[{ type: 'dns', value: dns }]], + issuer: 'C=DE', + extensions: { + subjectAlternativeName: { + name: [{ type: 'dns', value: dns }], + }, + }, }) return { certificate, base64Certificate: certificate.toString('base64') } diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 30ddf0a54c..b8228e5e49 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -927,16 +927,17 @@ describe('OpenId4Vc', () => { }, }) - const certificate = await verifier.agent.x509.createSelfSignedCertificate({ - key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), - extensions: [[{ type: 'dns', value: `localhost:${serverPort}` }]], + const certificate = await verifier.agent.x509.createCertificate({ + issuer: 'CN=credo', + authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: `localhost:${serverPort}` }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) - await holder.agent.x509.addTrustedCertificate(rawCertificate) - await verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.addTrustedCertificate(rawCertificate) + verifier.agent.x509.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredential', @@ -1159,16 +1160,17 @@ describe('OpenId4Vc', () => { }, }) - const certificate = await verifier.agent.x509.createSelfSignedCertificate({ - key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), - extensions: [[{ type: 'dns', value: `localhost:${serverPort}` }]], + const certificate = await verifier.agent.x509.createCertificate({ + issuer: 'CN=credo', + authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: `localhost:${serverPort}` }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) - await holder.agent.x509.addTrustedCertificate(rawCertificate) - await verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.addTrustedCertificate(rawCertificate) + verifier.agent.x509.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredential', @@ -1406,17 +1408,18 @@ describe('OpenId4Vc', () => { }, }) - const certificate = await verifier.agent.x509.createSelfSignedCertificate({ - key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), - extensions: [[{ type: 'dns', value: `localhost:${serverPort}` }]], + const certificate = await verifier.agent.x509.createCertificate({ + issuer: 'CN=credo', + authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: `localhost:${serverPort}` }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) await holder.agent.sdJwtVc.store(signedSdJwtVc2.compact) - await holder.agent.x509.addTrustedCertificate(rawCertificate) - await verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.addTrustedCertificate(rawCertificate) + verifier.agent.x509.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredentials', @@ -1712,13 +1715,12 @@ describe('OpenId4Vc', () => { it('e2e flow with tenants, issuer endpoints requesting a mdoc', async () => { const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) - const selfSignedIssuerCertificate = await issuerTenant1.x509.createSelfSignedCertificate({ - key: await issuerTenant1.wallet.createKey({ keyType: KeyType.P256 }), - extensions: [], - name: 'C=DE', + const issuerCertificate = await issuerTenant1.x509.createCertificate({ + authorityKey: await issuerTenant1.wallet.createKey({ keyType: KeyType.P256 }), + issuer: 'C=DE', }) - const selfSignedIssuerCertPem = selfSignedIssuerCertificate.toString('pem') - await issuerTenant1.x509.setTrustedCertificates([selfSignedIssuerCertPem]) + const issuerCertificatePem = issuerCertificate.toString('pem') + await issuerTenant1.x509.setTrustedCertificates([issuerCertificatePem]) const openIdIssuerTenant1 = await issuerTenant1.modules.openId4VcIssuer.createIssuer({ dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.ES256], @@ -1760,7 +1762,7 @@ describe('OpenId4Vc', () => { }) const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - await holderTenant1.x509.setTrustedCertificates([selfSignedIssuerCertPem]) + await holderTenant1.x509.setTrustedCertificates([issuerCertificatePem]) const resolvedCredentialOffer1 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( credentialOffer1 @@ -1861,19 +1863,18 @@ describe('OpenId4Vc', () => { it('e2e flow with verifier endpoints verifying a mdoc fails without direct_post.jwt', async () => { const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(issuer.agent.context, { - key: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), - extensions: [], - name: 'C=DE', + const issuerCertificate = await X509Service.createCertificate(issuer.agent.context, { + authorityKey: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + issuer: 'C=DE', }) - await verifier.agent.x509.setTrustedCertificates([selfSignedCertificate.toString('pem')]) + await verifier.agent.x509.setTrustedCertificates([issuerCertificate.toString('pem')]) const holderKey = await holder.agent.context.wallet.createKey({ keyType: KeyType.P256 }) const signedMdoc = await issuer.agent.mdoc.sign({ docType: 'org.eu.university', holderKey, - issuerCertificate: selfSignedCertificate.toString('pem'), + issuerCertificate: issuerCertificate.toString('pem'), namespaces: { 'eu.europa.ec.eudi.pid.1': { university: 'innsbruck', @@ -1884,16 +1885,17 @@ describe('OpenId4Vc', () => { }, }) - const certificate = await verifier.agent.x509.createSelfSignedCertificate({ - key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), - extensions: [[{ type: 'dns', value: 'localhost:1234' }]], + const certificate = await verifier.agent.x509.createCertificate({ + authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost:1234' }] } }, + issuer: 'CN=credo', }) const rawCertificate = certificate.toString('base64') await holder.agent.mdoc.store(signedMdoc) - await holder.agent.x509.addTrustedCertificate(rawCertificate) - await verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.addTrustedCertificate(rawCertificate) + verifier.agent.x509.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'mDL-sample-req', @@ -1985,13 +1987,12 @@ describe('OpenId4Vc', () => { }) await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) - const selfSignedCertificate = await X509Service.createSelfSignedCertificate(issuer.agent.context, { - key: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), - extensions: [], - name: 'C=DE', + const issuerCertificate = await X509Service.createCertificate(issuer.agent.context, { + authorityKey: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }), + issuer: 'C=DE', }) - await verifier.agent.x509.setTrustedCertificates([selfSignedCertificate.toString('pem')]) + await verifier.agent.x509.setTrustedCertificates([issuerCertificate.toString('pem')]) const parsedDid = parseDid(issuer.kid) if (!parsedDid.fragment) { @@ -2003,7 +2004,7 @@ describe('OpenId4Vc', () => { const signedMdoc = await issuer.agent.mdoc.sign({ docType: 'org.eu.university', holderKey, - issuerCertificate: selfSignedCertificate.toString('pem'), + issuerCertificate: issuerCertificate.toString('pem'), namespaces: { 'eu.europa.ec.eudi.pid.1': { university: 'innsbruck', @@ -2014,16 +2015,17 @@ describe('OpenId4Vc', () => { }, }) - const certificate = await verifier.agent.x509.createSelfSignedCertificate({ - key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), - extensions: [[{ type: 'dns', value: 'localhost:1234' }]], + const certificate = await verifier.agent.x509.createCertificate({ + issuer: 'CN=credo', + authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost:1234' }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.mdoc.store(signedMdoc) - await holder.agent.x509.addTrustedCertificate(rawCertificate) - await verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.addTrustedCertificate(rawCertificate) + verifier.agent.x509.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'mDL-sample-req', @@ -2341,17 +2343,18 @@ describe('OpenId4Vc', () => { }, }) - const certificate = await verifier.agent.x509.createSelfSignedCertificate({ - key: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), - extensions: [[{ type: 'dns', value: 'localhost:1234' }]], + const certificate = await verifier.agent.x509.createCertificate({ + issuer: 'CN=credo', + authorityKey: await verifier.agent.wallet.createKey({ keyType: KeyType.Ed25519 }), + extensions: { subjectAlternativeName: { name: [{ type: 'dns', value: 'localhost:1234' }] } }, }) const rawCertificate = certificate.toString('base64') await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) await holder.agent.sdJwtVc.store(signedSdJwtVc2.compact) - await holder.agent.x509.addTrustedCertificate(rawCertificate) - await verifier.agent.x509.addTrustedCertificate(rawCertificate) + holder.agent.x509.addTrustedCertificate(rawCertificate) + verifier.agent.x509.addTrustedCertificate(rawCertificate) const presentationDefinition = { id: 'OpenBadgeCredentials',