UNPKG

@sphereon/did-auth-siop

Version:

Self Issued OpenID V2 (SIOPv2) and OpenID 4 Verifiable Presentations (OID4VP)

288 lines 18.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.assertValidVerifiablePresentations = exports.putPresentationSubmissionInLocation = exports.createPresentationSubmission = exports.extractPresentationsFromVpToken = exports.extractPresentationsFromDcqlVpToken = exports.extractDcqlPresentationFromDcqlVpToken = exports.verifyPresentations = exports.extractNonceFromWrappedVerifiablePresentation = void 0; const pex_1 = require("@sphereon/pex"); const ssi_types_1 = require("@sphereon/ssi-types"); const dcql_1 = require("dcql"); const helpers_1 = require("../helpers"); const types_1 = require("../types"); const Dcql_1 = require("./Dcql"); const PresentationExchange_1 = require("./PresentationExchange"); const types_2 = require("./types"); const extractNonceFromWrappedVerifiablePresentation = (wrappedVp) => { var _a; // SD-JWT uses kb-jwt for the nonce if (ssi_types_1.CredentialMapper.isWrappedSdJwtVerifiablePresentation(wrappedVp)) { // SD-JWT uses kb-jwt for the nonce // TODO: replace this once `kbJwt.payload` is available on the decoded sd-jwt (pr in ssi-sdk) // If it doesn't end with ~, it contains a kbJwt if (!wrappedVp.presentation.compactSdJwtVc.endsWith('~')) { return wrappedVp.presentation.kbJwt.payload.nonce; } // No kb-jwt means no nonce (error will be handled later) return undefined; } if (wrappedVp.format === 'jwt_vp') { return wrappedVp.decoded.nonce; } // For LDP-VP a challenge is also fine if (wrappedVp.format === 'ldp_vp') { const w3cPresentation = wrappedVp.decoded; const proof = Array.isArray(w3cPresentation.proof) ? w3cPresentation.proof[0] : w3cPresentation.proof; return (_a = proof.nonce) !== null && _a !== void 0 ? _a : proof.challenge; } return undefined; }; exports.extractNonceFromWrappedVerifiablePresentation = extractNonceFromWrappedVerifiablePresentation; const verifyPresentations = (authorizationResponse, verifyOpts) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f; let idPayload; if (authorizationResponse.idToken) { idPayload = yield authorizationResponse.idToken.payload(); } let wrappedPresentations = []; const presentationDefinitions = verifyOpts.presentationDefinitions ? Array.isArray(verifyOpts.presentationDefinitions) ? verifyOpts.presentationDefinitions : [verifyOpts.presentationDefinitions] : []; let presentationSubmission; let dcqlPresentation; let dcqlQuery = (_a = verifyOpts.dcqlQuery) !== null && _a !== void 0 ? _a : (_b = authorizationResponse === null || authorizationResponse === void 0 ? void 0 : authorizationResponse.authorizationRequest) === null || _b === void 0 ? void 0 : _b.payload.dcql_query; if (dcqlQuery) { dcqlQuery = dcql_1.DcqlQuery.parse(dcqlQuery); dcqlPresentation = (0, exports.extractDcqlPresentationFromDcqlVpToken)(authorizationResponse.payload.vp_token, { hasher: verifyOpts.hasher }); wrappedPresentations = Object.values(dcqlPresentation); const verifiedPresentations = yield Promise.all(wrappedPresentations.map((presentation) => verifyOpts.verification.presentationVerificationCallback(presentation.original))); yield Dcql_1.Dcql.assertValidDcqlPresentationResult(authorizationResponse.payload.vp_token, dcqlQuery, { hasher: verifyOpts.hasher }); if (verifiedPresentations.some((verified) => !verified)) { const message = verifiedPresentations .map((verified) => verified.reason) .filter(Boolean) .join(', '); throw Error(`Failed to verify presentations. ${message}`); } } else { const presentations = authorizationResponse.payload.vp_token ? (0, exports.extractPresentationsFromVpToken)(authorizationResponse.payload.vp_token, { hasher: verifyOpts.hasher }) : []; wrappedPresentations = Array.isArray(presentations) ? presentations : [presentations]; // todo: Probably wise to check against request for the location of the submission_data presentationSubmission = (_d = (_c = idPayload === null || idPayload === void 0 ? void 0 : idPayload._vp_token) === null || _c === void 0 ? void 0 : _c.presentation_submission) !== null && _d !== void 0 ? _d : authorizationResponse.payload.presentation_submission; yield (0, exports.assertValidVerifiablePresentations)({ presentationDefinitions, presentations, verificationCallback: verifyOpts.verification.presentationVerificationCallback, opts: { presentationSubmission, restrictToFormats: verifyOpts.restrictToFormats, restrictToDIDMethods: verifyOpts.restrictToDIDMethods, hasher: verifyOpts.hasher, }, }); } const presentationsWithoutMdoc = wrappedPresentations.filter((p) => p.format !== 'mso_mdoc'); const nonces = new Set(presentationsWithoutMdoc.map(exports.extractNonceFromWrappedVerifiablePresentation)); if (presentationsWithoutMdoc.length > 0 && nonces.size !== 1) { throw Error(`${nonces.size} nonce values found for ${presentationsWithoutMdoc.length}. Should be 1`); } // Nonce may be undefined in case there's only mdoc presentations (verified differently) const nonce = Array.from(nonces)[0]; if (presentationsWithoutMdoc.length > 0 && typeof nonce !== 'string') { throw new Error('Expected all presentations to contain a nonce value'); } const revocationVerification = ((_e = verifyOpts.verification) === null || _e === void 0 ? void 0 : _e.revocationOpts) ? verifyOpts.verification.revocationOpts.revocationVerification : types_1.RevocationVerification.IF_PRESENT; if (revocationVerification !== types_1.RevocationVerification.NEVER) { if (!((_f = verifyOpts.verification.revocationOpts) === null || _f === void 0 ? void 0 : _f.revocationVerificationCallback)) { throw Error(`Please provide a revocation callback as revocation checking of credentials and presentations is not disabled`); } for (const vp of wrappedPresentations) { yield (0, helpers_1.verifyRevocation)(vp, verifyOpts.verification.revocationOpts.revocationVerificationCallback, revocationVerification); } } if (presentationDefinitions) { return { presentationExchange: { nonce, presentations: wrappedPresentations, presentationDefinitions, submissionData: presentationSubmission } }; } else { return { dcql: { nonce, presentation: dcqlPresentation, dcqlQuery } }; } }); exports.verifyPresentations = verifyPresentations; const extractDcqlPresentationFromDcqlVpToken = (vpToken, opts) => { const dcqlPresentation = Object.fromEntries(Object.entries(dcql_1.DcqlPresentation.parse(vpToken)).map(([credentialQueryId, vp]) => [ credentialQueryId, ssi_types_1.CredentialMapper.toWrappedVerifiablePresentation(vp, { hasher: opts.hasher }), ])); return dcqlPresentation; }; exports.extractDcqlPresentationFromDcqlVpToken = extractDcqlPresentationFromDcqlVpToken; const extractPresentationsFromDcqlVpToken = (vpToken, opts) => { return Object.values((0, exports.extractDcqlPresentationFromDcqlVpToken)(vpToken, opts)); }; exports.extractPresentationsFromDcqlVpToken = extractPresentationsFromDcqlVpToken; const extractPresentationsFromVpToken = (vpToken, opts) => { const tokens = Array.isArray(vpToken) ? vpToken : [vpToken]; const wrappedTokens = tokens.map((vp) => ssi_types_1.CredentialMapper.toWrappedVerifiablePresentation(vp, { hasher: opts === null || opts === void 0 ? void 0 : opts.hasher })); return tokens.length === 1 ? wrappedTokens[0] : wrappedTokens; }; exports.extractPresentationsFromVpToken = extractPresentationsFromVpToken; const createPresentationSubmission = (verifiablePresentations, opts) => __awaiter(void 0, void 0, void 0, function* () { var _g, _h; let submission_data; for (const verifiablePresentation of verifiablePresentations) { const wrappedPresentation = ssi_types_1.CredentialMapper.toWrappedVerifiablePresentation(verifiablePresentation); let submission = ssi_types_1.CredentialMapper.isWrappedW3CVerifiablePresentation(wrappedPresentation) && ((_h = (_g = wrappedPresentation.presentation.presentation_submission) !== null && _g !== void 0 ? _g : wrappedPresentation.decoded.presentation_submission) !== null && _h !== void 0 ? _h : (typeof wrappedPresentation.original !== 'string' && wrappedPresentation.original.presentation_submission)); if (typeof submission === 'string') { submission = JSON.parse(submission); } if (!submission && (opts === null || opts === void 0 ? void 0 : opts.presentationDefinitions) && !ssi_types_1.CredentialMapper.isWrappedMdocPresentation(wrappedPresentation)) { console.log(`No submission_data in VPs and not provided. Will try to deduce, but it is better to create the submission data beforehand`); for (const definitionOpt of opts.presentationDefinitions) { const definition = 'definition' in definitionOpt ? definitionOpt.definition : definitionOpt; const result = new pex_1.PEX().evaluatePresentation(definition, wrappedPresentation.original, { generatePresentationSubmission: true, presentationSubmissionLocation: pex_1.PresentationSubmissionLocation.EXTERNAL, }); if (result.areRequiredCredentialsPresent) { submission = result.value; break; } } } if (!submission) { throw Error('Verifiable Presentation has no submission_data, it has not been provided separately, and could also not be deduced'); } // let's merge all submission data into one object if (!submission_data) { submission_data = submission; } else { // We are pushing multiple descriptors into one submission_data, as it seems this is something which is assumed in OpenID4VP, but not supported in Presentation Exchange (a single VP always has a single submission_data) Array.isArray(submission_data.descriptor_map) ? submission_data.descriptor_map.push(...submission.descriptor_map) : (submission_data.descriptor_map = [...submission.descriptor_map]); } } if (typeof submission_data === 'string') { submission_data = JSON.parse(submission_data); } return submission_data; }); exports.createPresentationSubmission = createPresentationSubmission; const putPresentationSubmissionInLocation = (authorizationRequest, responsePayload, resOpts, idTokenPayload) => __awaiter(void 0, void 0, void 0, function* () { var _j, _k, _l, _m, _o, _p; const version = yield authorizationRequest.getSupportedVersion(); const idTokenType = yield authorizationRequest.containsResponseType(types_1.ResponseType.ID_TOKEN); const authResponseType = yield authorizationRequest.containsResponseType(types_1.ResponseType.VP_TOKEN); // const requestPayload = await authorizationRequest.mergedPayloads(); if (!resOpts.presentationExchange) { return; } else if (resOpts.presentationExchange.verifiablePresentations.length === 0) { throw Error('Presentation Exchange options set, but no verifiable presentations provided'); } if (!resOpts.presentationExchange.presentationSubmission && (!resOpts.presentationExchange.verifiablePresentations || resOpts.presentationExchange.verifiablePresentations.length === 0)) { throw Error(`Either a presentationSubmission or verifiable presentations are needed at this point`); } const submissionData = (_j = resOpts.presentationExchange.presentationSubmission) !== null && _j !== void 0 ? _j : (yield (0, exports.createPresentationSubmission)(resOpts.presentationExchange.verifiablePresentations, { presentationDefinitions: yield authorizationRequest.getPresentationDefinitions(), })); const location = (_l = (_k = resOpts.presentationExchange) === null || _k === void 0 ? void 0 : _k.vpTokenLocation) !== null && _l !== void 0 ? _l : (idTokenType && version < types_1.SupportedVersion.SIOPv2_D11 ? types_2.VPTokenLocation.ID_TOKEN : types_2.VPTokenLocation.AUTHORIZATION_RESPONSE); switch (location) { case types_2.VPTokenLocation.TOKEN_RESPONSE: { throw Error('Token response for VP token is not supported yet'); } case types_2.VPTokenLocation.ID_TOKEN: { if (!idTokenPayload) { throw Error('Cannot place submission data _vp_token in id token if no id token is present'); } else if (version >= types_1.SupportedVersion.SIOPv2_D11) { throw Error(`This version of the OpenID4VP spec does not allow to store the vp submission data in the ID token`); } else if (!idTokenType) { throw Error(`Cannot place vp token in ID token as the RP didn't provide an "openid" scope in the request`); } if ((_m = idTokenPayload._vp_token) === null || _m === void 0 ? void 0 : _m.presentation_submission) { if (submissionData !== idTokenPayload._vp_token.presentation_submission) { throw Error('Different submission data was provided as an option, but exising submission data was already present in the id token'); } } else { if (!idTokenPayload._vp_token) { idTokenPayload._vp_token = { presentation_submission: submissionData }; } else { idTokenPayload._vp_token.presentation_submission = submissionData; } } break; } case types_2.VPTokenLocation.AUTHORIZATION_RESPONSE: { if (!authResponseType) { throw Error('Cannot place vp token in Authorization Response as there is no vp_token scope in the auth request'); } if (responsePayload.presentation_submission) { if (submissionData !== responsePayload.presentation_submission) { throw Error('Different submission data was provided as an option, but exising submission data was already present in the authorization response'); } } else { responsePayload.presentation_submission = submissionData; } } } responsePayload.vp_token = ((_o = resOpts.presentationExchange) === null || _o === void 0 ? void 0 : _o.verifiablePresentations.length) === 1 ? resOpts.presentationExchange.verifiablePresentations[0] : (_p = resOpts.presentationExchange) === null || _p === void 0 ? void 0 : _p.verifiablePresentations; }); exports.putPresentationSubmissionInLocation = putPresentationSubmissionInLocation; const assertValidVerifiablePresentations = (args) => __awaiter(void 0, void 0, void 0, function* () { const { presentations } = args; if (!presentations || (Array.isArray(presentations) && presentations.length === 0)) { return Promise.reject(Error('missing presentation(s)')); } // Handle mdocs, keep them out of pex let presentationsArray = Array.isArray(presentations) ? presentations : [presentations]; if (presentationsArray.every((p) => p.format === 'mso_mdoc')) { return; } presentationsArray = presentationsArray.filter((p) => p.format !== 'mso_mdoc'); if ((!args.presentationDefinitions || args.presentationDefinitions.filter((a) => a.definition).length === 0) && (!presentationsArray || (Array.isArray(presentationsArray) && presentationsArray.filter((vp) => vp.presentation).length === 0))) { return; } PresentationExchange_1.PresentationExchange.assertValidPresentationDefinitionWithLocations(args.presentationDefinitions); if (args.presentationDefinitions && args.presentationDefinitions.length && (!presentationsArray || (Array.isArray(presentationsArray) && presentationsArray.length === 0))) { return Promise.reject(Error(types_1.SIOPErrors.AUTH_REQUEST_EXPECTS_VP)); } else if ((!args.presentationDefinitions || args.presentationDefinitions.length === 0) && presentationsArray && ((Array.isArray(presentationsArray) && presentationsArray.length > 0) || !Array.isArray(presentationsArray))) { return Promise.reject(Error(types_1.SIOPErrors.AUTH_REQUEST_DOESNT_EXPECT_VP)); } else if (args.presentationDefinitions && !args.opts.presentationSubmission) { return Promise.reject(Error(`No presentation submission present. Please use presentationSubmission opt argument!`)); } else if (args.presentationDefinitions && presentationsArray) { yield PresentationExchange_1.PresentationExchange.validatePresentationsAgainstDefinitions(args.presentationDefinitions, args.presentations, args.verificationCallback, args.opts); } }); exports.assertValidVerifiablePresentations = assertValidVerifiablePresentations; //# sourceMappingURL=OpenID4VP.js.map