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

420 lines (416 loc) • 19.5 kB
import { CredoError } from "../../error/CredoError.mjs"; import "../../error/index.mjs"; import { TypedArrayEncoder } from "../../utils/TypedArrayEncoder.mjs"; import { Hasher } from "../../crypto/hashes/Hasher.mjs"; import { IntegrityVerifier } from "../../utils/IntegrityVerifier.mjs"; import { dateToSeconds, nowInSeconds } from "../../utils/timestamp.mjs"; import "../../utils/index.mjs"; import { JwtPayload } from "../../crypto/jose/jwt/JwtPayload.mjs"; import { injectable } from "../../plugins/index.mjs"; import { __decorateMetadata } from "../../_virtual/_@oxc-project_runtime@0.112.0/helpers/decorateMetadata.mjs"; import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.112.0/helpers/decorate.mjs"; import "../../agent/index.mjs"; import { KeyManagementApi } from "../kms/KeyManagementApi.mjs"; import "../kms/index.mjs"; import { X509Certificate } from "../x509/X509Certificate.mjs"; import { X509ModuleConfig } from "../x509/X509ModuleConfig.mjs"; import { X509Service } from "../x509/X509Service.mjs"; import "../x509/index.mjs"; import { parseDid } from "../dids/domain/parse.mjs"; import { getPublicJwkFromVerificationMethod } from "../dids/domain/key-type/keyDidMapping.mjs"; import "../dids/index.mjs"; import { ClaimFormat } from "../vc/models/ClaimFormat.mjs"; import "../../crypto/index.mjs"; import { extractKeyFromHolderBinding, getSdJwtSigner, getSdJwtVerifier, parseHolderBindingFromCredential, resolveDidUrl, resolveSigningPublicJwkFromDidUrl } from "./utils.mjs"; import "../vc/index.mjs"; import { decodeSdJwtVc, sdJwtVcHasher } from "./decodeSdJwtVc.mjs"; import { SdJwtVcRepository } from "./repository/SdJwtVcRepository.mjs"; import "./repository/index.mjs"; import { getDomainFromUrl } from "../../utils/domain.mjs"; import { fetchWithTimeout } from "../../utils/fetch.mjs"; import { buildDisclosureFrameForPayload } from "./disclosureFrame.mjs"; import { SdJwtVcError } from "./SdJwtVcError.mjs"; import { SdJwtVcModuleConfig } from "./SdJwtVcModuleConfig.mjs"; import { decodeSdJwtSync } from "@sd-jwt/decode"; import { selectDisclosures } from "@sd-jwt/present"; import { SDJwtVcInstance } from "@sd-jwt/sd-jwt-vc"; //#region src/modules/sd-jwt-vc/SdJwtVcService.ts var _ref; let SdJwtVcService = class SdJwtVcService { constructor(sdJwtVcRepository) { this.sdJwtVcRepository = sdJwtVcRepository; } async sign(agentContext, options) { const { payload, disclosureFrame, hashingAlgorithm } = options; if (hashingAlgorithm && hashingAlgorithm !== "sha-256") throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`); const issuer = await this.extractKeyFromIssuer(agentContext, options.issuer, true); const holderBinding = options.holder ? await extractKeyFromHolderBinding(agentContext, options.holder) : void 0; const header = { alg: issuer.alg, typ: options.headerType ?? "dc+sd-jwt", kid: issuer.kid, x5c: issuer.x5c?.map((cert) => cert.toString("base64")) }; const sdJwt = new SDJwtVcInstance({ ...this.getBaseSdJwtConfig(agentContext), signer: getSdJwtSigner(agentContext, issuer.publicJwk), hashAlg: "sha-256", signAlg: issuer.alg }); if (!payload.vct || typeof payload.vct !== "string") throw new SdJwtVcError("Missing required parameter 'vct'"); const compact = await sdJwt.issue({ ...payload, cnf: holderBinding?.cnf, iss: issuer.iss, iat: nowInSeconds(), vct: payload.vct }, disclosureFrame, { header }); const prettyClaims = await sdJwt.getClaims(compact); const sdJwtPayload = (await sdJwt.decode(compact)).jwt?.payload; if (!sdJwtPayload) throw new SdJwtVcError("Invalid sd-jwt-vc state."); return { compact, prettyClaims, header, holder: options.holder, payload: sdJwtPayload, claimFormat: ClaimFormat.SdJwtDc, encoded: compact }; } fromCompact(compactSdJwtVc, typeMetadata) { return decodeSdJwtVc(compactSdJwtVc, typeMetadata); } applyDisclosuresForPayload(compactSdJwtVc, requestedPayload) { const decoded = decodeSdJwtSync(compactSdJwtVc, Hasher.hash); const presentationFrame = buildDisclosureFrameForPayload(requestedPayload) ?? {}; if (decoded.kbJwt) throw new SdJwtVcError("Cannot apply limit disclosure on an sd-jwt with key binding jwt"); const requiredDisclosures = selectDisclosures(decoded.jwt.payload, decoded.disclosures.map((d) => ({ digest: d.digestSync({ alg: "sha-256", hasher: Hasher.hash }), encoded: d.encode(), key: d.key, salt: d.salt, value: d.value })), presentationFrame); const [jwt] = compactSdJwtVc.split("~"); return decodeSdJwtVc(`${jwt}~${requiredDisclosures.length > 0 ? `${requiredDisclosures.map((d) => d.encoded).join("~")}~` : ""}`); } async present(agentContext, { sdJwtVc, presentationFrame, verifierMetadata, additionalPayload }) { const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext)); const compactSdJwtVc = typeof sdJwtVc === "string" ? sdJwtVc : sdJwtVc.compact; const holderBinding = parseHolderBindingFromCredential((await sdjwt.decode(compactSdJwtVc)).jwt?.payload); if (!holderBinding && verifierMetadata) throw new SdJwtVcError("Verifier metadata provided, but credential has no 'cnf' claim to create a KB-JWT from"); const holder = holderBinding ? await extractKeyFromHolderBinding(agentContext, holderBinding, { forSigning: true, jwkKeyId: typeof sdJwtVc !== "string" ? sdJwtVc.kmsKeyId : void 0 }) : void 0; sdjwt.config({ kbSigner: holder ? getSdJwtSigner(agentContext, holder.publicJwk) : void 0, kbSignAlg: holder?.alg }); return await sdjwt.present(compactSdJwtVc, presentationFrame, { kb: verifierMetadata ? { payload: { iat: verifierMetadata.issuedAt, nonce: verifierMetadata.nonce, aud: verifierMetadata.audience, ...additionalPayload } } : void 0 }); } assertValidX5cJwtIssuer(agentContext, iss, leafCertificate) { if (!iss) return; if (!iss.startsWith("https://") && !(iss.startsWith("http://") && agentContext.config.allowInsecureHttpUrls)) throw new SdJwtVcError("The X509 certificate issuer must be a HTTPS URI."); if (!leafCertificate.sanUriNames?.includes(iss) && !leafCertificate.sanDnsNames?.includes(getDomainFromUrl(iss))) throw new SdJwtVcError(`The 'iss' claim in the payload does not match a 'SAN-URI' name and the domain extracted from the HTTPS URI does not match a 'SAN-DNS' name in the x5c certificate. Either remove the 'iss' claim or make it match with at least one SAN-URI or DNS-URI entry`); } async verify(agentContext, { compactSdJwtVc, keyBinding, requiredClaimKeys, fetchTypeMetadata, trustedCertificates, now }) { const sdjwt = new SDJwtVcInstance({ ...this.getBaseSdJwtConfig(agentContext), loadTypeMetadataFormat: false }); let sdJwtVc; let holderBinding; try { sdJwtVc = await sdjwt.decode(compactSdJwtVc); if (!sdJwtVc.jwt) throw new CredoError("Invalid sd-jwt-vc"); holderBinding = parseHolderBindingFromCredential(sdJwtVc.jwt.payload) ?? void 0; } catch (error) { return { isValid: false, error }; } const returnSdJwtVc = { payload: sdJwtVc.jwt.payload, header: sdJwtVc.jwt.header, compact: compactSdJwtVc, prettyClaims: await sdJwtVc.getClaims(sdJwtVcHasher), holder: holderBinding, kbJwt: sdJwtVc.kbJwt ? { payload: sdJwtVc.kbJwt.payload, header: sdJwtVc.kbJwt.header } : void 0, claimFormat: ClaimFormat.SdJwtDc, encoded: compactSdJwtVc }; let typeMetadataChain; try { const credentialIssuer = await this.parseIssuerFromCredential(agentContext, sdJwtVc, returnSdJwtVc, trustedCertificates); const issuer = await this.extractKeyFromIssuer(agentContext, credentialIssuer); const holder = returnSdJwtVc.holder ? await extractKeyFromHolderBinding(agentContext, returnSdJwtVc.holder) : void 0; sdjwt.config({ verifier: getSdJwtVerifier(agentContext, issuer.publicJwk), kbVerifier: holder ? getSdJwtVerifier(agentContext, holder.publicJwk) : void 0 }); try { await sdjwt.verify(compactSdJwtVc, { requiredClaimKeys: requiredClaimKeys ? [...requiredClaimKeys, "vct"] : ["vct"], keyBindingNonce: keyBinding?.nonce, currentDate: dateToSeconds(now ?? /* @__PURE__ */ new Date()), skewSeconds: agentContext.config.validitySkewSeconds }); } catch (error) { return { error, isValid: false, sdJwtVc: returnSdJwtVc }; } if (sdJwtVc.jwt.header?.typ !== "vc+sd-jwt" && sdJwtVc.jwt.header?.typ !== "dc+sd-jwt") return { error: new SdJwtVcError(`SD-JWT VC header 'typ' must be 'dc+sd-jwt' or 'vc+sd-jwt'`), isValid: false, sdJwtVc: returnSdJwtVc }; try { JwtPayload.fromJson(returnSdJwtVc.payload).validate({ now: dateToSeconds(now ?? /* @__PURE__ */ new Date()), skewSeconds: agentContext.config.validitySkewSeconds }); } catch (error) { return { error, isValid: false, sdJwtVc: returnSdJwtVc }; } try { if (keyBinding) { if (!sdJwtVc.kbJwt || !sdJwtVc.kbJwt.payload) throw new SdJwtVcError("Keybinding is required for verification of the sd-jwt-vc"); if (sdJwtVc.kbJwt.payload.aud !== keyBinding.audience) throw new SdJwtVcError("The key binding JWT does not contain the expected audience"); if (sdJwtVc.kbJwt.payload.nonce !== keyBinding.nonce) throw new SdJwtVcError("The key binding JWT does not contain the expected nonce"); } } catch (error) { return { error, isValid: false, sdJwtVc: returnSdJwtVc }; } if (fetchTypeMetadata) { const resolvedTypeMetadata = await this.fetchTypeMetadata(agentContext, returnSdJwtVc, { throwErrorOnFetchError: false, throwErrorOnUnsupportedVctValue: false }); returnSdJwtVc.typeMetadata = resolvedTypeMetadata?.mergedTypeMetadata; typeMetadataChain = resolvedTypeMetadata?.typeMetadataChain; } } catch (error) { return { isValid: false, error, sdJwtVc: returnSdJwtVc }; } return { isValid: true, sdJwtVc: returnSdJwtVc, typeMetadataChain }; } /** * Resolve the type metadata for an SD-JWT instance. * * This method resolves the `vct` value, and any `extends` values. */ async fetchTypeMetadata(agentContext, sdJwtVc, { throwErrorOnFetchError = true, throwErrorOnUnsupportedVctValue = true } = {}) { return await new SDJwtVcInstance({ ...this.getBaseSdJwtConfig(agentContext), vctFetcher: this.getVctFetcher(agentContext, { baseVct: sdJwtVc.payload.vct, throwErrorOnFetchError, throwErrorOnUnsupportedVctValue }) }).getVct(sdJwtVc.compact); } async store(agentContext, options) { await this.sdJwtVcRepository.save(agentContext, options.record); return options.record; } async getById(agentContext, id) { return await this.sdJwtVcRepository.getById(agentContext, id); } async getAll(agentContext) { return await this.sdJwtVcRepository.getAll(agentContext); } async findByQuery(agentContext, query, queryOptions) { return await this.sdJwtVcRepository.findByQuery(agentContext, query, queryOptions); } async deleteById(agentContext, id) { await this.sdJwtVcRepository.deleteById(agentContext, id); } async update(agentContext, sdJwtVcRecord) { await this.sdJwtVcRepository.update(agentContext, sdJwtVcRecord); } async extractKeyFromIssuer(agentContext, issuer, forSigning = false) { if (issuer.method === "did") { const parsedDid = parseDid(issuer.didUrl); if (!parsedDid.fragment) throw new SdJwtVcError(`didUrl '${issuer.didUrl}' does not contain a '#'. Unable to derive key from did document`); let publicJwk; if (forSigning) publicJwk = await resolveSigningPublicJwkFromDidUrl(agentContext, issuer.didUrl); else { const { verificationMethod } = await resolveDidUrl(agentContext, issuer.didUrl); publicJwk = getPublicJwkFromVerificationMethod(verificationMethod); } const supportedSignatureAlgorithms = publicJwk.supportedSignatureAlgorithms; if (supportedSignatureAlgorithms.length === 0) throw new SdJwtVcError(`No supported JWA signature algorithms found for key ${publicJwk.jwkTypeHumanDescription}`); return { alg: supportedSignatureAlgorithms[0], publicJwk, iss: parsedDid.did, kid: `#${parsedDid.fragment}` }; } if (issuer.method === "x5c") { const leafCertificate = issuer.x5c[0]; if (!leafCertificate) throw new SdJwtVcError("Empty 'x5c' array provided"); if (forSigning && !leafCertificate.publicJwk.hasKeyId) throw new SdJwtVcError("Expected leaf certificate in 'x5c' array to have a key id configured."); const publicJwk = leafCertificate.publicJwk; const supportedSignatureAlgorithms = publicJwk.supportedSignatureAlgorithms; if (supportedSignatureAlgorithms.length === 0) throw new SdJwtVcError(`No supported JWA signature algorithms found for key ${publicJwk.jwkTypeHumanDescription}`); const alg = supportedSignatureAlgorithms[0]; this.assertValidX5cJwtIssuer(agentContext, issuer.issuer, leafCertificate); return { publicJwk, iss: issuer.issuer, x5c: issuer.x5c, alg }; } throw new SdJwtVcError("Unsupported credential issuer. Only 'did' and 'x5c' is supported at the moment."); } async parseIssuerFromCredential(agentContext, sdJwtVc, credoSdJwtVc, _trustedCertificates) { const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig); if (!sdJwtVc.jwt?.payload) throw new SdJwtVcError("Credential not exist"); const iss = sdJwtVc.jwt.payload.iss; if (sdJwtVc.jwt.header?.x5c) { if (!Array.isArray(sdJwtVc.jwt.header.x5c)) throw new SdJwtVcError("Invalid x5c header in credential. Not an array."); if (sdJwtVc.jwt.header.x5c.length === 0) throw new SdJwtVcError("Invalid x5c header in credential. Empty array."); if (sdJwtVc.jwt.header.x5c.some((x5c) => typeof x5c !== "string")) throw new SdJwtVcError("Invalid x5c header in credential. Not an array of strings."); let trustedCertificates = _trustedCertificates; const certificateChain = sdJwtVc.jwt.header.x5c.map((cert) => X509Certificate.fromEncodedCertificate(cert)); if (!trustedCertificates) trustedCertificates = await x509Config.getTrustedCertificatesForVerification?.(agentContext, { certificateChain, verification: { type: "credential", credential: credoSdJwtVc } }) ?? x509Config.trustedCertificates; if (!trustedCertificates) throw new SdJwtVcError("No trusted certificates configured for X509 certificate chain validation. Issuer cannot be verified."); await X509Service.validateCertificateChain(agentContext, { certificateChain: sdJwtVc.jwt.header.x5c, trustedCertificates }); return { method: "x5c", x5c: certificateChain, issuer: iss }; } if (iss?.startsWith("did:")) { if (!sdJwtVc.jwt?.header) throw new SdJwtVcError("Credential does not contain a header"); if (!sdJwtVc.jwt.header.kid) throw new SdJwtVcError("Credential does not contain a kid in the header"); const issuerKid = sdJwtVc.jwt.header.kid; let didUrl; if (issuerKid.startsWith("#")) didUrl = `${iss}${issuerKid}`; else if (issuerKid.startsWith("did:")) { const didFromKid = parseDid(issuerKid); if (didFromKid.did !== iss) throw new SdJwtVcError(`kid in header is an absolute DID URL, but the did (${didFromKid.did}) does not match with the 'iss' did (${iss})`); didUrl = issuerKid; } else throw new SdJwtVcError("Invalid issuer kid for did. Only absolute or relative (starting with #) did urls are supported."); return { method: "did", didUrl }; } throw new SdJwtVcError("Unsupported signing method for SD-JWT VC. Only did and x5c are supported at the moment."); } getBaseSdJwtConfig(agentContext) { const kms = agentContext.resolve(KeyManagementApi); return { hasher: sdJwtVcHasher, statusListFetcher: this.getStatusListFetcher(agentContext), saltGenerator: (length) => TypedArrayEncoder.toBase64Url(kms.randomBytes({ length })).slice(0, length) }; } getStatusListFetcher(agentContext) { return async (uri) => { const response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, uri, { headers: { Accept: "application/statuslist+jwt" } }); if (!response.ok) throw new CredoError(`Received invalid response with status ${response.status} when fetching status list from ${uri}. ${await response.text()}`); return await response.text(); }; } getVctFetcher(agentContext, { baseVct, throwErrorOnFetchError = true, throwErrorOnUnsupportedVctValue = true }) { const sdJwtVcConfig = agentContext.dependencyManager.resolve(SdJwtVcModuleConfig); return async (uri, integrity) => { if (sdJwtVcConfig.customTypeMetadataResolver) return await sdJwtVcConfig.customTypeMetadataResolver(uri, integrity, { isExtendedVct: uri !== baseVct, defaultResolver: (userOverrideOptions) => this.httpsTypeMetadataResolver(agentContext, uri, integrity, { throwErrorOnFetchError: userOverrideOptions.throwErrorOnFetchError ?? throwErrorOnFetchError, throwErrorOnUnsupportedVctValue: userOverrideOptions.throwErrorOnUnsupportedVctValue ?? throwErrorOnUnsupportedVctValue, isExtendedVct: uri !== baseVct }) }); if (!uri.startsWith("https://")) { if (!throwErrorOnUnsupportedVctValue) return void 0; throw new SdJwtVcError(`Unable to resolve type metadata for vct '${uri}'. Only https supported and no 'customTypeMetadataResolver' provided in agent config.`); } return await this.httpsTypeMetadataResolver(agentContext, uri, integrity, { throwErrorOnFetchError, throwErrorOnUnsupportedVctValue, isExtendedVct: uri !== baseVct }); }; } async httpsTypeMetadataResolver(agentContext, vct, integrity, { throwErrorOnFetchError, throwErrorOnUnsupportedVctValue, isExtendedVct }) { if (!vct.startsWith("https://")) { if (!throwErrorOnUnsupportedVctValue) return void 0; throw new SdJwtVcError(`Unable to resolve type metadata for vct '${vct}'. Only https supported for default resolver`); } let firstError; const firstResponse = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, vct).catch((error) => { firstError = error; }); let response = firstResponse; if (!response || !response?.ok) { const vctElements = vct.split("/"); vctElements.splice(3, 0, ".well-known/vct"); const legacyVctUrl = vctElements.join("/"); response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, legacyVctUrl).catch(() => void 0); } if (!response?.ok) { if (!throwErrorOnFetchError) return void 0; if (firstResponse) throw new SdJwtVcError(`Unable to resolve type metadata vct '${vct}'. Fetch returned a non-successful ${firstResponse.status} response. ${await firstResponse.text()}.`, { cause: firstError }); else throw new SdJwtVcError(`Unable to resolve type metadata vct '${vct}'. Fetch returned a non-successful response.`, { cause: firstError }); } const typeMetadata = await response.clone().json(); if (integrity) { if (typeof integrity !== "string") throw new SdJwtVcError(`Found '${isExtendedVct ? "extends" : "vct"}#integrity' with value '${integrity}' but value was not of type 'string'.`); IntegrityVerifier.verifyIntegrity(new Uint8Array(await response.arrayBuffer()), integrity); } return typeMetadata; } }; SdJwtVcService = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof (_ref = typeof SdJwtVcRepository !== "undefined" && SdJwtVcRepository) === "function" ? _ref : Object])], SdJwtVcService); //#endregion export { SdJwtVcService }; //# sourceMappingURL=SdJwtVcService.mjs.map