UNPKG

@credo-ts/core

Version:

<p align="center"> <br /> <img alt="Credo Logo" src="https://github.com/openwallet-foundation/credo-ts/blob/c7886cb8377ceb8ee4efe8d264211e561a75072d/images/credo-logo.png" height="250px" /> </p> <h1 align="center"><b>Credo - Core</b></h1

222 lines (221 loc) • 9.8 kB
import { CredoError } from "../error/CredoError.mjs"; import "../error/index.mjs"; import { TypedArrayEncoder } from "../utils/TypedArrayEncoder.mjs"; import { JsonEncoder } from "../utils/JsonEncoder.mjs"; import "../utils/index.mjs"; import { JwtPayload } from "./jose/jwt/JwtPayload.mjs"; import { injectable } from "../plugins/index.mjs"; import __decorate from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.mjs"; import { KeyManagementError } from "../modules/kms/error/KeyManagementError.mjs"; import { isKnownJwaSignatureAlgorithm } from "../modules/kms/jwk/jwa.mjs"; import { getJwkHumanDescription } from "../modules/kms/jwk/humanDescription.mjs"; import { asymmetricPublicJwkMatches } from "../modules/kms/jwk/equals.mjs"; import { assertJwkAsymmetric } from "../modules/kms/jwk/knownJwk.mjs"; import { PublicJwk } from "../modules/kms/jwk/PublicJwk.mjs"; import { KeyManagementApi } from "../modules/kms/KeyManagementApi.mjs"; import "../modules/kms/index.mjs"; import { convertLegacyTrustedCertificates } from "../modules/x509/utils/convertLegacyTrustedCertificates.mjs"; import { X509ModuleConfig } from "../modules/x509/X509ModuleConfig.mjs"; import { X509Service } from "../modules/x509/X509Service.mjs"; import "../modules/x509/X509ServiceOptions.mjs"; import { isJsonObject } from "../types.mjs"; import { JWS_COMPACT_FORMAT_MATCHER } from "./JwsTypes.mjs"; import "./jose/jwt/index.mjs"; //#region src/crypto/JwsService.ts let JwsService = class JwsService { async createJwsBase(agentContext, options) { const { jwk, alg, x5c } = options.protectedHeaderOptions; const kms = agentContext.dependencyManager.resolve(KeyManagementApi); const key = await kms.getPublicKey({ keyId: options.keyId }); assertJwkAsymmetric(key); const publicJwk = PublicJwk.fromPublicJwk(key); if (x5c) { const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: x5c }); if (!asymmetricPublicJwkMatches(certificate.publicJwk.toJson(), key)) throw new CredoError("Protected header x5c does not match key for signing."); } const jwkInstance = jwk instanceof PublicJwk ? jwk : jwk ? PublicJwk.fromUnknown(jwk) : void 0; if (jwkInstance && !asymmetricPublicJwkMatches(jwkInstance.toJson(), key)) throw new CredoError("Protected header JWK does not match key for signing."); if (!publicJwk.supportedSignatureAlgorithms.includes(alg)) throw new CredoError(`alg '${alg}' is not a valid JWA signature algorithm for this jwk with ${publicJwk.jwkTypeHumanDescription}. Supported algorithms are ${publicJwk.supportedSignatureAlgorithms.join(", ")}`); const payload = options.payload instanceof JwtPayload ? JsonEncoder.toUint8Array(options.payload.toJson()) : options.payload; const base64Payload = TypedArrayEncoder.toBase64Url(payload); const base64UrlProtectedHeader = JsonEncoder.toBase64Url(this.buildProtected(options.protectedHeaderOptions)); const signResult = await kms.sign({ algorithm: alg, data: TypedArrayEncoder.fromUtf8String(`${base64UrlProtectedHeader}.${base64Payload}`), keyId: options.keyId }); return { base64Payload, base64UrlProtectedHeader, signature: TypedArrayEncoder.toBase64Url(signResult.signature) }; } async createJws(agentContext, { payload, keyId, header, protectedHeaderOptions }) { const { base64UrlProtectedHeader, signature, base64Payload } = await this.createJwsBase(agentContext, { payload, keyId, protectedHeaderOptions }); return { protected: base64UrlProtectedHeader, signature, header, payload: base64Payload }; } /** * @see {@link https://www.rfc-editor.org/rfc/rfc7515#section-3.1} * */ async createJwsCompact(agentContext, { payload, keyId, protectedHeaderOptions }) { const { base64Payload, base64UrlProtectedHeader, signature } = await this.createJwsBase(agentContext, { payload, keyId, protectedHeaderOptions }); return `${base64UrlProtectedHeader}.${base64Payload}.${signature}`; } /** * Verify a JWS */ async verifyJws(agentContext, { jws, resolveJwsSigner, trustedCertificates, jwsSigner: expectedJwsSigner, allowedJwsSignerMethods = [ "did", "jwk", "x5c" ] }) { let signatures = []; let payload; if (expectedJwsSigner && !allowedJwsSignerMethods.includes(expectedJwsSigner.method)) throw new CredoError(`jwsSigner provided with method '${expectedJwsSigner.method}', but allowed jws signer methods are ${allowedJwsSignerMethods.join(", ")}.`); if (typeof jws === "string") { if (!JWS_COMPACT_FORMAT_MATCHER.test(jws)) throw new CredoError(`Invalid JWS compact format for value '${jws}'.`); const [protectedHeader, _payload, signature] = jws.split("."); payload = _payload; signatures.push({ header: {}, protected: protectedHeader, signature }); } else if ("signatures" in jws) { signatures = jws.signatures; payload = jws.payload; } else { signatures.push(jws); payload = jws.payload; } if (signatures.length === 0) throw new CredoError("Unable to verify JWS, no signatures present in JWS."); const jwsFlattened = { signatures, payload }; const jwsSigners = []; for (const jws of signatures) { const protectedJson = JsonEncoder.fromBase64Url(jws.protected); if (!isJsonObject(protectedJson)) throw new CredoError("Unable to verify JWS, protected header is not a valid JSON object."); if (!protectedJson.alg || typeof protectedJson.alg !== "string") throw new CredoError("Unable to verify JWS, protected header alg is not provided or not a string."); const jwsSigner = expectedJwsSigner ?? await this.jwsSignerFromJws(agentContext, { jws, payload, protectedHeader: { ...protectedJson, alg: protectedJson.alg }, allowedJwsSignerMethods, resolveJwsSigner }); await this.verifyJwsSigner(agentContext, { jwsSigner, trustedCertificates }); if (!jwsSigner.jwk.supportedSignatureAlgorithms.includes(protectedJson.alg)) throw new CredoError(`alg '${protectedJson.alg}' is not a valid JWA signature algorithm for this jwk ${getJwkHumanDescription(jwsSigner.jwk.toJson())}. Supported algorithms are ${jwsSigner.jwk.supportedSignatureAlgorithms.join(", ")}`); const data = TypedArrayEncoder.fromUtf8String(`${jws.protected}.${payload}`); const signature = TypedArrayEncoder.fromBase64Url(jws.signature); jwsSigners.push(jwsSigner); const kms = agentContext.dependencyManager.resolve(KeyManagementApi); try { const { verified } = await kms.verify({ key: { publicJwk: jwsSigner.jwk.toJson() }, data, signature, algorithm: protectedJson.alg }); if (!verified) return { isValid: false, jwsSigners: [], jws: jwsFlattened }; } catch (error) { if (error instanceof KeyManagementError) return { isValid: false, jwsSigners: [], jws: jwsFlattened }; throw error; } } return { isValid: true, jwsSigners, jws: jwsFlattened }; } buildProtected(options) { return { ...options, alg: options.alg, jwk: options.jwk instanceof PublicJwk ? options.jwk.toJson() : options.jwk, kid: options.kid }; } async verifyJwsSigner(agentContext, options) { const { jwsSigner } = options; const x509ModuleConfig = agentContext.dependencyManager.resolve(X509ModuleConfig); if (jwsSigner.method === "x5c") { const trustedCertificatesFromConfig = x509ModuleConfig.trustedCertificates ?? []; const trustedCertificates = options.trustedCertificates ?? trustedCertificatesFromConfig; if (trustedCertificates.length === 0) throw new CredoError(`trustedCertificates is required when the JWS protected header contains an 'x5c' property.`); const convertedTrustedCertificates = convertLegacyTrustedCertificates(trustedCertificates); await X509Service.validateCertificateChain(agentContext, { certificateChain: jwsSigner.x5c, trustedCertificates: convertedTrustedCertificates.flatMap(({ issuance }) => issuance) }); } } async jwsSignerFromJws(agentContext, options) { const { protectedHeader, resolveJwsSigner, jws, payload, allowedJwsSignerMethods } = options; const alg = protectedHeader.alg; if (!isKnownJwaSignatureAlgorithm(alg)) throw new CredoError(`Unsupported JWA signature algorithm '${protectedHeader.alg}'`); if (protectedHeader.x5c && allowedJwsSignerMethods.includes("x5c")) { if (!Array.isArray(protectedHeader.x5c) || protectedHeader.x5c.some((certificate) => typeof certificate !== "string")) throw new CredoError("x5c header is not a valid JSON array of strings."); return { method: "x5c", jwk: X509Service.getLeafCertificate(agentContext, { certificateChain: protectedHeader.x5c }).publicJwk, x5c: protectedHeader.x5c }; } if (protectedHeader.jwk && allowedJwsSignerMethods.includes("jwk")) { if (!isJsonObject(protectedHeader.jwk)) throw new CredoError("JWK is not a valid JSON object."); return { method: "jwk", jwk: PublicJwk.fromUnknown(protectedHeader.jwk) }; } if (!resolveJwsSigner) throw new CredoError(`resolveJwsSigner is required for resolving jws signers other than 'jwk' and 'x5c'.`); try { const jwsSigner = await resolveJwsSigner({ jws, protectedHeader: { ...protectedHeader, alg }, payload }); if (!allowedJwsSignerMethods.includes(jwsSigner.method)) throw new CredoError(`resolveJwsSigner returned jws signer with method '${jwsSigner.method}', but allowed jws signer methods are ${allowedJwsSignerMethods.join(", ")}.`); return jwsSigner; } catch (error) { throw new CredoError(`Error when resolving jws signer for jws in resolveJwsSigner. ${error.message}`, { cause: error }); } } }; JwsService = __decorate([injectable()], JwsService); //#endregion export { JwsService }; //# sourceMappingURL=JwsService.mjs.map