UNPKG

@sphereon/ssi-sdk.mdl-mdoc

Version:

285 lines (261 loc) • 11.5 kB
import { com, Nullable } from '@sphereon/kmp-mdoc-core' import { calculateJwkThumbprint, globalCrypto, verifyRawSignature } from '@sphereon/ssi-sdk-ext.key-utils' import { CertificateInfo, derToPEM, getCertificateInfo, getSubjectDN, pemOrDerToX509Certificate, validateX509CertificateChain, X509ValidationResult, } from '@sphereon/ssi-sdk-ext.x509-utils' import { JWK } from '@sphereon/ssi-types' import * as crypto from 'crypto' import { Certificate, CryptoEngine, setEngine } from 'pkijs' // @ts-ignore import { fromString } from 'uint8arrays/from-string' import { IRequiredContext, VerifyCertificateChainArgs } from '../types/ImDLMdoc' type CoseKeyCbor = com.sphereon.crypto.cose.CoseKeyCbor type ICoseKeyCbor = com.sphereon.crypto.cose.ICoseKeyCbor type ToBeSignedCbor = com.sphereon.crypto.cose.ToBeSignedCbor const CoseJoseKeyMappingService = com.sphereon.crypto.CoseJoseKeyMappingService type SignatureAlgorithm = com.sphereon.crypto.generic.SignatureAlgorithm type ICoseCryptoCallbackJS = com.sphereon.crypto.ICoseCryptoCallbackJS type IKey = com.sphereon.crypto.IKey type IX509ServiceJS = com.sphereon.crypto.IX509ServiceJS type Jwk = com.sphereon.crypto.jose.Jwk const KeyInfo = com.sphereon.crypto.KeyInfo type X509VerificationProfile = com.sphereon.crypto.X509VerificationProfile const DateTimeUtils = com.sphereon.kmp.DateTimeUtils const decodeFrom = com.sphereon.kmp.decodeFrom const encodeTo = com.sphereon.kmp.encodeTo const Encoding = com.sphereon.kmp.Encoding type LocalDateTimeKMP = com.sphereon.kmp.LocalDateTimeKMP const SignatureAlgorithm = com.sphereon.crypto.generic.SignatureAlgorithm const DefaultCallbacks = com.sphereon.crypto.DefaultCallbacks export class CoseCryptoService implements ICoseCryptoCallbackJS { constructor(private context?: IRequiredContext) {} setContext(context: IRequiredContext) { this.context = context } async signAsync(input: ToBeSignedCbor, requireX5Chain: Nullable<boolean>): Promise<Int8Array> { if (!this.context) { throw Error('No context provided. Please provide a context with the setContext method or constructor') } const { keyInfo, alg, value } = input let kmsKeyRef = keyInfo.kmsKeyRef ?? undefined if (!kmsKeyRef) { const key = keyInfo.key if (key == null) { return Promise.reject(Error('No key present in keyInfo. This implementation cannot sign without a key!')) } const resolvedKeyInfo = com.sphereon.crypto.ResolvedKeyInfo.Static.fromKeyInfo(keyInfo, key) const jwkKeyInfo: com.sphereon.crypto.ResolvedKeyInfo<Jwk> = CoseJoseKeyMappingService.toResolvedJwkKeyInfo(resolvedKeyInfo) const kid = jwkKeyInfo.kid ?? calculateJwkThumbprint({ jwk: jwkKeyInfo.key.toJsonDTO() }) ?? jwkKeyInfo.key.getKidAsString(true) if (!kid) { return Promise.reject(Error('No kid present and not kmsKeyRef provided')) } kmsKeyRef = kid } const result = await this.context.agent.keyManagerSign({ algorithm: alg.jose!!.value, data: encodeTo(value, Encoding.UTF8), encoding: 'utf-8', keyRef: kmsKeyRef!!, }) return decodeFrom(result, Encoding.UTF8) } async verify1Async<CborType>( input: com.sphereon.crypto.cose.CoseSign1Cbor<CborType>, keyInfo: com.sphereon.crypto.IKeyInfo<ICoseKeyCbor>, requireX5Chain: Nullable<boolean>, ): Promise<com.sphereon.crypto.generic.IVerifySignatureResult<ICoseKeyCbor>> { const getCertAndKey = async ( x5c: Nullable<Array<string>>, ): Promise<{ issuerCert?: Certificate issuerJwk?: Jwk }> => { if (requireX5Chain && (!x5c || x5c.length === 0)) { // We should not be able to get here anyway, as the MLD-mdoc library already validated at this point. But let's make sure return Promise.reject(new Error(`No x5chain was present in the CoseSign headers!`)) } // TODO: According to the IETF spec there should be a x5t in case the x5chain is in the protected headers. In the Funke this does not seem to be done/used! issuerCert = x5c ? pemOrDerToX509Certificate(x5c[0]) : undefined let issuerJwk: Jwk | undefined if (issuerCert) { const info = await getCertificateInfo(issuerCert) issuerJwk = info.publicKeyJWK } return { issuerCert, issuerJwk } } const coseKeyInfo = CoseJoseKeyMappingService.toCoseKeyInfo(keyInfo) if (coseKeyInfo?.key?.d) { throw Error('Do not use private keys to verify!') } else if (!input.payload?.value) { return Promise.reject(Error('Signature validation without payload not supported')) } const sign1Json = input.toJson() // Let's make it a bit easier on ourselves, instead of working with CBOR const coseAlg = sign1Json.protectedHeader.alg if (!coseAlg) { return Promise.reject(Error('No alg protected header present')) } let issuerCert: Certificate | undefined let issuerCoseKey: CoseKeyCbor | undefined let kid = coseKeyInfo?.kid ?? sign1Json.protectedHeader.kid ?? sign1Json.unprotectedHeader?.kid // Please note this method does not perform chain validation. The MDL-MSO_MDOC library already performed this before this step const x5c = coseKeyInfo?.key?.getX509CertificateChain() ?? sign1Json.protectedHeader?.x5chain ?? sign1Json.unprotectedHeader?.x5chain if (!coseKeyInfo || !coseKeyInfo?.key || coseKeyInfo?.key?.x5chain) { const certAndKey = await getCertAndKey(x5c) issuerCoseKey = certAndKey.issuerJwk ? CoseJoseKeyMappingService.toCoseKey(certAndKey.issuerJwk) : undefined issuerCert = certAndKey.issuerCert } if (!issuerCoseKey) { if (!coseKeyInfo?.key) { return Promise.reject(Error(`Either a x5c needs to be in the headers, or you need to provide a key for verification`)) } if (kid === null) { kid = coseKeyInfo.key.getKidAsString(false) } issuerCoseKey = com.sphereon.crypto.cose.CoseKeyCbor.Static.fromDTO(coseKeyInfo.key) } const issuerCoseKeyInfo = new KeyInfo<CoseKeyCbor>( kid, issuerCoseKey, coseKeyInfo.opts, coseKeyInfo.keyVisibility, issuerCoseKey.getSignatureAlgorithm() ?? coseKeyInfo.signatureAlgorithm, x5c, coseKeyInfo.kmsKeyRef, coseKeyInfo.kms, coseKeyInfo.keyType ?? issuerCoseKey.getKty(), ) const recalculatedToBeSigned = input.toBeSignedJson(issuerCoseKeyInfo, SignatureAlgorithm.Static.fromCose(coseAlg)) const key = CoseJoseKeyMappingService.toJoseJwk(issuerCoseKeyInfo.key!).toJsonDTO<JWK>() const valid = await verifyRawSignature({ data: fromString(recalculatedToBeSigned.base64UrlValue, 'base64url'), signature: fromString(sign1Json.signature, 'base64url'), key, }) return { name: 'mdoc', critical: true, error: !valid, message: `Signature of '${issuerCert ? getSubjectDN(issuerCert).DN : kid}' was ${valid ? '' : 'in'}valid`, keyInfo: issuerCoseKeyInfo, } satisfies com.sphereon.crypto.generic.IVerifySignatureResult<ICoseKeyCbor> } resolvePublicKeyAsync<KT extends com.sphereon.crypto.IKey>( keyInfo: com.sphereon.crypto.IKeyInfo<KT>, ): Promise<com.sphereon.crypto.IResolvedKeyInfo<KT>> { if (keyInfo.key) { return Promise.resolve(CoseJoseKeyMappingService.toResolvedKeyInfo(keyInfo, keyInfo.key)) } return Promise.reject(Error('No key present in keyInfo. This implementation cannot resolve public keys on its own currently!')) } } /** * This class can be used for X509 validations. * Either have an instance per trustedCerts and verification invocation or use a single instance and provide the trusted certs in the method argument * * The class is also registered with the low-level mDL/mdoc Kotlin Multiplatform library * Next to the specific function for the library it exports a more powerful version of the same verification method as well */ export class X509CallbackService implements IX509ServiceJS { private _trustedCerts?: Array<string> constructor(trustedCerts?: Array<string>) { this.setTrustedCerts(trustedCerts) } /** * A more powerful version of the method below. Allows to verify at a specific time and returns more information * @param chain * @param trustAnchors * @param verificationTime */ async verifyCertificateChain({ chain, trustAnchors = this.getTrustedCerts(), verificationTime, opts, }: VerifyCertificateChainArgs): Promise<X509ValidationResult> { return await validateX509CertificateChain({ chain, trustAnchors, verificationTime, opts, }) } /** * This method is the implementation used within the mDL/Mdoc library */ async verifyCertificateChainJS<KeyType extends IKey>( chainDER: Nullable<Int8Array[]>, chainPEM: Nullable<string[]>, trustedCerts: Nullable<string[]>, verificationProfile?: X509VerificationProfile | undefined, verificationTime?: Nullable<LocalDateTimeKMP>, ): Promise<com.sphereon.crypto.IX509VerificationResult<KeyType>> { const verificationAt = verificationTime ?? DateTimeUtils.Static.DEFAULT.dateTimeLocal() let chain: Array<string | Uint8Array> = [] if (chainDER && chainDER.length > 0) { chain = chainDER.map((der) => Uint8Array.from(der)) } if (chainPEM && chainPEM.length > 0) { chain = (chain ?? []).concat(chainPEM) } const result = await validateX509CertificateChain({ chain: chain, // The function will handle an empty array trustAnchors: trustedCerts ?? this.getTrustedCerts(), verificationTime: new Date(verificationAt.toEpochSeconds().toULong() * 1000), opts: { trustRootWhenNoAnchors: true }, }) const cert: CertificateInfo | undefined = result.certificateChain ? result.certificateChain[result.certificateChain.length - 1] : undefined return { publicKey: cert?.publicKeyJWK as KeyType, // fixme publicKeyAlgorithm: cert?.publicKeyJWK?.alg, name: 'x.509', critical: result.critical, message: result.message, error: result.error, verificationTime: verificationAt, } satisfies com.sphereon.crypto.IX509VerificationResult<KeyType> } setTrustedCerts = (trustedCertsInPEM?: Array<string>) => { this._trustedCerts = trustedCertsInPEM?.map((cert) => { if (cert.includes('CERTIFICATE')) { // PEM return cert } return derToPEM(cert) }) } getTrustedCerts = () => this._trustedCerts } const defaultCryptoEngine = () => { // @ts-ignore if (typeof self !== 'undefined') { // @ts-ignore if ('crypto' in self) { let engineName = 'webcrypto' // @ts-ignore if ('webkitSubtle' in self.crypto) { engineName = 'safari' } // @ts-ignore setEngine(engineName, new CryptoEngine({ name: engineName, crypto: crypto })) } } else if (typeof crypto !== 'undefined' && 'webcrypto' in crypto) { const name = 'NodeJS ^15' const nodeCrypto = crypto.webcrypto // @ts-ignore setEngine(name, new CryptoEngine({ name, crypto: nodeCrypto })) } else { // @ts-ignore const name = 'crypto' setEngine(name, new CryptoEngine({ name, crypto: globalCrypto(false) })) } } defaultCryptoEngine() // We register the services with the mDL/mdoc library. Please note that the context is not passed in, meaning we cannot sign by default. DefaultCallbacks.setCoseCryptoDefault(new CoseCryptoService()) DefaultCallbacks.setX509Default(new X509CallbackService())