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

417 lines (416 loc) • 19.1 kB
import { TypedArrayEncoder } from "../../utils/TypedArrayEncoder.mjs"; import "../../utils/index.mjs"; import { injectable } from "../../plugins/index.mjs"; import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.mjs"; import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.mjs"; import "../../agent/index.mjs"; import { KeyManagementApi } from "../kms/KeyManagementApi.mjs"; import "../kms/index.mjs"; import { convertLegacyTrustedCertificates } from "../x509/utils/convertLegacyTrustedCertificates.mjs"; import { X509Certificate } from "../x509/X509Certificate.mjs"; import { X509ModuleConfig } from "../x509/X509ModuleConfig.mjs"; import { EventEmitter } from "../../agent/EventEmitter.mjs"; import { X509Service } from "../x509/X509Service.mjs"; import { mapNonEmptyArray } from "../../types.mjs"; import "../../utils/credentialUseTypes.mjs"; import { TrustedIssuerContext } from "../../agent/TrustedIssuerContext.mjs"; import { getMdocContext } from "../../crypto/contexts/mdocContext.mjs"; import "../x509/index.mjs"; import { MdocDeviceRequestNotSatisfiedError, MdocError, MdocVerificationSessionExpiredError } from "./MdocError.mjs"; import { mdocSigningJwk, nameSpacesRecordToDeviceNamespaces } from "./mdocUtil.mjs"; import { Mdoc } from "./Mdoc.mjs"; import { MdocRecord } from "./repository/MdocRecord.mjs"; import { MdocRepository } from "./repository/MdocRepository.mjs"; import { MdocVerificationSessionRecord } from "./repository/MdocVerificationSessionRecord.mjs"; import { MdocVerificationSessionRepository } from "./repository/MdocVerificationSessionRepository.mjs"; import "./repository/index.mjs"; import { canUseInstanceFromCredentialRecord, useInstanceFromCredentialRecord } from "../../utils/credentialUse.mjs"; import { MdocDeviceResponse } from "./MdocDeviceResponse.mjs"; import "./MdocEvents.mjs"; import "./MdocVerificationSessionState.mjs"; import { createDeviceRequestDefinition, getDeviceRequestMatchOptions } from "./utils/deviceRequestDefinition.mjs"; import { CoseKey, EncryptionInfo, Holder, IsoMdocDcApi, Verifier, defaultVerificationCallback, onCategoryCheck, reportDeviceRequestMatch } from "@owf/mdoc"; //#region src/modules/mdoc/MdocDcApiService.ts const defaultExpiresInSeconds = 300; let MdocDcApiService = class MdocDcApiService { constructor(mdocRepository, mdocVerificationSessionRepository, eventEmitter) { this.mdocRepository = mdocRepository; this.mdocVerificationSessionRepository = mdocVerificationSessionRepository; this.eventEmitter = eventEmitter; } /** * Create an `org-iso-mdoc` request (verifier side) and persist a verification session for it. * * The origin is not part of the request payload; it only enters the protocol through the session * transcript. It therefore has to be supplied again to {@link MdocDcApiService.verifyResponse}, * except that a `readerAuth` request signs over the transcript and is fixed to the origin it is * created for. */ async createVerificationSession(agentContext, options) { const kms = agentContext.resolve(KeyManagementApi); const mdocContext = getMdocContext(agentContext); const deviceRequestDefinition = createDeviceRequestDefinition(options.docRequests, options.treatAmbiguousMultipleDocRequestsAsAlternatives); const { keyId, publicJwk } = await kms.createKey({ type: { kty: "EC", crv: "P-256" } }); try { const readerCertificateChain = options.readerAuth ? Array.isArray(options.readerAuth.certificate) ? options.readerAuth.certificate : [options.readerAuth.certificate] : void 0; const { request, encryptionInfo } = await IsoMdocDcApi.createRequest({ docRequests: deviceRequestDefinition.docRequests.map(({ docType, nameSpaces }) => ({ docType, namespaces: Object.fromEntries(Object.entries(nameSpaces).map(([nameSpace, elements]) => [nameSpace, Object.fromEntries(Object.entries(elements).map(([elementIdentifier, { intentToRetain }]) => [elementIdentifier, intentToRetain]))])) })), recipientPublicKey: CoseKey.fromJwk({ ...publicJwk, kid: keyId }), readerAuth: readerCertificateChain && options.readerAuth ? { signingKey: CoseKey.fromJwk(mdocSigningJwk(readerCertificateChain[0].publicJwk)), certificateChain: readerCertificateChain.map((certificate) => certificate.rawCertificate), origin: options.readerAuth.origin } : void 0 }, mdocContext); const verificationSession = new MdocVerificationSessionRecord({ state: "RequestCreated", deviceRequestBase64Url: request.deviceRequest, deviceRequestDefinition, sessionTranscript: { type: "isoMdocDcApi", encryptionInfoBase64Url: request.encryptionInfo, nonce: TypedArrayEncoder.toBase64Url(encryptionInfo.nonce) }, sessionKeyId: keyId, expiresAt: new Date(Date.now() + (options.expiresInSeconds ?? defaultExpiresInSeconds) * 1e3) }); await this.mdocVerificationSessionRepository.save(agentContext, verificationSession); this.emitStateChangedEvent(agentContext, verificationSession, null); return { verificationSession, request }; } catch (error) { await kms.deleteKey({ keyId }).catch((deleteError) => { agentContext.config.logger.error(`Failed to delete session key ${keyId} after failed request creation`, { deleteError, requestCreationError: error }); }); throw error; } } /** * Decrypt and verify an `org-iso-mdoc` response (verifier side). * * The response is bound to the origin through the session transcript, so decryption only succeeds * for the origin the wallet actually used. That is what makes a response relayed from another * origin fail. */ async verifyResponse(agentContext, options) { const mdocContext = getMdocContext(agentContext); const verificationSession = await this.mdocVerificationSessionRepository.getById(agentContext, options.verificationSessionId); verificationSession.assertState("RequestCreated"); const sessionTranscript = verificationSession.getSessionTranscript("isoMdocDcApi"); const response = options.response.response; try { if (verificationSession.isExpired) throw new MdocVerificationSessionExpiredError(verificationSession.expiresAt); const recipientKey = CoseKey.fromJwk({ ...EncryptionInfo.fromBase64Url(sessionTranscript.encryptionInfoBase64Url).recipientPublicKey.jwk, kid: verificationSession.sessionKeyId }); const decrypted = await IsoMdocDcApi.decryptResponse({ response, origin: options.origin, encryptionInfo: sessionTranscript.encryptionInfoBase64Url, recipientKey }, mdocContext); const deviceResponse = MdocDeviceResponse.fromBase64Url(TypedArrayEncoder.toBase64Url(decrypted.deviceResponse.encode())); const sessionTranscriptOptions = { type: "sessionTranscriptBytes", sessionTranscriptBytes: decrypted.sessionTranscript.encode() }; if (options.trustedCertificates) await deviceResponse.verify(agentContext, { trustedCertificates: options.trustedCertificates, sessionTranscriptOptions, now: options.now }); else for (const documentResponse of deviceResponse.splitIntoSingleDocumentResponses()) { const mdoc = new Mdoc(documentResponse.deviceResponse.documents[0].issuerSigned); await documentResponse.verify(agentContext, { trustedCertificates: await this.getTrustedCertificatesForMdoc(agentContext, mdoc), sessionTranscriptOptions, now: options.now }); } const deviceRequestMatch = Verifier.matchDeviceRequest({ deviceRequest: TypedArrayEncoder.fromBase64Url(verificationSession.deviceRequestBase64Url), deviceResponse: decrypted.deviceResponse, matchOptions: getDeviceRequestMatchOptions(verificationSession.deviceRequestDefinition) }); if (!deviceRequestMatch.success) { const reasons = []; reportDeviceRequestMatch(deviceRequestMatch, ({ status, reason, check }) => { if (status === "FAILED") reasons.push(reason ?? check); }); throw new MdocDeviceRequestNotSatisfiedError(deviceRequestMatch, `Device response does not satisfy the device request. ${reasons.join(". ")}`); } verificationSession.sessionTranscript = { ...sessionTranscript, origin: options.origin }; await this.updateState(agentContext, verificationSession, "ResponseVerified"); return { verificationSession, deviceResponse, deviceRequestMatch, origin: options.origin }; } catch (error) { verificationSession.errorMessage = error instanceof Error ? error.message : "Unknown error"; await this.updateState(agentContext, verificationSession, "Error"); throw error; } } /** * Resolve the trusted certificates for a single mdoc in a device response. * * Same resolution order as the other credential verification paths: the global * `getTrustedIssuersForVerification` callback first, then the deprecated * `getTrustedCertificatesForVerification` callback, and only if neither returns anything the * statically configured trusted certificates. Both callbacks receive the certificate chain of * the mdoc being verified, so trust can be decided per document. */ async getTrustedCertificatesForMdoc(agentContext, mdoc) { const x509ModuleConfig = agentContext.dependencyManager.resolve(X509ModuleConfig); const certificateChain = mdoc.issuerSignedCertificateChain.map((certificate) => X509Certificate.fromRawCertificate(certificate)); return (await TrustedIssuerContext.getTrustedIssuersForVerification(agentContext, { signer: { method: "x509", certificateChain }, verification: { type: "credential", credential: mdoc } }))?.trustedIssuers ?? await x509ModuleConfig.getTrustedCertificatesForVerification?.(agentContext, { certificateChain, verification: { type: "credential", credential: mdoc } }) ?? x509ModuleConfig.trustedCertificates ?? []; } /** * Assert the reader certificate chain of a single doc request is trusted. * * Same resolution order as the issuer trust in {@link MdocDcApiService.getTrustedCertificatesForMdoc}: * the certificates passed to `resolveRequest` first, then the global * `getTrustedIssuersForVerification` callback, then the deprecated * `getTrustedCertificatesForVerification` callback, and only if none of these return anything * the statically configured trusted certificates. * * As everywhere else x509 signers are verified, resolving nothing is not a reason to continue * without establishing trust: the reader certificate chain is signed by the reader itself, so * skipping the chain check would leave the reader identity self-asserted. A wallet that wants to * decide on the reader identity itself can return the leaf certificate from the callback, which * trusts the reader on the certificate it presented. */ async assertTrustedReaderCertificateChain(agentContext, options) { const x509ModuleConfig = agentContext.dependencyManager.resolve(X509ModuleConfig); let trustedCertificates = options.trustedReaderCertificates?.map((certificate) => typeof certificate === "string" ? certificate : certificate.toString("pem")); if (!trustedCertificates) { const trustedIssuers = await TrustedIssuerContext.getTrustedIssuersForVerification(agentContext, { signer: { method: "x509", certificateChain: options.certificateChain }, verification: options.verification }); const legacyTrustedCertificates = trustedIssuers ? void 0 : await x509ModuleConfig.getTrustedCertificatesForVerification?.(agentContext, { certificateChain: options.certificateChain, verification: options.verification }); trustedCertificates = trustedIssuers?.trustedIssuers.flatMap(({ issuance }) => issuance) ?? (legacyTrustedCertificates ? convertLegacyTrustedCertificates(legacyTrustedCertificates).flatMap(({ issuance }) => issuance) : void 0) ?? x509ModuleConfig.trustedCertificates; } if (!trustedCertificates) throw new MdocError("No trusted certificates found. Cannot verify reader authentication."); const onCheck = onCategoryCheck(defaultVerificationCallback, "READER_AUTH"); try { await X509Service.validateCertificateChain(agentContext, { certificateChain: options.certificateChain.map((certificate) => certificate.toString("pem")), trustedCertificates, verificationDate: options.now }); onCheck({ status: "PASSED", check: "Reader certificate chain must be trusted" }); } catch (error) { onCheck({ status: "FAILED", check: "Reader certificate chain must be trusted", reason: error instanceof Error ? error.message : "Unknown error" }); } } /** * Parse an incoming `org-iso-mdoc` request (wallet side) and match it against stored mdocs. * * `origin` must be the origin provided by the platform; Annex C C.5 requires the mdoc to abort * when no origin was received. */ async resolveRequest(agentContext, options) { const mdocContext = getMdocContext(agentContext); const parsedRequest = await IsoMdocDcApi.parseRequest({ request: options.request, origin: options.origin, disableReaderCertificateChainValidation: true, now: options.now }, mdocContext); const candidates = await this.getCandidates(agentContext, parsedRequest.docRequests.map((docRequest) => docRequest.docType), options.useMode ?? "NewOrFirst"); const deviceNamespaces = options.deviceNameSpaces ? nameSpacesRecordToDeviceNamespaces(options.deviceNameSpaces) : void 0; const holderMatch = Holder.matchDeviceRequest({ deviceRequest: parsedRequest.deviceRequest, credentials: candidates.map(({ mdoc }) => ({ issuerSigned: mdoc.issuerSigned, deviceNamespaces })), treatAmbiguousMultipleDocRequestsAsAlternatives: options.treatAmbiguousMultipleDocRequestsAsAlternatives }); const toValidCredential = ({ credentialIndex, ...credentialMatch }) => ({ ...credentialMatch, record: candidates[credentialIndex].record }); const toFailedCredential = ({ credentialIndex, ...credentialMatch }) => ({ ...credentialMatch, record: candidates[credentialIndex].record }); const docRequests = []; for (const [docRequestIndex, docRequest] of parsedRequest.docRequests.entries()) { const nameSpaces = Object.fromEntries(Array.from(docRequest.namespaces.entries()).map(([nameSpace, elements]) => [nameSpace, Object.fromEntries(elements.entries())])); const readerAuth = docRequest.hasReaderAuth ? { certificateChain: (docRequest.readerCertificateChain ?? []).map((certificate) => X509Certificate.fromRawCertificate(certificate)) } : void 0; if (readerAuth) await this.assertTrustedReaderCertificateChain(agentContext, { certificateChain: readerAuth.certificateChain, verification: { type: "mdocReaderAuth", exchange: { type: "dcApi", origin: parsedRequest.origin }, docType: docRequest.docType, nameSpaces }, trustedReaderCertificates: options.trustedReaderCertificates, now: options.now }); const docRequestMatch = holderMatch.docRequests[docRequestIndex]; const resolvedDocRequest = { docRequestIndex, docType: docRequest.docType, nameSpaces, readerAuth, failedCredentials: docRequestMatch.failedCredentials.map(toFailedCredential) }; docRequests.push(docRequestMatch.success ? { ...resolvedDocRequest, success: true, validCredentials: mapNonEmptyArray(docRequestMatch.validCredentials, toValidCredential) } : { ...resolvedDocRequest, success: false, validCredentials: [] }); } if (holderMatch.docRequestsAsAlternatives) return { origin: parsedRequest.origin, success: holderMatch.success, docRequestsAsAlternatives: true, docRequests, parsedRequest }; return holderMatch.success ? { origin: parsedRequest.origin, success: true, docRequestsAsAlternatives: false, docRequests: docRequests.filter((docRequest) => docRequest.success), parsedRequest } : { origin: parsedRequest.origin, success: false, docRequestsAsAlternatives: false, docRequests, parsedRequest }; } /** * Fetch the stored mdocs for the requested doctypes. * * Only the requested doctypes are fetched, as a wallet can hold many more mdocs than a request * covers. The issuer-signed CBOR of each record is decoded once, and matched against every doc * request from there. * * Records that cannot provide an instance for `useMode` are left out, as creating a response with * them would throw. */ async getCandidates(agentContext, docTypes, useMode) { const uniqueDocTypes = Array.from(new Set(docTypes)); if (uniqueDocTypes.length === 0) return []; return (await this.mdocRepository.findByQuery(agentContext, { $or: uniqueDocTypes.map((docType) => ({ docType })) })).filter((record) => canUseInstanceFromCredentialRecord({ credentialRecord: record, useMode })).map((record) => ({ record, mdoc: record.firstCredential })); } /** * Create the encrypted `org-iso-mdoc` response (wallet side). */ async createResponse(agentContext, options) { const mdocContext = getMdocContext(agentContext); const parsedRequest = options.resolvedRequest.parsedRequest; const documents = await Promise.all(options.credentials.map(async ({ docRequestIndex, record, useMode, elements, deviceNameSpaces }) => { const mdocRecord = record instanceof MdocRecord ? record : await this.mdocRepository.getById(agentContext, record); const { credentialInstance: mdoc } = await useInstanceFromCredentialRecord({ agentContext, credentialRecord: mdocRecord, useMode: useMode ?? "NewOrFirst" }); const deviceKeyJwk = mdocSigningJwk(mdoc.deviceKey); if (!deviceKeyJwk.kid) throw new MdocError("Unable to create device response. The mdoc device key has no key id."); return { docRequestIndex, issuerSigned: mdoc.issuerSigned, deviceKey: CoseKey.fromJwk(deviceKeyJwk), elements, deviceNamespaces: deviceNameSpaces ? nameSpacesRecordToDeviceNamespaces(deviceNameSpaces) : void 0 }; })); return await IsoMdocDcApi.createResponse({ parsedRequest, documents }, mdocContext); } async updateState(agentContext, verificationSession, newState) { const previousState = verificationSession.state; verificationSession.state = newState; await this.mdocVerificationSessionRepository.update(agentContext, verificationSession); this.emitStateChangedEvent(agentContext, verificationSession, previousState); } emitStateChangedEvent(agentContext, verificationSession, previousState) { this.eventEmitter.emit(agentContext, { type: "Mdoc.VerificationSessionStateChanged", payload: { verificationSession: verificationSession.clone(), previousState } }); } }; MdocDcApiService = __decorate([injectable(), __decorateMetadata("design:paramtypes", [ typeof MdocRepository === "undefined" ? Object : MdocRepository, typeof MdocVerificationSessionRepository === "undefined" ? Object : MdocVerificationSessionRepository, typeof EventEmitter === "undefined" ? Object : EventEmitter ])], MdocDcApiService); //#endregion export { MdocDcApiService }; //# sourceMappingURL=MdocDcApiService.mjs.map