@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
JavaScript
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