Skip to content

Commit

Permalink
feat(x509): able to add Certificate Authority Key Identifier to self-…
Browse files Browse the repository at this point in the history
…signed certificates

Signed-off-by: Berend Sliedrecht <sliedrecht@berend.io>
  • Loading branch information
Berend Sliedrecht committed Nov 25, 2024
1 parent e70a2ce commit 6038642
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 19 deletions.
109 changes: 90 additions & 19 deletions packages/core/src/modules/x509/X509Certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { X509CreateSelfSignedCertificateOptions } from './X509ServiceOption
import type { CredoWebCrypto } from '../../crypto/webcrypto'

import { AsnParser } from '@peculiar/asn1-schema'
import { id_ce_subjectAltName, SubjectPublicKeyInfo } from '@peculiar/asn1-x509'
import {
id_ce_authorityKeyIdentifier,
id_ce_subjectAltName,
id_ce_subjectKeyIdentifier,
SubjectPublicKeyInfo,
} from '@peculiar/asn1-x509'
import * as x509 from '@peculiar/x509'

import { Key } from '../../crypto/Key'
Expand All @@ -14,7 +19,16 @@ import { TypedArrayEncoder } from '../../utils'

import { X509Error } from './X509Error'

type Extension = Record<string, undefined | Array<{ type: string; value: string }>>
type ExtensionObjectIdentifier = string

type SubjectAlternativeNameExtension = Array<{ type: 'url' | 'dns'; value: string }>
type AuthorityKeyIdentifierExtension = { keyId: string }
type SubjectKeyIdentifierExtension = { keyId: string }

type ExtensionValues = SubjectAlternativeNameExtension | AuthorityKeyIdentifierExtension | SubjectKeyIdentifierExtension

type Extension = Record<ExtensionObjectIdentifier, ExtensionValues>

export type ExtensionInput = Array<Array<{ type: 'dns' | 'url'; value: string }>>

export type X509CertificateOptions = {
Expand Down Expand Up @@ -67,38 +81,83 @@ export class X509Certificate {

const key = new Key(keyBytes, keyType)

const extensions = certificate.extensions
.map((e) => {
if (e instanceof x509.AuthorityKeyIdentifierExtension) {
return { [e.type]: { keyId: e.keyId as string } }
} else if (e instanceof x509.SubjectKeyIdentifierExtension) {
return { [e.type]: { keyId: e.keyId } }
} else if (e instanceof x509.SubjectAlternativeNameExtension) {
return { [e.type]: JSON.parse(JSON.stringify(e.names)) as SubjectAlternativeNameExtension }
}

// 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<typeof e, undefined> => e !== undefined)

return new X509Certificate({
publicKey: key,
privateKey,
extensions: certificate.extensions
?.map((e) => JSON.parse(JSON.stringify(e)))
.map((e) => ({ [e.type]: e.names })) as Array<Extension>,
extensions,
rawCertificate: new Uint8Array(certificate.rawData),
})
}

private getMatchingExtensions<T>(name: string, type: string): Array<T> | undefined {
const extensionsWithName = this.extensions
?.filter((e) => e[name])
?.flatMap((e) => e[name])
?.filter((e): e is Exclude<typeof e, undefined> => e !== undefined && e.type === type)
?.map((e) => e.value)

return extensionsWithName as Array<T>
private getMatchingExtensions<T extends ExtensionValues>(objectIdentifier: string): Array<T> | undefined {
return this.extensions?.map((e) => e[objectIdentifier])?.filter(Boolean) as Array<T> | undefined
}

public get sanDnsNames() {
const subjectAlternativeNameExtensionDns = this.getMatchingExtensions<string>(id_ce_subjectAltName, 'dns')
return subjectAlternativeNameExtensionDns?.filter((e) => typeof e === 'string') ?? []
const san = this.getMatchingExtensions<SubjectAlternativeNameExtension>(id_ce_subjectAltName)
return san

Check failure on line 114 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Insert `(⏎······`
?.flatMap((e) => e)

Check failure on line 115 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Insert `··`
?.filter((e) => e.type === 'dns')

Check failure on line 116 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Insert `··`
?.map((e) => e.value) ?? []

Check failure on line 117 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Replace `?.map((e)·=>·e.value)·??·[]` with `··?.map((e)·=>·e.value)·??·[]⏎····)`
}

public get sanUriNames() {
const subjectAlternativeNameExtensionUri = this.getMatchingExtensions<string>(id_ce_subjectAltName, 'url')
return subjectAlternativeNameExtensionUri?.filter((e) => typeof e === 'string') ?? []
const san = this.getMatchingExtensions<SubjectAlternativeNameExtension>(id_ce_subjectAltName)
return san

Check failure on line 122 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Insert `(⏎······`
?.flatMap((e) => e)

Check failure on line 123 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Insert `··`
?.filter((e) => e.type === 'url')

Check failure on line 124 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Insert `··`
?.map((e) => e.value) ?? []

Check failure on line 125 in packages/core/src/modules/x509/X509Certificate.ts

View workflow job for this annotation

GitHub Actions / Validate

Replace `?.map((e)·=>·e.value)·??·[]` with `··?.map((e)·=>·e.value)·??·[]⏎····)`
}

public get authorityKeyIdentifier() {
const keyIds = this.getMatchingExtensions<AuthorityKeyIdentifierExtension>(id_ce_authorityKeyIdentifier)?.map(
(e) => e.keyId
)

if (keyIds && keyIds.length > 1) {
throw new X509Error('Multiple Authority Key Identifiers are not allowed')
}

return keyIds?.[0]
}

public get subjectKeyIdentifier() {
const keyIds = this.getMatchingExtensions<SubjectKeyIdentifierExtension>(id_ce_subjectKeyIdentifier)?.map(
(e) => e.keyId
)

if (keyIds && keyIds.length > 1) {
throw new X509Error('Multiple Subject Key Identifiers are not allowed')
}

return keyIds?.[0]
}

public static async createSelfSigned(
{ key, extensions, notAfter, notBefore, name }: X509CreateSelfSignedCertificateOptions,
{
key,
extensions,
notAfter,
notBefore,
name,
includeAuthorityKeyIdentifier = true,
}: X509CreateSelfSignedCertificateOptions,
webCrypto: CredoWebCrypto
) {
const cryptoKeyAlgorithm = credoKeyTypeIntoCryptoKeyAlgorithm(key.keyType)
Expand All @@ -120,11 +179,23 @@ export class X509Certificate {
]
: name

const hexPublicKey = TypedArrayEncoder.toHex(key.publicKey)

const x509Extensions: Array<x509.Extension> = [new x509.SubjectKeyIdentifierExtension(hexPublicKey)]

if (includeAuthorityKeyIdentifier) {
x509Extensions.push(new x509.AuthorityKeyIdentifierExtension(hexPublicKey))
}

for (const extension of extensions ?? []) {
x509Extensions.push(new x509.SubjectAlternativeNameExtension(extension))
}

const certificate = await x509.X509CertificateGenerator.createSelfSigned(
{
keys: { publicKey, privateKey },
name: issuerName,
extensions: extensions?.map((extension) => new x509.SubjectAlternativeNameExtension(extension)),
extensions: x509Extensions,
notAfter,
notBefore,
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/modules/x509/X509ServiceOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface X509ValidateCertificateChainOptions {
export interface X509CreateSelfSignedCertificateOptions {
key: Key
extensions?: ExtensionInput
includeAuthorityKeyIdentifier?: boolean
notBefore?: Date
notAfter?: Date
name?: string
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/modules/x509/__tests__/X509Service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { CredoWebCrypto, CredoWebCryptoKey } from '../../../crypto/webcrypto'
import { X509Error } from '../X509Error'
import { X509Service } from '../X509Service'

import { TypedArrayEncoder } from '@credo-ts/core'

/**
*
* Get the next month, accounting for a new year
Expand Down Expand Up @@ -199,6 +201,8 @@ describe('X509Service', () => {
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),
})

expect(x509Certificate.publicKey.publicKey.length).toStrictEqual(33)
Expand Down Expand Up @@ -258,12 +262,14 @@ describe('X509Service', () => {

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({
Expand Down

0 comments on commit 6038642

Please sign in to comment.