@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
232 lines (231 loc) • 11.8 kB
JavaScript
import { TypedArrayEncoder } from "../../utils/TypedArrayEncoder.mjs";
import "../../utils/index.mjs";
import "../../agent/index.mjs";
import "../kms/index.mjs";
import "../vc/models/ClaimFormat.mjs";
import "../vc/index.mjs";
import { getMdocContext } from "../../crypto/contexts/mdocContext.mjs";
import { MdocError } from "./MdocError.mjs";
import { isMdocSupportedSignatureAlgorithm, mdocSupportedSignatureAlgorithms } from "./mdocSupportedAlgs.mjs";
import { assertMdocStatusListChainsMatchIssuanceChain, convertMdocTrustedCertificates, mdocTrustedCertificatesToRaw, nameSpacesRecordToDeviceNamespaces, nameSpacesRecordToMap } from "./mdocUtil.mjs";
import { convertDocumentRequest } from "./utils/convertDocumentRequest.mjs";
import { CoseKey, DeviceRequest, DeviceResponse, DocRequest, Holder, ItemsRequest, SessionTranscript, defaultVerificationCallback } from "@owf/mdoc";
import { convertDcqlQueryToDeviceRequest, convertPresentationDefinitionToDeviceRequest } from "@verifiables/request-converter";
//#region src/modules/mdoc/MdocDeviceResponse.ts
var MdocDeviceResponse = class MdocDeviceResponse {
constructor(deviceResponse) {
this.deviceResponse = deviceResponse;
}
/**
* claim format is convenience method added to all credential instances
*/
get claimFormat() {
return "mso_mdoc";
}
/**
* Encoded is convenience method added to all credential instances
*/
get encoded() {
return TypedArrayEncoder.toBase64Url(this.deviceResponse.encode());
}
/**
* To support a single DeviceResponse with multiple documents in OpenID4VP
*/
splitIntoSingleDocumentResponses() {
const deviceResponses = [];
if (!this.deviceResponse.documents || this.deviceResponse.documents.length === 0) throw new MdocError("mdoc device response does not contain any mdocs");
for (const document of this.deviceResponse.documents) deviceResponses.push(new MdocDeviceResponse(DeviceResponse.createSimple({
version: this.deviceResponse.version,
status: this.deviceResponse.status,
documentErrors: this.deviceResponse.documentErrors,
documents: [document]
})));
return deviceResponses;
}
static fromBase64Url(base64Url) {
const parsed = DeviceResponse.decode(TypedArrayEncoder.fromBase64Url(base64Url));
return new MdocDeviceResponse(parsed);
}
static async createDeviceResponseWithPresentationDefinition(agentContext, options) {
const { deviceRequest } = convertPresentationDefinitionToDeviceRequest(options.presentationDefinition);
const documentRequests = convertDocumentRequest(deviceRequest.docRequests);
return MdocDeviceResponse.createDeviceResponse(agentContext, {
...options,
documentRequests
});
}
/**
* @deprecated Credo does not use this method itself, and it only supports part of DCQL. Every claim
* of every `mso_mdoc` credential query is requested: `credential_sets`, `claim_sets` and `values`
* are ignored, credential queries with the same doctype are merged, nested claim paths are
* truncated, and a claim missing from the mdoc throws instead of being treated as optional. Use
* `DcqlService.createPresentation` to present mdocs for a DCQL query, or
* {@link MdocDeviceResponse.createDeviceResponse} with explicit `documentRequests`.
*/
static async createDeviceResponseWithDcqlQuery(agentContext, options) {
const { deviceRequest } = convertDcqlQueryToDeviceRequest(options.dcqlQuery);
const documentRequests = convertDocumentRequest(deviceRequest.docRequests);
return MdocDeviceResponse.createDeviceResponse(agentContext, {
...options,
documentRequests
});
}
static async limitDisclosureToInputDescriptor(_agentContext, { mdoc, ...options }) {
const inputDescriptor = MdocDeviceResponse.assertMdocInputDescriptor(options.inputDescriptor);
const docRequestMatch = MdocDeviceResponse.matchInputDescriptor(mdoc, inputDescriptor, { skipOptionalFields: false }) ?? MdocDeviceResponse.matchInputDescriptor(mdoc, inputDescriptor, { skipOptionalFields: true });
if (!docRequestMatch) throw new MdocError(`Mdoc with doctype ${mdoc.docType} does not satisfy the input descriptor`);
const disclosedPayloadAsRecord = {};
for (const claim of docRequestMatch.claims.validClaims) {
var _claim$namespace;
disclosedPayloadAsRecord[_claim$namespace = claim.namespace] ?? (disclosedPayloadAsRecord[_claim$namespace] = {});
disclosedPayloadAsRecord[claim.namespace][claim.disclosedElementIdentifier] = claim.elementValue;
}
return disclosedPayloadAsRecord;
}
static matchInputDescriptor(mdoc, inputDescriptor, { skipOptionalFields }) {
const { deviceRequest: { docRequests: [convertedDocumentRequest] } } = convertPresentationDefinitionToDeviceRequest({
id: "<UNUSED_CREDO_ID>",
input_descriptors: [inputDescriptor]
}, { skipOptionalFields });
const { docRequests } = Holder.matchDeviceRequest({
deviceRequest: DeviceRequest.create({
version: "1.0",
docRequests: [DocRequest.create({ itemsRequest: ItemsRequest.create({
docType: convertedDocumentRequest.itemsRequest.docType,
namespaces: convertedDocumentRequest.itemsRequest.nameSpaces
}) })]
}),
credentials: [mdoc.issuerSigned]
});
return docRequests[0].success ? docRequests[0].validCredentials[0] : void 0;
}
static assertMdocInputDescriptor(inputDescriptor) {
if (!inputDescriptor.format?.mso_mdoc) throw new MdocError(`Input descriptor must contain 'mso_mdoc' format property`);
if (!inputDescriptor.format.mso_mdoc.alg) throw new MdocError(`Input descriptor mso_mdoc must contain 'alg' property`);
if (!inputDescriptor.constraints?.limit_disclosure || inputDescriptor.constraints.limit_disclosure !== "required") throw new MdocError(`Input descriptor must contain 'limit_disclosure' constraints property which is set to required`);
if (!inputDescriptor.constraints?.fields?.every((field) => field.intent_to_retain !== void 0)) throw new MdocError(`Input descriptor must contain 'intent_to_retain' constraints property`);
return {
...inputDescriptor,
format: { mso_mdoc: inputDescriptor.format.mso_mdoc },
constraints: {
...inputDescriptor.constraints,
limit_disclosure: "required",
fields: (inputDescriptor.constraints.fields ?? []).map((field) => {
return {
...field,
intent_to_retain: field.intent_to_retain ?? false
};
})
}
};
}
static async createDeviceResponse(agentContext, options) {
const documents = [];
for (const document of options.mdocs) {
const deviceKeyJwk = document.deviceKey;
const deviceKeyJwkJson = deviceKeyJwk.toJson();
if (!deviceKeyJwkJson.alg) deviceKeyJwkJson.alg = MdocDeviceResponse.getAlgForDeviceKeyJwk(deviceKeyJwk);
if (!deviceKeyJwk.hasKeyId) deviceKeyJwk.keyId = deviceKeyJwk.legacyKeyId;
const deviceRequestForDocument = DeviceRequest.create({
version: "1.0",
docRequests: options.documentRequests.filter((request) => request.docType === document.docType).map((request) => DocRequest.create({ itemsRequest: ItemsRequest.create({
docType: request.docType,
namespaces: nameSpacesRecordToMap(request.nameSpaces)
}) }))
});
const mdocContext = getMdocContext(agentContext);
const deviceResponse = await DeviceResponse.createWithDeviceRequest({
deviceRequest: deviceRequestForDocument,
sessionTranscript: await MdocDeviceResponse.calculateSessionTranscriptBytes(mdocContext, options.sessionTranscriptOptions),
documents: [{
issuerSigned: document.issuerSigned,
docRequestIndex: 0,
deviceNamespaces: options.deviceNameSpaces ? nameSpacesRecordToDeviceNamespaces(options.deviceNameSpaces) : void 0,
signature: { signingKey: CoseKey.fromJwk(deviceKeyJwk.toJson()) }
}]
}, mdocContext);
if (!deviceResponse.documents) throw new MdocError("Device response does not contain any documents");
documents.push(deviceResponse.documents[0]);
}
return new MdocDeviceResponse(DeviceResponse.createSimple({
version: "1.0",
documents
}));
}
async verify(agentContext, options) {
const mdocContext = getMdocContext(agentContext);
defaultVerificationCallback({
status: this.deviceResponse.documents?.length ? "PASSED" : "FAILED",
check: "Device Response must include at least one document.",
category: "DOCUMENT_FORMAT"
});
const trustedCertificates = convertMdocTrustedCertificates(options.trustedCertificates ?? []);
const { documents: verificationResults } = await this.deviceResponse.verify({
trustedCertificates: mdocTrustedCertificatesToRaw(trustedCertificates),
disableCertificateChainValidation: false,
now: options.now,
skewSeconds: agentContext.config.validitySkewSeconds,
sessionTranscript: await MdocDeviceResponse.calculateSessionTranscriptBytes(mdocContext, options.sessionTranscriptOptions)
}, mdocContext).catch((error) => {
throw new MdocError(`Mdoc with doctype ${this.deviceResponse.documents?.[0].docType} is not valid`, { cause: error });
});
for (const { trustedIssuanceChain, trustedStatusListChain, trustedIdentifierListChain } of verificationResults) {
if (!trustedIssuanceChain) continue;
assertMdocStatusListChainsMatchIssuanceChain(trustedCertificates, {
trustedIssuanceChain,
trustedStatusListChain,
trustedIdentifierListChain
});
}
}
static async calculateSessionTranscriptBytes(mdocContext, options) {
if (options.type === "sessionTranscriptBytes") return options.sessionTranscriptBytes;
if (options.type === "openId4VpDraft18") return await SessionTranscript.forOid4VpDraft18(options, mdocContext);
if (options.type === "openId4Vp") {
const { encryptionJwk, verifierGeneratedNonce, ...rest } = options;
return await SessionTranscript.forOid4Vp({
...rest,
jwkThumbprint: encryptionJwk ? encryptionJwk.getJwkThumbprint() : void 0,
nonce: verifierGeneratedNonce
}, mdocContext);
}
if (options.type === "openId4VpDcApi") {
const { encryptionJwk, verifierGeneratedNonce, ...rest } = options;
return await SessionTranscript.forOid4VpDcApi({
...rest,
nonce: verifierGeneratedNonce,
jwkThumbprint: encryptionJwk ? encryptionJwk.getJwkThumbprint() : void 0
}, mdocContext);
}
if (options.type === "openId4VpDcApiDraft24") {
const { verifierGeneratedNonce, ...rest } = options;
return await SessionTranscript.forOid4VpDcApiDraft24({
...rest,
nonce: verifierGeneratedNonce
}, mdocContext);
}
throw new MdocError("Unsupported session transcript option");
}
static getAlgForDeviceKeyJwk(jwk) {
const signatureAlgorithm = jwk.supportedSignatureAlgorithms.find(isMdocSupportedSignatureAlgorithm);
if (!signatureAlgorithm) throw new MdocError(`Unable to create mdoc device response. No supported signature algorithm found to sign device response for jwk ${jwk.jwkTypeHumanDescription}. Key supports algs ${jwk.supportedSignatureAlgorithms.join(", ")}. mdoc supports algs ${mdocSupportedSignatureAlgorithms.join(", ")}`);
return signatureAlgorithm;
}
get issuerClaims() {
if (!this.deviceResponse.documents) return {};
return this.deviceResponse.documents.reduce((prev, document) => ({
...prev,
[document.docType]: Object.fromEntries(Array.from(document.issuerSigned.issuerNamespaces?.issuerNamespaces.entries() ?? []).map(([namespace, claim]) => [namespace, Object.fromEntries(claim.map((c) => [c.elementIdentifier, c.elementValue]))]))
}), {});
}
get deviceClaims() {
if (!this.deviceResponse.documents) return {};
return this.deviceResponse.documents.reduce((prev, document) => ({
...prev,
[document.docType]: Object.fromEntries(Array.from(document.deviceSigned.deviceNamespaces.deviceNamespaces.entries()).map(([namespace, claim]) => [namespace, Object.fromEntries(claim.deviceSignedItems)]))
}), {});
}
};
//#endregion
export { MdocDeviceResponse };
//# sourceMappingURL=MdocDeviceResponse.mjs.map