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

219 lines (218 loc) • 9.24 kB
import { Hasher } from "../../crypto/hashes/Hasher.mjs"; import "../../crypto/index.mjs"; import { SdJwtVcError } from "./SdJwtVcError.mjs"; import { isObject } from "class-validator"; import { SD_DIGEST, SD_LIST_KEY, createHashMappingSync, decodeSdJwtSync, getClaimsSync, getSDAlgAndPayload, selectDisclosures } from "@sd-jwt/core"; //#region src/modules/sd-jwt-vc/disclosureFrame.ts /** * @deprecated use `buildPresentationFrameForPaths` instead */ function buildDisclosureFrameForPayload(input) { return Object.fromEntries(Object.entries(input).map(([key, value]) => { if (Array.isArray(value)) return [key, true]; if (isObject(value)) { if (Object.keys.length === 0) return [key, false]; return [key, buildDisclosureFrameForPayload(value)]; } return [key, true]; })); } /** * @deprecated use `applyDisclosuresForPaths` instead */ function applyDisclosuresForPayload(compactSdJwt, requestedPayload) { const decoded = decodeSdJwtSync(compactSdJwt, 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] = compactSdJwt.split("~"); return `${jwt}~${requiredDisclosures.length > 0 ? `${requiredDisclosures.map((d) => d.encoded).join("~")}~` : ""}`; } /** * The paths to the claims a DCQL claim set selects, from the paths of its claims queries and its output. * * A claims query path stands for the claim and everything below it, so it gives a single path however * much is below the claim. A `null` in the path selects the array elements that matched, which are the * ones the output holds: for an element that did not match, DCQL leaves `null` in the output of a claims * query, and `undefined` or nothing in the output of a claim set. * * The output of a claim set merges the output of its claims queries, so an element can be in it for * another claims query of the claim set. Its path is then selected by that claims query anyway. */ function getClaimPathsForDcqlClaimSet(claimsQueryPaths, output) { const getPaths = (queryPath, value, path) => { if (value === null || value === void 0) return []; if (queryPath.length === 0) return [path]; const [pathElement, ...rest] = queryPath; if (pathElement === null || typeof pathElement === "number") { if (!Array.isArray(value)) return []; if (pathElement !== null) return getPaths(rest, value[pathElement], [...path, pathElement]); return value.flatMap((item, index) => getPaths(rest, item, [...path, index])); } if (!isObject(value)) return []; return getPaths(rest, value[pathElement], [...path, pathElement]); }; return claimsQueryPaths.flatMap((queryPath) => getPaths(queryPath, output, [])); } /** * Builds the presentation frame that discloses the claims at the given paths of an SD-JWT, including * everything below them. * * Unlike {@link buildDisclosureFrameForPayload}, array elements are selected by their position, so an * element that is selectively disclosable on its own is disclosed when a path selects it, and only then. */ function buildPresentationFrameForPaths(compactSdJwt, paths) { const { payload, disclosuresByDigest } = decodeForPaths(compactSdJwt); return selectClaimsAtPaths(payload, disclosuresByDigest, paths).frame; } /** * Discloses the claims at the given paths of an SD-JWT, including everything below them. * * Besides the compact SD-JWT holding only the disclosures for those claims, returns what it discloses: * - `prettyClaims`, the claims as a verifier receives them. * - `disclosedPaths`, the paths to the claims it discloses. These are not only the given paths: a * claim that is not selectively disclosable is always disclosed. A claim that is disclosed with * everything below it has a single path, so a path only goes deeper where something below it is not * disclosed. And unlike in the pretty claims, where an element that is not disclosed is left out, * an array element has the position it has in the credential. */ function applyDisclosuresForPaths(compactSdJwt, paths) { const { signedPayload, payload, disclosures, disclosuresByDigest, hasher } = decodeForPaths(compactSdJwt); const { digests } = selectClaimsAtPaths(payload, disclosuresByDigest, paths); const selectedDisclosures = disclosures.filter((disclosure) => digests.has(disclosure.digestSync(hasher))); const [jwt] = compactSdJwt.split("~"); const disclosedPaths = getDisclosedClaimPaths(payload, disclosuresByDigest, digests); return { compact: `${jwt}~${selectedDisclosures.map((disclosure) => `${disclosure.encode()}~`).join("")}`, prettyClaims: getClaimsSync(signedPayload, selectedDisclosures, Hasher.hash), disclosedPaths }; } function decodeForPaths(compactSdJwt) { const decoded = decodeSdJwtSync(compactSdJwt, Hasher.hash); if (decoded.kbJwt) throw new SdJwtVcError("Cannot apply limit disclosure on an sd-jwt with key binding jwt"); const { _sd_alg: alg, payload } = getSDAlgAndPayload(decoded.jwt.payload); const hasher = { alg, hasher: Hasher.hash }; return { signedPayload: decoded.jwt.payload, payload, hasher, disclosures: decoded.disclosures, disclosuresByDigest: createHashMappingSync(decoded.disclosures, hasher) }; } /** * The claims directly below a value of a signed SD-JWT payload, or `undefined` for a value that is not * an object or array. A digest without a disclosure is a decoy, and not a claim. */ function getClaimsBelow(value, disclosuresByDigest) { if (Array.isArray(value)) { const claims = []; value.forEach((item, frameKey) => { const digest = isObject(item) ? item[SD_LIST_KEY] : void 0; if (digest === void 0) { claims.push({ key: claims.length, frameKey, value: item }); return; } const disclosure = disclosuresByDigest[digest]; if (disclosure) claims.push({ key: claims.length, frameKey, value: disclosure.value, digest }); }); return claims; } if (isObject(value)) { const { [SD_DIGEST]: digests, ...claims } = value; return [...Object.entries(claims).map(([key, claim]) => ({ key, frameKey: key, value: claim })), ...(digests ?? []).flatMap((digest) => { const disclosure = disclosuresByDigest[digest]; if (!disclosure?.key) return []; return [{ key: disclosure.key, frameKey: disclosure.key, value: disclosure.value, digest }]; })]; } } /** * The disclosures, and the presentation frame, for the claims at the given paths and everything below * them. A selectively disclosable claim on the way to a path is disclosed as well, as the claim below * it is not reachable otherwise, but only when there is a claim at the path. */ function selectClaimsAtPaths(payload, disclosuresByDigest, paths) { const toKey = (path) => JSON.stringify(path); const selectedPaths = new Set(paths.map(toKey)); const pathsOnTheWay = new Set(paths.flatMap((path) => path.slice(0, -1).map((_, index) => toKey(path.slice(0, index + 1))))); const digests = /* @__PURE__ */ new Set(); const select = (value, path, isSelected) => { const claims = getClaimsBelow(value, disclosuresByDigest); if (!claims) return void 0; const frame = {}; for (const claim of claims) { const claimPath = [...path, claim.key]; const isClaimSelected = isSelected || selectedPaths.has(toKey(claimPath)); if (!isClaimSelected && !pathsOnTheWay.has(toKey(claimPath))) continue; const claimFrame = select(claim.value, claimPath, isClaimSelected); if (!isClaimSelected && (!claimFrame || Object.keys(claimFrame).length === 0)) continue; if (claim.digest) digests.add(claim.digest); frame[String(claim.frameKey)] = claimFrame ?? true; } return frame; }; return { frame: select(payload, [], false) ?? {}, digests }; } /** * The shortest paths to the claims of a signed SD-JWT payload that the given disclosures disclose: a * claim that is disclosed with everything below it has a single path. An object or array none of whose * claims is disclosed has no path. */ function getDisclosedClaimPaths(payload, disclosuresByDigest, disclosedDigests) { const getDisclosed = (value, path) => { const claims = getClaimsBelow(value, disclosuresByDigest); if (!claims) return { paths: [path], isComplete: true }; const disclosed = claims.filter((claim) => !claim.digest || disclosedDigests.has(claim.digest)).map((claim) => getDisclosed(claim.value, [...path, claim.key])); const isComplete = disclosed.length === claims.length && disclosed.every((claim) => claim.isComplete); if (isComplete && path.length > 0) return { paths: [path], isComplete }; return { paths: disclosed.flatMap((claim) => claim.paths), isComplete }; }; return getDisclosed(payload, []).paths; } //#endregion export { applyDisclosuresForPaths, applyDisclosuresForPayload, buildDisclosureFrameForPayload, buildPresentationFrameForPaths, getClaimPathsForDcqlClaimSet }; //# sourceMappingURL=disclosureFrame.mjs.map