@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
213 lines (209 loc) • 10.8 kB
JavaScript
import { TypedArrayEncoder } from "../../utils/TypedArrayEncoder.mjs";
import "../../utils/index.mjs";
import "../../agent/index.mjs";
import "../kms/index.mjs";
import { X509Certificate } from "../x509/X509Certificate.mjs";
import "../x509/index.mjs";
import { ClaimFormat } from "../vc/models/ClaimFormat.mjs";
import "../vc/index.mjs";
import { MdocError } from "./MdocError.mjs";
import { getMdocContext } from "./MdocContext.mjs";
import { isMdocSupportedSignatureAlgorithm, mdocSupportedSignatureAlgorithms } from "./mdocSupportedAlgs.mjs";
import { nameSpacesRecordToMap } from "./mdocUtil.mjs";
import { convertDocumentRequest } from "./utils/convertDocumentRequest.mjs";
import { CoseKey, DeviceNamespaces, DeviceRequest, DeviceResponse, DeviceSignedItems, DocRequest, ItemsRequest, SessionTranscript, defaultVerificationCallback, limitDisclosureToDeviceRequestNameSpaces } 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 ClaimFormat.MsoMdoc;
}
/**
* 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) {
return new MdocDeviceResponse(DeviceResponse.decode(TypedArrayEncoder.fromBase64Url(base64Url)));
}
static async createDeviceResponseWithPresentationDefinition(agentContext, options) {
const { deviceRequest } = convertPresentationDefinitionToDeviceRequest(options.presentationDefinition);
const documentRequests = convertDocumentRequest(deviceRequest.docRequests);
return MdocDeviceResponse.createDeviceResponse(agentContext, {
...options,
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);
let issuerNamespaces;
try {
const { deviceRequest: { docRequests: [convertedDocumentRequest] } } = convertPresentationDefinitionToDeviceRequest({
id: "<UNUSED_CREDO_ID>",
input_descriptors: [inputDescriptor]
});
const documentRequest = DocRequest.create({ itemsRequest: ItemsRequest.create({
docType: convertedDocumentRequest.itemsRequest.docType,
namespaces: convertedDocumentRequest.itemsRequest.nameSpaces
}) });
issuerNamespaces = limitDisclosureToDeviceRequestNameSpaces(mdoc.issuerSigned, documentRequest);
} catch (_error) {
const { deviceRequest: { docRequests: [convertedDocumentRequest] } } = convertPresentationDefinitionToDeviceRequest({
id: "<UNUSED_CREDO_ID>",
input_descriptors: [inputDescriptor]
}, { skipOptionalFields: true });
const documentRequest = DocRequest.create({ itemsRequest: ItemsRequest.create({
docType: convertedDocumentRequest.itemsRequest.docType,
namespaces: convertedDocumentRequest.itemsRequest.nameSpaces
}) });
issuerNamespaces = limitDisclosureToDeviceRequestNameSpaces(mdoc.issuerSigned, documentRequest);
}
return Object.fromEntries(Array.from(issuerNamespaces.issuerNamespaces.entries()).map(([namespace, issuerSignedItem]) => [namespace, Object.fromEntries(issuerSignedItem.map((isi) => [isi.elementIdentifier, isi.elementValue]))]));
}
static assertMdocInputDescriptor(inputDescriptor) {
if (!inputDescriptor.format || !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,
issuerSigned: [document.issuerSigned],
sessionTranscript: await MdocDeviceResponse.calculateSessionTranscriptBytes(mdocContext, options.sessionTranscriptOptions),
deviceNamespaces: options.deviceNameSpaces ? DeviceNamespaces.create({ deviceNamespaces: new Map(Object.entries(options.deviceNameSpaces).map(([namespace, namespaceValue]) => [namespace, DeviceSignedItems.create({ deviceSignedItems: new Map(Object.entries(namespaceValue)) })])) }) : 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"
});
await this.deviceResponse.verify({
trustedCertificates: options.trustedCertificates?.map((certificate) => X509Certificate.fromEncodedCertificate(certificate).rawCertificate) ?? [],
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 });
});
}
static async calculateSessionTranscriptBytes(mdocContext, options) {
if (options.type === "sesionTranscriptBytes") return options.sessionTranscriptBytes;
if (options.type === "openId4VpDraft18") return SessionTranscript.forOid4VpDraft18(options, mdocContext);
if (options.type === "openId4Vp") {
const { encryptionJwk, verifierGeneratedNonce, ...rest } = options;
return SessionTranscript.forOid4Vp({
...rest,
jwkThumbprint: encryptionJwk ? encryptionJwk.getJwkThumbprint() : void 0,
nonce: verifierGeneratedNonce
}, mdocContext);
}
if (options.type === "openId4VpDcApi") {
const { encryptionJwk, verifierGeneratedNonce, ...rest } = options;
return SessionTranscript.forOid4VpDcApi({
...rest,
nonce: verifierGeneratedNonce,
jwkThumbprint: encryptionJwk ? encryptionJwk.getJwkThumbprint() : void 0
}, mdocContext);
}
if (options.type === "openId4VpDcApiDraft24") {
const { verifierGeneratedNonce, ...rest } = options;
return 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