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

1 lines • 19.2 kB
{"version":3,"file":"disclosureFrame.mjs","names":[],"sources":["../../../src/modules/sd-jwt-vc/disclosureFrame.ts"],"sourcesContent":["import {\n createHashMappingSync,\n decodeSdJwtSync,\n getClaimsSync,\n getSDAlgAndPayload,\n SD_DIGEST,\n SD_LIST_KEY,\n selectDisclosures,\n} from '@sd-jwt/core'\nimport { isObject } from 'class-validator'\nimport { Hasher } from '../../crypto'\nimport type { JsonObject } from '../../types'\nimport { SdJwtVcError } from './SdJwtVcError'\n\ntype DisclosureFrame = {\n [key: string]: boolean | DisclosureFrame\n}\n\n/**\n * @deprecated use `buildPresentationFrameForPaths` instead\n */\nexport function buildDisclosureFrameForPayload(input: JsonObject): DisclosureFrame {\n return Object.fromEntries(\n Object.entries(input).map(([key, value]) => {\n // TODO: Array disclosure frames are not yet supported - treating entire array as disclosed\n if (Array.isArray(value)) {\n return [key, true]\n }\n if (isObject(value)) {\n if (Object.keys.length === 0) return [key, false]\n return [key, buildDisclosureFrameForPayload(value)]\n }\n return [key, true]\n })\n )\n}\n\n/**\n * @deprecated use `applyDisclosuresForPaths` instead\n */\nexport function applyDisclosuresForPayload(compactSdJwt: string, requestedPayload: JsonObject) {\n const decoded = decodeSdJwtSync(compactSdJwt, Hasher.hash)\n const presentationFrame = buildDisclosureFrameForPayload(requestedPayload) ?? {}\n\n if (decoded.kbJwt) {\n throw new SdJwtVcError('Cannot apply limit disclosure on an sd-jwt with key binding jwt')\n }\n\n const requiredDisclosures = selectDisclosures(\n decoded.jwt.payload,\n // Map to sd-jwt disclosure format\n decoded.disclosures.map((d) => ({\n digest: d.digestSync({ alg: 'sha-256', hasher: Hasher.hash }),\n encoded: d.encode(),\n key: d.key,\n salt: d.salt,\n value: d.value,\n })),\n presentationFrame as { [key: string]: boolean }\n )\n const [jwt] = compactSdJwt.split('~')\n const disclosuresString =\n requiredDisclosures.length > 0 ? `${requiredDisclosures.map((d) => d.encoded).join('~')}~` : ''\n const sdJwt = `${jwt}~${disclosuresString}`\n return sdJwt\n}\n\n/**\n * The path to a claim, with property names for objects and element positions for arrays.\n *\n * An element has the position it has in the claims of the SD-JWT, as DCQL gives it. Decoy digests in an\n * array are not elements, and have no position.\n *\n * A path stands for the claim and everything below it, as a claims path does in DCQL.\n */\nexport type ClaimPath = Array<string | number>\n\n/**\n * The paths to the claims a DCQL claim set selects, from the paths of its claims queries and its output.\n *\n * A claims query path stands for the claim and everything below it, so it gives a single path however\n * much is below the claim. A `null` in the path selects the array elements that matched, which are the\n * ones the output holds: for an element that did not match, DCQL leaves `null` in the output of a claims\n * query, and `undefined` or nothing in the output of a claim set.\n *\n * The output of a claim set merges the output of its claims queries, so an element can be in it for\n * another claims query of the claim set. Its path is then selected by that claims query anyway.\n */\nexport function getClaimPathsForDcqlClaimSet(\n claimsQueryPaths: Array<Array<string | number | null>>,\n output: unknown\n): ClaimPath[] {\n const getPaths = (queryPath: Array<string | number | null>, value: unknown, path: ClaimPath): ClaimPath[] => {\n // FIXME: logic should be updated once this PR is released:\n // https://github.com/openwallet-foundation-labs/identity-common-ts/pull/261\n // DCQL leaves `null`, `undefined` or nothing where no claim is selected\n if (value === null || value === undefined) return []\n // The end of the query path selects the claim with everything below it\n if (queryPath.length === 0) return [path]\n\n const [pathElement, ...rest] = queryPath\n\n // `null` goes into every element of an array, and a position into one\n if (pathElement === null || typeof pathElement === 'number') {\n if (!Array.isArray(value)) return []\n if (pathElement !== null) return getPaths(rest, value[pathElement], [...path, pathElement])\n return value.flatMap((item, index) => getPaths(rest, item, [...path, index]))\n }\n\n // A property name goes into the claim of an object\n if (!isObject(value)) return []\n return getPaths(rest, (value as Record<string, unknown>)[pathElement], [...path, pathElement])\n }\n\n return claimsQueryPaths.flatMap((queryPath) => getPaths(queryPath, output, []))\n}\n\n/**\n * Builds the presentation frame that discloses the claims at the given paths of an SD-JWT, including\n * everything below them.\n *\n * Unlike {@link buildDisclosureFrameForPayload}, array elements are selected by their position, so an\n * element that is selectively disclosable on its own is disclosed when a path selects it, and only then.\n */\nexport function buildPresentationFrameForPaths(compactSdJwt: string, paths: ClaimPath[]): DisclosureFrame {\n const { payload, disclosuresByDigest } = decodeForPaths(compactSdJwt)\n // Only the frame is needed, the digests are for applyDisclosuresForPaths\n return selectClaimsAtPaths(payload, disclosuresByDigest, paths).frame\n}\n\n/**\n * Discloses the claims at the given paths of an SD-JWT, including everything below them.\n *\n * Besides the compact SD-JWT holding only the disclosures for those claims, returns what it discloses:\n * - `prettyClaims`, the claims as a verifier receives them.\n * - `disclosedPaths`, the paths to the claims it discloses. These are not only the given paths: a\n * claim that is not selectively disclosable is always disclosed. A claim that is disclosed with\n * everything below it has a single path, so a path only goes deeper where something below it is not\n * disclosed. And unlike in the pretty claims, where an element that is not disclosed is left out,\n * an array element has the position it has in the credential.\n */\nexport function applyDisclosuresForPaths(\n compactSdJwt: string,\n paths: ClaimPath[]\n): { compact: string; prettyClaims: JsonObject; disclosedPaths: ClaimPath[] } {\n const { signedPayload, payload, disclosures, disclosuresByDigest, hasher } = decodeForPaths(compactSdJwt)\n // Find the digests of the disclosures needed for the paths\n const { digests } = selectClaimsAtPaths(payload, disclosuresByDigest, paths)\n\n // Keep only those disclosures, in the order of the SD-JWT\n const selectedDisclosures = disclosures.filter((disclosure) => digests.has(disclosure.digestSync(hasher)))\n const [jwt] = compactSdJwt.split('~')\n\n // Before getClaimsSync, which replaces the `_sd` in the values of nested disclosures with their claims\n const disclosedPaths = getDisclosedClaimPaths(payload, disclosuresByDigest, digests)\n\n // Rebuild the SD-JWT and its claims from the selected disclosures\n return {\n compact: `${jwt}~${selectedDisclosures.map((disclosure) => `${disclosure.encode()}~`).join('')}`,\n prettyClaims: getClaimsSync(signedPayload, selectedDisclosures, Hasher.hash) as JsonObject,\n disclosedPaths,\n }\n}\n\nfunction decodeForPaths(compactSdJwt: string) {\n const decoded = decodeSdJwtSync(compactSdJwt, Hasher.hash)\n\n if (decoded.kbJwt) {\n throw new SdJwtVcError('Cannot apply limit disclosure on an sd-jwt with key binding jwt')\n }\n\n // Split off `_sd_alg`, so it is not taken for a claim\n const { _sd_alg: alg, payload } = getSDAlgAndPayload(decoded.jwt.payload)\n const hasher = { alg, hasher: Hasher.hash }\n\n return {\n // With `_sd_alg`, which the functions of @sd-jwt/core read the hash algorithm from\n signedPayload: decoded.jwt.payload,\n payload,\n hasher,\n disclosures: decoded.disclosures,\n // To look up the disclosure for a digest in the payload\n disclosuresByDigest: createHashMappingSync(decoded.disclosures, hasher),\n }\n}\n\ntype DisclosuresByDigest = ReturnType<typeof createHashMappingSync>\n\n/**\n * A claim directly below a value of a signed SD-JWT payload.\n *\n * `key` is the property name or element position of the claim, and `frameKey` the one to use in a\n * presentation frame. These differ for an array element after a decoy digest: its position leaves out\n * the decoys, as in the claims of the SD-JWT, while @sd-jwt/core counts them in a presentation frame.\n */\ninterface ClaimBelow {\n key: string | number\n frameKey: string | number\n value: unknown\n // The digest of the disclosure, for a selectively disclosable claim\n digest?: string\n}\n\n/**\n * The claims directly below a value of a signed SD-JWT payload, or `undefined` for a value that is not\n * an object or array. A digest without a disclosure is a decoy, and not a claim.\n */\nfunction getClaimsBelow(value: unknown, disclosuresByDigest: DisclosuresByDigest): ClaimBelow[] | undefined {\n if (Array.isArray(value)) {\n const claims: ClaimBelow[] = []\n\n // `frameKey` counts every element, `key` (the length so far) only the claims\n value.forEach((item, frameKey) => {\n // An element is a digest when it is an object with `...`, which @sd-jwt/core checks is a string\n const digest = isObject(item) ? (item as Record<string, string | undefined>)[SD_LIST_KEY] : undefined\n // A plain element is a claim that is always disclosed\n if (digest === undefined) {\n claims.push({ key: claims.length, frameKey, value: item })\n return\n }\n\n // A digest is a selectively disclosable element, or a decoy when it has no disclosure\n const disclosure = disclosuresByDigest[digest]\n if (disclosure) claims.push({ key: claims.length, frameKey, value: disclosure.value, digest })\n })\n\n return claims\n }\n\n if (isObject(value)) {\n const { [SD_DIGEST]: digests, ...claims } = value as Record<string, unknown>\n\n return [\n // The properties next to `_sd` are always disclosed\n ...Object.entries(claims).map(([key, claim]) => ({ key, frameKey: key, value: claim })),\n // The digests in `_sd` are selectively disclosable properties, or decoys when they have no disclosure\n ...((digests as string[] | undefined) ?? []).flatMap((digest) => {\n const disclosure = disclosuresByDigest[digest]\n if (!disclosure?.key) return []\n\n return [{ key: disclosure.key, frameKey: disclosure.key, value: disclosure.value, digest }]\n }),\n ]\n }\n\n return undefined\n}\n\n/**\n * The disclosures, and the presentation frame, for the claims at the given paths and everything below\n * them. A selectively disclosable claim on the way to a path is disclosed as well, as the claim below\n * it is not reachable otherwise, but only when there is a claim at the path.\n */\nfunction selectClaimsAtPaths(payload: unknown, disclosuresByDigest: DisclosuresByDigest, paths: ClaimPath[]) {\n // A path as a string, to look it up in a set. A position stays a number, so it only selects an array element\n const toKey = (path: ClaimPath) => JSON.stringify(path)\n // The claims at the paths, which are disclosed with everything below them\n const selectedPaths = new Set(paths.map(toKey))\n // The claims above the paths, which are disclosed when there is a claim at a path below them\n const pathsOnTheWay = new Set(\n paths.flatMap((path) => path.slice(0, -1).map((_, index) => toKey(path.slice(0, index + 1))))\n )\n\n // Collects the digests of the disclosures to present, while building the frame\n const digests = new Set<string>()\n\n // The frame for the claims below a value, or `undefined` for a value that is not an object or array\n const select = (value: unknown, path: ClaimPath, isSelected: boolean): DisclosureFrame | undefined => {\n const claims = getClaimsBelow(value, disclosuresByDigest)\n // A value that is not an object or array has no frame, it is disclosed as `true`\n if (!claims) return undefined\n\n const frame: DisclosureFrame = {}\n\n for (const claim of claims) {\n const claimPath = [...path, claim.key]\n // A claim is selected when it is at a path, or below a selected claim\n const isClaimSelected = isSelected || selectedPaths.has(toKey(claimPath))\n // Skip claims that are neither selected nor on the way to a path\n if (!isClaimSelected && !pathsOnTheWay.has(toKey(claimPath))) continue\n\n // Go down first, to know whether a claim on the way leads to a claim at a path\n const claimFrame = select(claim.value, claimPath, isClaimSelected)\n // Nothing below a claim on the way is selected when there is no claim at the path\n if (!isClaimSelected && (!claimFrame || Object.keys(claimFrame).length === 0)) continue\n\n // Disclose the claim, and add it to the frame under the key @sd-jwt/core uses\n if (claim.digest) digests.add(claim.digest)\n frame[String(claim.frameKey)] = claimFrame ?? true\n }\n\n return frame\n }\n\n // An empty path selects the whole payload\n return { frame: select(payload, [], false) ?? {}, digests }\n}\n\n/**\n * The shortest paths to the claims of a signed SD-JWT payload that the given disclosures disclose: a\n * claim that is disclosed with everything below it has a single path. An object or array none of whose\n * claims is disclosed has no path.\n */\nfunction getDisclosedClaimPaths(\n payload: unknown,\n disclosuresByDigest: DisclosuresByDigest,\n disclosedDigests: Set<string>\n): ClaimPath[] {\n const getDisclosed = (value: unknown, path: ClaimPath): { paths: ClaimPath[]; isComplete: boolean } => {\n const claims = getClaimsBelow(value, disclosuresByDigest)\n // A value that is not an object or array is disclosed as a whole\n if (!claims) return { paths: [path], isComplete: true }\n\n // Go down into the claims that are disclosed: always, or by one of the digests\n const disclosed = claims\n .filter((claim) => !claim.digest || disclosedDigests.has(claim.digest))\n .map((claim) => getDisclosed(claim.value, [...path, claim.key]))\n\n // Complete when every claim below is disclosed, with everything below it\n const isComplete = disclosed.length === claims.length && disclosed.every((claim) => claim.isComplete)\n // A complete claim has a single path, except the payload, which has a path per claim\n if (isComplete && path.length > 0) return { paths: [path], isComplete }\n\n // Otherwise the paths of the disclosed claims below\n return { paths: disclosed.flatMap((claim) => claim.paths), isComplete }\n }\n\n return getDisclosed(payload, []).paths\n}\n"],"mappings":"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"}