From bcb401eb86fe108f1e527be86f35165721fe63de Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Sun, 19 Jul 2026 22:42:16 +0100 Subject: [PATCH 1/9] Fix build warning --- packages/credential-sdk/src/vc/status-list2021-credential.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/credential-sdk/src/vc/status-list2021-credential.js b/packages/credential-sdk/src/vc/status-list2021-credential.js index 207cf369c..8b271404a 100644 --- a/packages/credential-sdk/src/vc/status-list2021-credential.js +++ b/packages/credential-sdk/src/vc/status-list2021-credential.js @@ -8,7 +8,6 @@ import { gzip, ungzip } from 'pako'; import { u8aToU8a } from '../utils/types/bytes'; import VerifiableCredential from './verifiable-credential'; import { ensureStatusListId } from '../utils'; -import { KeyDoc } from "./helpers"; // eslint-disable-line /** * Status list 2021 verifiable credential as per https://www.w3.org/TR/vc-status-list/#statuslist2021credential. @@ -44,7 +43,7 @@ export default class StatusList2021Credential extends VerifiableCredential { * To allow unrevoking indices in the future, use `statusPurpose` = `suspension`. * The proof will be generated immediately using supplied `keyDoc`. * - * @param {KeyDoc} keyDoc + * @param {object} keyDoc - key document used to sign the credential (see `getKeyDoc`) * @param {string} id - on-chain hex identifier for the `StatusList2021Credential`. * @param {object} [params={}] * @param {'revocation'|'suspension'} [params.statusPurpose=revocation] - `statusPurpose` of the `StatusList2021Credential`. @@ -79,7 +78,7 @@ export default class StatusList2021Credential extends VerifiableCredential { * the underlying value won't be modified. * Throws an error if the underlying status list can't be decoded or any of the supplied indices is out of range. * - * @param {KeyDoc} keyDoc + * @param {object} keyDoc - key document used to re-sign the credential (see `getKeyDoc`) * @param {object} [update={}] * @param {Iterable} update.revokeIndices - indices to be revoked or suspended * @param {Iterable} update.unsuspendIndices - indices to be unsuspended From 7b846034e6f8bff19734679050e6e504351b8a75 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Sun, 19 Jul 2026 23:18:00 +0100 Subject: [PATCH 2/9] SD-JWT verification crypto-utils --- examples/CHANGELOG.md | 7 + examples/package.json | 4 +- packages/ap2/CHANGELOG.md | 7 + packages/ap2/package.json | 4 +- packages/credential-sdk/CHANGELOG.md | 8 + packages/credential-sdk/package.json | 4 +- packages/crypto-utils/CHANGELOG.md | 6 + packages/crypto-utils/jest.config.js | 2 +- packages/crypto-utils/package.json | 13 +- packages/crypto-utils/src/index.js | 1 + packages/crypto-utils/src/key-utils.js | 86 ++++ packages/crypto-utils/src/vc/jwk.js | 84 +++- packages/crypto-utils/src/vc/jws.js | 71 ++- packages/crypto-utils/src/vc/sd-jwt.js | 327 ++++++++++++- .../crypto-utils/tests/crypto-utils.test.js | 47 -- packages/crypto-utils/tests/sd-jwt.test.js | 458 ++++++++++++++++++ scripts/bench/CHANGELOG.md | 7 + scripts/bench/package.json | 4 +- yarn.lock | 70 +++ 19 files changed, 1123 insertions(+), 87 deletions(-) create mode 100644 packages/crypto-utils/src/key-utils.js create mode 100644 packages/crypto-utils/tests/sd-jwt.test.js diff --git a/examples/CHANGELOG.md b/examples/CHANGELOG.md index 4222697cf..d9765fa7d 100644 --- a/examples/CHANGELOG.md +++ b/examples/CHANGELOG.md @@ -1,5 +1,12 @@ # @docknetwork/sdk-examples +## 0.22.9 + +### Patch Changes + +- Updated dependencies + - @docknetwork/credential-sdk@0.56.2 + ## 0.22.8 ### Patch Changes diff --git a/examples/package.json b/examples/package.json index 03e4841de..babbb9b73 100644 --- a/examples/package.json +++ b/examples/package.json @@ -2,7 +2,7 @@ "name": "@docknetwork/sdk-examples", "private": true, "type": "module", - "version": "0.22.8", + "version": "0.22.9", "scripts": { "bbs-dock-example": "babel-node ./src/bbs-dock.js", "claim-deduction-example": "babel-node ./src/claim-deduction.js", @@ -18,7 +18,7 @@ "lint": "eslint \"src/**/*.js\"" }, "dependencies": { - "@docknetwork/credential-sdk": "0.56.1", + "@docknetwork/credential-sdk": "0.56.2", "@docknetwork/cheqd-blockchain-api": "6.0.1", "@docknetwork/cheqd-blockchain-modules": "6.0.1" }, diff --git a/packages/ap2/CHANGELOG.md b/packages/ap2/CHANGELOG.md index 2de0c18d0..6c01ea93a 100644 --- a/packages/ap2/CHANGELOG.md +++ b/packages/ap2/CHANGELOG.md @@ -1,5 +1,12 @@ # @docknetwork/ap2 +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @docknetwork/crypto-utils@0.2.2 + ## 0.2.1 ### Patch Changes diff --git a/packages/ap2/package.json b/packages/ap2/package.json index d974d516f..7f0054658 100644 --- a/packages/ap2/package.json +++ b/packages/ap2/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/ap2", - "version": "0.2.1", + "version": "0.2.2", "description": "Basic AP2 support", "license": "MIT", "type": "module", @@ -34,7 +34,7 @@ "node": ">=22.0.0" }, "dependencies": { - "@docknetwork/crypto-utils": "0.2.1", + "@docknetwork/crypto-utils": "0.2.2", "ajv": "^8.20.0" }, "devDependencies": { diff --git a/packages/credential-sdk/CHANGELOG.md b/packages/credential-sdk/CHANGELOG.md index 92390dff9..faab18264 100644 --- a/packages/credential-sdk/CHANGELOG.md +++ b/packages/credential-sdk/CHANGELOG.md @@ -1,5 +1,13 @@ # @docknetwork/credential-sdk +## 0.56.2 + +### Patch Changes + +- SD-JWT verification +- Updated dependencies + - @docknetwork/crypto-utils@0.2.2 + ## 0.56.1 ### Patch Changes diff --git a/packages/credential-sdk/package.json b/packages/credential-sdk/package.json index 8296923af..633abccf1 100644 --- a/packages/credential-sdk/package.json +++ b/packages/credential-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/credential-sdk", - "version": "0.56.1", + "version": "0.56.2", "license": "MIT", "type": "module", "files": [ @@ -43,7 +43,7 @@ "dependencies": { "@astronautlabs/jsonpath": "^1.1.2", "@digitalcredentials/vc-status-list": "^9.0.0", - "@docknetwork/crypto-utils": "0.2.1", + "@docknetwork/crypto-utils": "0.2.2", "@docknetwork/crypto-wasm-ts": "^0.63.0", "@docknetwork/vc-delegation-engine": "1.0.4", "@juanelas/base64": "^1.1.5", diff --git a/packages/crypto-utils/CHANGELOG.md b/packages/crypto-utils/CHANGELOG.md index 187400ab4..db625fcfa 100644 --- a/packages/crypto-utils/CHANGELOG.md +++ b/packages/crypto-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @docknetwork/crypto-utils +## 0.2.2 + +### Patch Changes + +- SD-JWT verification + ## 0.2.1 ### Patch Changes diff --git a/packages/crypto-utils/jest.config.js b/packages/crypto-utils/jest.config.js index 419b83e8b..46dd25e9f 100644 --- a/packages/crypto-utils/jest.config.js +++ b/packages/crypto-utils/jest.config.js @@ -6,7 +6,7 @@ export default { '^.+\\.(ts|js)$': ['babel-jest', { rootMode: 'upward' }], }, transformIgnorePatterns: [ - '/node_modules/(?!@stablelib|uuid)', + '/node_modules/(?!@stablelib|uuid|@sd-jwt|@ar-agents/ap2|jose)', ], verbose: true, }; diff --git a/packages/crypto-utils/package.json b/packages/crypto-utils/package.json index 97c3c66d8..e37ce8f8f 100644 --- a/packages/crypto-utils/package.json +++ b/packages/crypto-utils/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/crypto-utils", - "version": "0.2.1", + "version": "0.2.2", "description": "Lightweight Dock keypairs, crypto value types, byte utilities, and JWS helpers", "license": "MIT", "type": "module", @@ -16,6 +16,11 @@ "require": "./dist/cjs/index.cjs", "default": "./dist/esm/index.js" }, + "./key-utils": { + "import": "./dist/esm/key-utils.js", + "require": "./dist/cjs/key-utils.cjs", + "default": "./dist/esm/key-utils.js" + }, "./keypairs": { "import": "./dist/esm/keypairs/index.js", "require": "./dist/cjs/keypairs/index.cjs", @@ -53,12 +58,15 @@ "node": ">=22.0.0" }, "dependencies": { + "@sd-jwt/crypto-nodejs": "^0.9.2", + "@sd-jwt/sd-jwt-vc": "^0.9.2", "@stablelib/ed25519": "^1.0.0", "base64url": "^3.0.1", "elliptic": "6.6.1", "js-sha256": "^0.11.0" }, "devDependencies": { + "@ar-agents/ap2": "^0.3.1", "@babel/eslint-parser": "^7.25.9", "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", @@ -67,7 +75,8 @@ "eslint": "^8.0.0", "jest": "^29.7.0", "rollup": "^4.28.0", - "rollup-plugin-multi-input": "^1.4.1" + "rollup-plugin-multi-input": "^1.4.1", + "zod": "^4.4.3" }, "scripts": { "build": "rollup -c", diff --git a/packages/crypto-utils/src/index.js b/packages/crypto-utils/src/index.js index bd20b12b6..c4ea8ae46 100644 --- a/packages/crypto-utils/src/index.js +++ b/packages/crypto-utils/src/index.js @@ -1,3 +1,4 @@ +export * from './key-utils'; export * from './keypairs'; export * from './types'; export * from './utils'; diff --git a/packages/crypto-utils/src/key-utils.js b/packages/crypto-utils/src/key-utils.js new file mode 100644 index 000000000..b9d4549e1 --- /dev/null +++ b/packages/crypto-utils/src/key-utils.js @@ -0,0 +1,86 @@ +import { jwkToPublicKey } from './vc/jwk'; +import { verKeyType } from './vc/jws'; + +/** + * Wraps a value in an array when it is a single non-null entry. + * + * @param {*} value + * @returns {Array} + */ +export function potentialToArray(value) { + if (value == null) { + return []; + } + return Array.isArray(value) ? value : [value]; +} + +/** + * Returns true when a document already looks like a verification key document. + * + * @param {*} document + * @returns {boolean} + */ +export function isKeyDocument(document) { + return Boolean( + document + && typeof document === 'object' + && ( + document.publicKeyJwk + || document.publicKeyBase58 + || document.publicKeyMultibase + || (document.publicKey && !Array.isArray(document.publicKey)) + || typeof document.verifier === 'function' + || verKeyType(document) != null + ), + ); +} + +/** + * Selects a key document from a DID document by verification-method id. + * If `didDocument` is already a key document, it is returned as-is. + * + * @param {object} didDocument + * @param {string} didUrl + * @returns {object|null} + */ +export function getKeyFromDIDDocument(didDocument, didUrl) { + if (isKeyDocument(didDocument)) { + return didDocument; + } + + const possibleKeys = [ + ...potentialToArray(didDocument?.verificationMethod), + ...potentialToArray(didDocument?.keyAgreement), + ...potentialToArray(didDocument?.publicKey), + ]; + + return possibleKeys.find((key) => key?.id === didUrl) ?? null; +} + +/** + * Extracts a usable verification key from a key document for a JWT algorithm. + * Returns LD-style keys with `.verifier()` unchanged; otherwise Dock public-key + * material suitable for `verifyJwtSignature`. + * + * @param {object|null|undefined} keyDocument + * @param {string} algorithm + * @returns {*|null} + */ +export function publicKeyFromKeyDocument(keyDocument, algorithm) { + if (keyDocument == null) { + return null; + } + if (typeof keyDocument.verifier === 'function') { + return keyDocument; + } + if (verKeyType(keyDocument) != null) { + return keyDocument; + } + if (keyDocument.publicKeyJwk) { + return jwkToPublicKey(keyDocument.publicKeyJwk, algorithm); + } + if (keyDocument.publicKey && !Array.isArray(keyDocument.publicKey)) { + return keyDocument.publicKey; + } + throw new Error('Resolved key document does not contain a usable public key'); +} diff --git a/packages/crypto-utils/src/vc/jwk.js b/packages/crypto-utils/src/vc/jwk.js index ca6c38372..2c3060fda 100644 --- a/packages/crypto-utils/src/vc/jwk.js +++ b/packages/crypto-utils/src/vc/jwk.js @@ -1,8 +1,12 @@ import { ECDH } from 'node:crypto'; +import elliptic from 'elliptic'; + import { valueBytes } from '../utils'; -const CURVE = 'prime256v1'; +const P256_CURVE = 'prime256v1'; +const EC = elliptic.ec; +const secp256k1Curve = new EC('secp256k1'); /** * Converts a compressed or uncompressed P-256 public key to a public JWK. @@ -13,7 +17,7 @@ const CURVE = 'prime256v1'; export function secp256r1PublicKeyToJwk(publicKey) { const uncompressed = ECDH.convertKey( Buffer.from(valueBytes(publicKey)), - CURVE, + P256_CURVE, undefined, undefined, 'uncompressed', @@ -53,9 +57,83 @@ export function jwkToSecp256r1PublicKey(jwk) { return new Uint8Array(ECDH.convertKey( Buffer.concat([Buffer.from([4]), x, y]), - CURVE, + P256_CURVE, undefined, undefined, 'compressed', )); } + +/** + * Converts a public Ed25519 JWK to raw public-key bytes. + * + * @param {{kty: string, crv: string, x: string}} jwk + * @returns {Uint8Array} + */ +export function jwkToEd25519PublicKey(jwk) { + if ( + jwk == null + || typeof jwk !== 'object' + || jwk.kty !== 'OKP' + || jwk.crv !== 'Ed25519' + || typeof jwk.x !== 'string' + ) { + throw new TypeError('Expected a public Ed25519 OKP JWK'); + } + + const publicKey = new Uint8Array(Buffer.from(jwk.x, 'base64url')); + if (publicKey.length !== 32) { + throw new Error('Ed25519 JWK x must be 32 bytes'); + } + return publicKey; +} + +/** + * Converts a public secp256k1 JWK to compressed SEC1 public-key bytes. + * + * @param {{kty: string, crv: string, x: string, y: string}} jwk + * @returns {Uint8Array} + */ +export function jwkToSecp256k1PublicKey(jwk) { + if ( + jwk == null + || typeof jwk !== 'object' + || jwk.kty !== 'EC' + || jwk.crv !== 'secp256k1' + || typeof jwk.x !== 'string' + || typeof jwk.y !== 'string' + ) { + throw new TypeError('Expected a public secp256k1 EC JWK'); + } + + const x = Buffer.from(jwk.x, 'base64url'); + const y = Buffer.from(jwk.y, 'base64url'); + if (x.length !== 32 || y.length !== 32) { + throw new Error('secp256k1 JWK coordinates must be 32 bytes'); + } + + const point = secp256k1Curve.keyFromPublic( + Buffer.concat([Buffer.from([4]), x, y]), + ); + return new Uint8Array(point.getPublic(true, 'array')); +} + +/** + * Converts a public JWK to Dock public-key bytes for a JWT algorithm. + * + * @param {object} jwk + * @param {string} algorithm EdDSA | ES256K | ES256 + * @returns {Uint8Array} + */ +export function jwkToPublicKey(jwk, algorithm) { + switch (algorithm) { + case 'EdDSA': + return jwkToEd25519PublicKey(jwk); + case 'ES256K': + return jwkToSecp256k1PublicKey(jwk); + case 'ES256': + return jwkToSecp256r1PublicKey(jwk); + default: + throw new Error(`Unsupported JWT algorithm for JWK: ${algorithm}`); + } +} diff --git a/packages/crypto-utils/src/vc/jws.js b/packages/crypto-utils/src/vc/jws.js index 3cbfe61db..d61c232e2 100644 --- a/packages/crypto-utils/src/vc/jws.js +++ b/packages/crypto-utils/src/vc/jws.js @@ -31,9 +31,9 @@ const jwtAlgorithms = { }, }; -const supportedJWTAlgorithms = Object.keys(jwtAlgorithms); +export const supportedJWTAlgorithms = Object.keys(jwtAlgorithms); -function verKeyType(value) { +export function verKeyType(value) { return value?.verKeyType ?? value?.constructor?.VerKeyType ?? value?.value?.constructor?.VerKeyType @@ -48,7 +48,7 @@ function inferJWTAlgorithm(keypair) { ); } -function getJWTAlgorithm(algorithm) { +export function getJWTAlgorithm(algorithm) { const config = jwtAlgorithms[algorithm]; if (!config) { throw new Error(`Unsupported JWT algorithm: ${algorithm}`); @@ -56,6 +56,44 @@ function getJWTAlgorithm(algorithm) { return config; } +/** + * Verifies a JOSE signing-input / signature pair with a Dock public key. + * + * @param {string|Uint8Array} data signing input (`header.payload`) + * @param {string|Uint8Array} signature base64url string or raw JOSE signature bytes + * @param {*} publicKey Dock public key + * @param {string} algorithm JWT alg (EdDSA | ES256K | ES256) + * @returns {boolean} + */ +export function verifyJwtSignature(data, signature, publicKey, algorithm) { + const { Keypair } = getJWTAlgorithm(algorithm); + const publicKeyType = verKeyType(publicKey); + if ( + publicKeyType != null + && !jwtAlgorithms[algorithm].verKeyTypes.includes(publicKeyType) + ) { + throw new Error( + `JWT algorithm ${algorithm} does not match public key type`, + ); + } + + const verifyData = typeof data === 'string' + ? new Uint8Array(Buffer.from(data, 'utf8')) + : valueBytes(data); + const signatureBytes = typeof signature === 'string' + ? new Uint8Array(base64url.toBuffer(signature)) + : valueBytes(signature); + const verificationSignature = algorithm.startsWith('ES') + ? joseSignatureToDER(signatureBytes) + : signatureBytes; + + return Keypair.verify( + verifyData, + verificationSignature, + valueBytes(publicKey), + ); +} + // Taken from https://github.com/transmute-industries/verifiable-data/blob/main/packages/jose-ld/src/JWS/createSigner.ts export async function signJWS(signer, type, options, data) { if (!type) { @@ -229,28 +267,11 @@ export function verifyJWT( throw new Error(`Unsupported JWT algorithm: ${protectedHeader.alg}`); } - const algorithm = getJWTAlgorithm(protectedHeader.alg); - const publicKeyType = verKeyType(publicKey); - if ( - publicKeyType != null - && !algorithm.verKeyTypes.includes(publicKeyType) - ) { - throw new Error( - `JWT algorithm ${protectedHeader.alg} does not match public key type`, - ); - } - - const signature = new Uint8Array(base64url.toBuffer(encodedSignature)); - const verifyData = new Uint8Array( - Buffer.from(`${encodedHeader}.${encodedPayload}`, 'utf8'), - ); - const verificationSignature = protectedHeader.alg.startsWith('ES') - ? joseSignatureToDER(signature) - : signature; - const verified = algorithm.Keypair.verify( - verifyData, - verificationSignature, - valueBytes(publicKey), + const verified = verifyJwtSignature( + `${encodedHeader}.${encodedPayload}`, + encodedSignature, + publicKey, + protectedHeader.alg, ); return verified diff --git a/packages/crypto-utils/src/vc/sd-jwt.js b/packages/crypto-utils/src/vc/sd-jwt.js index a244bcc04..bc4e708c4 100644 --- a/packages/crypto-utils/src/vc/sd-jwt.js +++ b/packages/crypto-utils/src/vc/sd-jwt.js @@ -1,6 +1,23 @@ import { createHash } from 'node:crypto'; -import { decodeJwtPayload } from './jws'; +import { digest, generateSalt, ES256 } from '@sd-jwt/crypto-nodejs'; +import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'; +import base64url from 'base64url'; + +import { + getKeyFromDIDDocument, + publicKeyFromKeyDocument, +} from '../key-utils'; +import { valueBytes } from '../utils'; +import { jwkToPublicKey } from './jwk'; +import { + decodeJwtPayload, + decodeJwtProtectedHeader, + getJWTAlgorithm, + supportedJWTAlgorithms, + verifyJwtSignature, + verKeyType, +} from './jws'; const SD_HASH_ALGORITHMS = { 'sha-256': 'sha256', @@ -8,6 +25,192 @@ const SD_HASH_ALGORITHMS = { 'sha-512': 'sha512', }; +function createSdJwtInstance(options = {}) { + return new SDJwtVcInstance({ + hasher: digest, + hashAlg: 'sha-256', + saltGenerator: generateSalt, + ...options, + }); +} + +function getDocLoader(documentLoader, resolver) { + if (documentLoader && resolver) { + throw new Error( + 'Passing resolver and documentLoader results in resolver being ignored, please re-factor.', + ); + } + if (documentLoader) { + return documentLoader; + } + if (!resolver) { + return null; + } + return async (uri) => ({ + document: await resolver.resolve(uri), + documentUrl: uri, + contextUrl: null, + }); +} + +function decodeJwtPartAsJson(part, partName) { + if (!part) { + return {}; + } + try { + return JSON.parse(base64url.decode(part)); + } catch (cause) { + throw new Error(`Invalid SD-JWT ${partName}`, { cause }); + } +} + +function getSDJWTSignAlg(jwt) { + try { + const header = decodeJwtProtectedHeader(jwt.split('~')[0], 'SD-JWT'); + return header.alg || 'EdDSA'; + } catch { + return 'EdDSA'; + } +} + +function claimsToCredentialResult(presentationClaims) { + const { + iss, + iat, + exp, + vct, + ...subject + } = presentationClaims; + + return { + '@context': ['https://www.w3.org/2018/credentials/v1'], + issuanceDate: new Date(iat * 1000).toISOString(), + issuer: String(iss).split('#')[0], + type: [vct], + expirationDate: exp ? new Date(exp * 1000).toISOString() : undefined, + credentialSubject: subject, + proof: null, + }; +} + +function extractSdJwtParts(jwtStr, decoded) { + const [jwtHeader, jwtPayload] = jwtStr.split('.'); + const rawHeader = decoded?.jwt?.header || decodeJwtPartAsJson(jwtHeader, 'header'); + const rawPayload = decoded?.jwt?.payload || decodeJwtPartAsJson(jwtPayload, 'payload'); + return { + issuerKey: decoded?.jwt?.payload?.iss ?? rawPayload?.iss, + headerKid: decoded?.jwt?.header?.kid ?? rawHeader?.kid, + verificationJWK: decoded?.jwt?.payload?.cnf?.jwk + || rawPayload?.cnf?.jwk + || decoded?.jwt?.header?.jwk + || rawHeader?.jwk, + }; +} + +function applyEmbeddedJwk(verificationJWK, signAlg) { + if (!verificationJWK) { + return { verifierKey: null, dockPublicKey: null, verificationJWK: null }; + } + if (signAlg === 'ES256') { + return { + verifierKey: { type: 'JsonWebKey2020', publicKeyJwk: verificationJWK }, + dockPublicKey: null, + verificationJWK, + }; + } + const dockPublicKey = jwkToPublicKey(verificationJWK, signAlg); + return { verifierKey: dockPublicKey, dockPublicKey, verificationJWK }; +} + +function normalizeResolvedKey(resolvedKey, signAlg) { + if (resolvedKey == null) { + return { verifierKey: null, dockPublicKey: null }; + } + if (typeof resolvedKey.verifier === 'function') { + return { verifierKey: resolvedKey, dockPublicKey: null }; + } + if (resolvedKey.publicKeyJwk) { + const dockPublicKey = jwkToPublicKey(resolvedKey.publicKeyJwk, signAlg); + return { verifierKey: resolvedKey, dockPublicKey }; + } + if (verKeyType(resolvedKey) != null) { + return { verifierKey: resolvedKey, dockPublicKey: resolvedKey }; + } + const dockPublicKey = valueBytes(resolvedKey); + return { verifierKey: resolvedKey, dockPublicKey }; +} + +async function resolveIssuerKey({ + keyResolver, + docLoader, + issuerKey, + headerKid, + signAlg, +}) { + if (typeof keyResolver === 'function') { + return normalizeResolvedKey( + await keyResolver(issuerKey ?? headerKid), + signAlg, + ); + } + if (!docLoader) { + // eslint-disable-next-line sonarjs/no-duplicate-string -- keep dock-compatible raw error text + throw new Error('Issuer key not found in SDJWT iss property'); + } + + const uri = headerKid || issuerKey; + const { document } = await docLoader(uri); + const keyDocument = getKeyFromDIDDocument(document, uri); + if (!keyDocument) { + throw new Error(`Cannot find key document with ID: ${uri}`); + } + const verifierKey = publicKeyFromKeyDocument(keyDocument, signAlg); + if (typeof verifierKey?.verifier === 'function') { + return { verifierKey, dockPublicKey: null }; + } + return { verifierKey, dockPublicKey: verifierKey }; +} + +function createPresentationVerifier({ + jwtStr, + signAlg, + getState, +}) { + let es256Verifier; + return async (data, sig) => { + const { verificationJWK, verifierKey, dockPublicKey } = getState(); + + if (verificationJWK && signAlg === 'ES256') { + if (!es256Verifier) { + es256Verifier = await ES256.getVerifier(verificationJWK); + } + const signInput = typeof data === 'string' ? data : Buffer.from(data).toString(); + return es256Verifier(signInput, sig); + } + + if (verifierKey && typeof verifierKey.verifier === 'function') { + const isJWK = verifierKey.type === 'JsonWebKey2020'; + const { verify } = verifierKey.verifier(); + if (isJWK) { + return verify({ + data: Buffer.from(data), + signature: jwtStr.split('~')[0], + }); + } + const signature = typeof sig === 'string' + ? new Uint8Array(base64url.toBuffer(sig)) + : valueBytes(sig); + return verify({ data, signature }); + } + + const publicKey = dockPublicKey ?? verifierKey; + if (publicKey == null) { + return false; + } + return verifyJwtSignature(data, sig, publicKey, signAlg); + }; +} + /** * Encodes an RFC 9901 Disclosure array as base64url. * @@ -136,3 +339,125 @@ export function computeSdHash({ issuerJwt, disclosures = [] }) { .update(Buffer.from(encodedPresentation, 'ascii')) .digest('base64url'); } + +/** + * Checks whether a compact JWT/SD-JWT string is an SD-JWT credential. + * + * @param {*} jwt + * @returns {boolean} + */ +export function isSDJWTCredential(jwt) { + if (typeof jwt !== 'string' || !jwt.includes('.')) { + return false; + } + + try { + const decodedHeader = JSON.parse(base64url.decode(jwt.split('.')[0])); + return decodedHeader?.typ === 'dc+sd-jwt' || decodedHeader?.typ === 'vc+sd-jwt'; + } catch { + return false; + } +} + +/** + * Decodes a compact SD-JWT without verifying its signature. + * + * @param {string} presentation + * @returns {Promise} + */ +export async function decodeSdJwt(presentation) { + const sdjwt = createSdJwtInstance(); + return sdjwt.decode(presentation); +} + +/** + * Verifies an SD-JWT credential using dock-API-compatible options and return shape. + * + * Callers supply issuer keys via `keyResolver`, embedded JWK, `documentLoader`, or + * `resolver`. crypto-utils does not resolve DIDs itself. + * + * @param {string|{jwt: string}} body + * @param {string[]} [requiredAttribs] + * @param {object} [options] + * @param {function} [options.keyResolver] async (issOrKid) => verification key + * @param {function} [options.documentLoader] (uri) => Promise<{document}> + * @param {{supports: function, resolve: function}} [options.resolver] + * @param {string[]} [options.algorithms] + * @returns {Promise<{verified: true, errors: [], results: [], credentialResults: object[]}>} + */ +export async function verifySDJWTCredential( + body, + requiredAttribs, + { + keyResolver = null, + documentLoader = null, + resolver = null, + algorithms = supportedJWTAlgorithms, + } = {}, +) { + const jwtStr = typeof body === 'string' ? body : body?.jwt; + if (typeof jwtStr !== 'string' || jwtStr.length === 0) { + throw new TypeError('SD-JWT credential must be a non-empty string or { jwt }'); + } + if (!isSDJWTCredential(jwtStr)) { + throw new Error('Credential is not an SD-JWT (expected typ dc+sd-jwt or vc+sd-jwt)'); + } + + const signAlg = getSDJWTSignAlg(jwtStr); + if (!algorithms.includes(signAlg)) { + throw new Error(`Unsupported JWT algorithm: ${signAlg}`); + } + getJWTAlgorithm(signAlg); + + // Validate loader options before decode so mutual-exclusion errors surface first. + const docLoader = getDocLoader(documentLoader, resolver); + + const state = { + verifierKey: null, + dockPublicKey: null, + verificationJWK: null, + }; + + const sdjwt = createSdJwtInstance({ + signAlg, + verifier: createPresentationVerifier({ + jwtStr, + signAlg, + getState: () => state, + }), + }); + + const decoded = await sdjwt.decode(jwtStr); + const { issuerKey, headerKid, verificationJWK } = extractSdJwtParts(jwtStr, decoded); + Object.assign(state, applyEmbeddedJwk(verificationJWK, signAlg)); + + if (!state.verifierKey && !issuerKey && !headerKid) { + // eslint-disable-next-line sonarjs/no-duplicate-string -- keep dock-compatible raw error text + throw new Error('Issuer key not found in SDJWT iss property'); + } + + if (!state.verifierKey) { + Object.assign(state, await resolveIssuerKey({ + keyResolver, + docLoader, + issuerKey, + headerKid, + signAlg, + })); + } + + if (!state.verifierKey && state.dockPublicKey == null) { + // eslint-disable-next-line sonarjs/no-duplicate-string -- keep dock-compatible raw error text + throw new Error('Issuer key not found in SDJWT iss property'); + } + + await sdjwt.verify(jwtStr, requiredAttribs); + const presentationClaims = await sdjwt.getClaims(jwtStr); + + return { + verified: true, + errors: [], + results: [], + credentialResults: [claimsToCredentialResult(presentationClaims)], + }; +} diff --git a/packages/crypto-utils/tests/crypto-utils.test.js b/packages/crypto-utils/tests/crypto-utils.test.js index 18828a75a..cde898501 100644 --- a/packages/crypto-utils/tests/crypto-utils.test.js +++ b/packages/crypto-utils/tests/crypto-utils.test.js @@ -1,5 +1,3 @@ -import { createHash } from 'node:crypto'; - import base64url from 'base64url'; import { @@ -9,15 +7,11 @@ import { SignatureEd25519Value, Secp256k1Keypair, Secp256r1Keypair, - computeSdHash, createJws, createJwsSigner, createRawSigner, - decodeJwtPayload, joseSignatureToDER, jwkToSecp256r1PublicKey, - parseFinalSdJwtPresentation, - parseSdJwtPresentation, secp256r1PublicKeyToJwk, signJWT, signJWS, @@ -163,47 +157,6 @@ describe('JWS helpers', () => { expect(Buffer.from(result)).toEqual(Buffer.from('header.\u0001\u0002\u0003')); }); - test('decodes JWT payloads and computes SD-JWT presentation hashes', () => { - const issuerPayload = { _sd_alg: 'sha-256', vct: 'mandate.payment.1' }; - const issuerJwt = [ - base64url.encode(JSON.stringify({ alg: 'ES256' })), - base64url.encode(JSON.stringify(issuerPayload)), - 'signature', - ].join('.'); - const disclosure = base64url.encode(JSON.stringify([ - 'salt', - 'payment_id', - 'PAY-001', - ])); - const expected = createHash('sha256') - .update(Buffer.from(`${issuerJwt}~${disclosure}~`, 'ascii')) - .digest('base64url'); - const kbJwt = [ - base64url.encode(JSON.stringify({ alg: 'ES256', typ: 'kb+jwt' })), - base64url.encode(JSON.stringify({ sd_hash: expected })), - 'kb-signature', - ].join('.'); - const presentation = `${issuerJwt}~${disclosure}~${kbJwt}`; - - expect(decodeJwtPayload(issuerJwt)).toEqual(issuerPayload); - expect(parseSdJwtPresentation(presentation)).toEqual({ - issuerJwt, - disclosures: [disclosure], - kbJwt, - }); - expect(parseFinalSdJwtPresentation( - `${issuerJwt}~~~${presentation}`, - )).toEqual({ - issuerJwt, - disclosures: [disclosure], - kbJwt, - }); - expect(computeSdHash({ - issuerJwt, - disclosures: [disclosure], - })).toBe(expected); - }); - test('converts P-256 public keys to and from JWK', () => { const keypair = Secp256r1Keypair.random(); const publicKey = keypair.publicKey(); diff --git a/packages/crypto-utils/tests/sd-jwt.test.js b/packages/crypto-utils/tests/sd-jwt.test.js new file mode 100644 index 000000000..baa80f6a1 --- /dev/null +++ b/packages/crypto-utils/tests/sd-jwt.test.js @@ -0,0 +1,458 @@ +import { createHash } from 'node:crypto'; + +import { + generateAp2KeyPair, + issueClosedPaymentMandate, + verifyClosedPaymentMandate, +} from '@ar-agents/ap2'; +import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'; +import base64url from 'base64url'; + +import { + Ed25519Keypair, + Secp256k1Keypair, + Secp256r1Keypair, + computeSdHash, + createJwsSigner, + decodeJwtPayload, + isSDJWTCredential, + jwkToSecp256r1PublicKey, + parseFinalSdJwtPresentation, + parseSdJwtPresentation, + secp256r1PublicKeyToJwk, + verifySDJWTCredential, +} from '../src'; + +async function issueTestSdJwt(keypair, algorithm, claims, disclosureFrame, header = {}) { + const signer = createJwsSigner(keypair); + const sdjwt = new SDJwtVcInstance({ + signer: async (data) => { + const bytes = typeof data === 'string' ? Buffer.from(data) : data; + const signature = await signer.sign({ data: bytes }); + return Buffer.from(signature).toString('base64url'); + }, + signAlg: algorithm, + hasher: digest, + hashAlg: 'sha-256', + saltGenerator: generateSalt, + }); + + return sdjwt.issue(claims, disclosureFrame, { + header: { + typ: 'vc+sd-jwt', + alg: algorithm, + ...header, + }, + }); +} + +describe('SD-JWT helpers', () => { + test('decodes JWT payloads and computes SD-JWT presentation hashes', () => { + const issuerPayload = { _sd_alg: 'sha-256', vct: 'mandate.payment.1' }; + const issuerJwt = [ + base64url.encode(JSON.stringify({ alg: 'ES256' })), + base64url.encode(JSON.stringify(issuerPayload)), + 'signature', + ].join('.'); + const disclosure = base64url.encode(JSON.stringify([ + 'salt', + 'payment_id', + 'PAY-001', + ])); + const expected = createHash('sha256') + .update(Buffer.from(`${issuerJwt}~${disclosure}~`, 'ascii')) + .digest('base64url'); + const kbJwt = [ + base64url.encode(JSON.stringify({ alg: 'ES256', typ: 'kb+jwt' })), + base64url.encode(JSON.stringify({ sd_hash: expected })), + 'kb-signature', + ].join('.'); + const presentation = `${issuerJwt}~${disclosure}~${kbJwt}`; + + expect(decodeJwtPayload(issuerJwt)).toEqual(issuerPayload); + expect(parseSdJwtPresentation(presentation)).toEqual({ + issuerJwt, + disclosures: [disclosure], + kbJwt, + }); + expect(parseFinalSdJwtPresentation( + `${issuerJwt}~~~${presentation}`, + )).toEqual({ + issuerJwt, + disclosures: [disclosure], + kbJwt, + }); + expect(computeSdHash({ + issuerJwt, + disclosures: [disclosure], + })).toBe(expected); + }); +}); + +describe('SD-JWT credential verification', () => { + test('detects SD-JWT credentials by header typ', () => { + const vcHeader = base64url.encode(JSON.stringify({ typ: 'vc+sd-jwt', alg: 'EdDSA' })); + const dcHeader = base64url.encode(JSON.stringify({ typ: 'dc+sd-jwt', alg: 'ES256' })); + const jwtHeader = base64url.encode(JSON.stringify({ typ: 'JWT', alg: 'EdDSA' })); + + expect(isSDJWTCredential(`${vcHeader}.PAY.SIG`)).toBe(true); + expect(isSDJWTCredential(`${dcHeader}.PAY.SIG`)).toBe(true); + expect(isSDJWTCredential(`${jwtHeader}.PAY.SIG`)).toBe(false); + expect(isSDJWTCredential({ not: 'a string' })).toBe(false); + }); + + test.each([ + { + algorithm: 'EdDSA', + Keypair: Ed25519Keypair, + sourceType: 'seed', + }, + { + algorithm: 'ES256K', + Keypair: Secp256k1Keypair, + sourceType: 'entropy', + }, + { + algorithm: 'ES256', + Keypair: Secp256r1Keypair, + sourceType: 'entropy', + }, + ])('issues and verifies a $algorithm SD-JWT with keyResolver', async ({ + algorithm, + Keypair, + sourceType, + }) => { + const source = Uint8Array.from( + { length: Keypair.SeedSize }, + (_, index) => index + 1, + ); + const keypair = sourceType === 'seed' + ? Keypair.fromSeed(source) + : Keypair.fromEntropy(source); + const iss = `did:ex:issuer#${algorithm.toLowerCase()}`; + const encoded = await issueTestSdJwt( + keypair, + algorithm, + { + iss, + iat: 1700000000, + exp: 1800000000, + vct: 'MyType', + givenName: 'Alice', + }, + { _sd: ['givenName'] }, + ); + + const result = await verifySDJWTCredential(encoded, ['givenName'], { + keyResolver: async (id) => { + expect(id).toBe(iss); + return keypair.publicKey(); + }, + }); + + expect(result).toMatchObject({ + verified: true, + errors: [], + results: [], + credentialResults: [{ + '@context': ['https://www.w3.org/2018/credentials/v1'], + issuer: 'did:ex:issuer', + type: ['MyType'], + issuanceDate: new Date(1700000000 * 1000).toISOString(), + expirationDate: new Date(1800000000 * 1000).toISOString(), + credentialSubject: { givenName: 'Alice' }, + proof: null, + }], + }); + + const asObject = await verifySDJWTCredential({ jwt: encoded }, undefined, { + keyResolver: async () => keypair.publicKey(), + }); + expect(asObject.verified).toBe(true); + }); + + test('verifies ES256 SD-JWT using embedded cnf.jwk without keyResolver', async () => { + const keypair = Secp256r1Keypair.fromEntropy(Uint8Array.from( + { length: Secp256r1Keypair.SeedSize }, + (_, index) => index + 1, + )); + const jwk = secp256r1PublicKeyToJwk(keypair.publicKey()); + const keyResolver = jest.fn(); + const encoded = await issueTestSdJwt( + keypair, + 'ES256', + { + iss: 'did:ex:issuer#es256', + iat: 1700000000, + vct: 'MyType', + givenName: 'Bob', + cnf: { jwk }, + }, + { _sd: ['givenName'] }, + { jwk }, + ); + + const result = await verifySDJWTCredential(encoded, ['givenName'], { + keyResolver, + }); + + expect(keyResolver).not.toHaveBeenCalled(); + expect(result.verified).toBe(true); + expect(result.credentialResults[0].credentialSubject).toEqual({ + givenName: 'Bob', + cnf: { jwk }, + }); + }); + + test('resolves issuer keys through documentLoader', async () => { + const keypair = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index + 1, + )); + const iss = 'did:ex:issuer#keys-1'; + const encoded = await issueTestSdJwt( + keypair, + 'EdDSA', + { + iss, + iat: 1700000000, + vct: 'MyType', + givenName: 'Carol', + }, + { _sd: ['givenName'] }, + { kid: iss }, + ); + + const result = await verifySDJWTCredential(encoded, ['givenName'], { + documentLoader: async (uri) => { + expect(uri).toBe(iss); + return { + document: { + id: 'did:ex:issuer', + verificationMethod: [{ + id: iss, + type: 'Ed25519VerificationKey2018', + publicKey: keypair.publicKey(), + }], + }, + documentUrl: uri, + contextUrl: null, + }; + }, + }); + + expect(result.verified).toBe(true); + expect(result.credentialResults[0].credentialSubject).toEqual({ + givenName: 'Carol', + }); + }); + + test('resolves issuer keys through resolver', async () => { + const keypair = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index + 2, + )); + const iss = 'did:ex:issuer#keys-2'; + const encoded = await issueTestSdJwt( + keypair, + 'EdDSA', + { + iss, + iat: 1700000000, + vct: 'MyType', + givenName: 'Dana', + }, + { _sd: ['givenName'] }, + ); + + const result = await verifySDJWTCredential(encoded, ['givenName'], { + resolver: { + supports: (uri) => uri.startsWith('did:ex:'), + resolve: async () => keypair.publicKey(), + }, + }); + + expect(result.verified).toBe(true); + }); + + test('rejects passing both resolver and documentLoader', async () => { + const header = base64url.encode(JSON.stringify({ typ: 'vc+sd-jwt', alg: 'EdDSA' })); + await expect(verifySDJWTCredential(`${header}.PAY.SIG~`, undefined, { + documentLoader: async () => ({ document: {} }), + resolver: { supports: () => true, resolve: async () => ({}) }, + })).rejects.toThrow( + 'Passing resolver and documentLoader results in resolver being ignored, please re-factor.', + ); + }); + + test('throws when issuer key cannot be resolved', async () => { + const keypair = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index + 3, + )); + const encoded = await issueTestSdJwt( + keypair, + 'EdDSA', + { + iat: 1700000000, + vct: 'MyType', + givenName: 'Eve', + }, + { _sd: ['givenName'] }, + ); + + await expect(verifySDJWTCredential(encoded)).rejects.toThrow( + 'Issuer key not found in SDJWT iss property', + ); + }); + + test('fails verification when required claims are missing', async () => { + const keypair = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index + 4, + )); + const encoded = await issueTestSdJwt( + keypair, + 'EdDSA', + { + iss: 'did:ex:issuer#keys-1', + iat: 1700000000, + vct: 'MyType', + givenName: 'Frank', + }, + { _sd: ['givenName'] }, + ); + + await expect(verifySDJWTCredential(encoded, ['familyName'], { + keyResolver: async () => keypair.publicKey(), + })).rejects.toThrow(); + }); + + test('fails verification with the wrong public key', async () => { + const keypair = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index + 5, + )); + const other = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index + 6, + )); + const encoded = await issueTestSdJwt( + keypair, + 'EdDSA', + { + iss: 'did:ex:issuer#keys-1', + iat: 1700000000, + vct: 'MyType', + givenName: 'Grace', + }, + { _sd: ['givenName'] }, + ); + + await expect(verifySDJWTCredential(encoded, ['givenName'], { + keyResolver: async () => other.publicKey(), + })).rejects.toThrow(); + }); + + test('verifies using an LD-style keyResolver with verifier()', async () => { + const keypair = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index + 7, + )); + const encoded = await issueTestSdJwt( + keypair, + 'EdDSA', + { + iss: 'did:ex:issuer#keys-1', + iat: 1700000000, + vct: 'MyType', + givenName: 'Heidi', + }, + { _sd: ['givenName'] }, + ); + + const result = await verifySDJWTCredential(encoded, ['givenName'], { + keyResolver: async () => ({ + type: 'Ed25519VerificationKey2020', + verifier: () => ({ + verify: async ({ data, signature }) => { + const verifyData = typeof data === 'string' + ? Buffer.from(data) + : data; + return Ed25519Keypair.verify( + verifyData, + signature, + keypair.publicKey(), + ); + }, + }), + }), + }); + + expect(result.verified).toBe(true); + expect(result.credentialResults[0].credentialSubject).toEqual({ + givenName: 'Heidi', + }); + }); + + test('verifies a closed payment mandate SD-JWT from the AP2 flow', async () => { + const agent = await generateAp2KeyPair('ES256'); + const transactionId = 'checkout-hash-ap2-001'; + const iat = 1700000000; + const presentation = await issueClosedPaymentMandate({ + mandate: { + vct: 'mandate.payment.1', + transaction_id: transactionId, + payee: { id: 'merchant_1' }, + payment_amount: { amount: 19900, currency: 'USD' }, + payment_instrument: { id: 'card_x', type: 'card' }, + iss: 'did:ex:shopping-agent', + iat, + }, + signingCtx: { + privateKey: agent.privateKey, + alg: 'ES256', + typ: 'dc+sd-jwt', + }, + }); + + expect(isSDJWTCredential(presentation)).toBe(true); + + const arAgentsResult = await verifyClosedPaymentMandate(presentation, { + issuerKey: agent.publicJwk, + expectedTransactionId: transactionId, + }); + expect(arAgentsResult.ok).toBe(true); + + const result = await verifySDJWTCredential( + presentation, + ['payment_amount', 'payment_instrument'], + { + keyResolver: async (iss) => { + expect(iss).toBe('did:ex:shopping-agent'); + return jwkToSecp256r1PublicKey(agent.publicJwk); + }, + }, + ); + + expect(result).toMatchObject({ + verified: true, + errors: [], + results: [], + credentialResults: [{ + '@context': ['https://www.w3.org/2018/credentials/v1'], + issuer: 'did:ex:shopping-agent', + type: ['mandate.payment.1'], + issuanceDate: new Date(iat * 1000).toISOString(), + credentialSubject: { + transaction_id: transactionId, + payee: { id: 'merchant_1' }, + payment_amount: { amount: 19900, currency: 'USD' }, + payment_instrument: { id: 'card_x', type: 'card' }, + }, + proof: null, + }], + }); + }); +}); diff --git a/scripts/bench/CHANGELOG.md b/scripts/bench/CHANGELOG.md index b5a006b36..efda3adc0 100644 --- a/scripts/bench/CHANGELOG.md +++ b/scripts/bench/CHANGELOG.md @@ -1,5 +1,12 @@ # @docknetwork/benchmarks +## 0.4.28 + +### Patch Changes + +- Updated dependencies + - @docknetwork/credential-sdk@0.56.2 + ## 0.4.27 ### Patch Changes diff --git a/scripts/bench/package.json b/scripts/bench/package.json index f4dbe7c8a..64afd4f28 100644 --- a/scripts/bench/package.json +++ b/scripts/bench/package.json @@ -2,14 +2,14 @@ "name": "@docknetwork/benchmarks", "private": true, "type": "module", - "version": "0.4.27", + "version": "0.4.28", "scripts": { "bench": "babel-node src/main.js" }, "dependencies": { "@docknetwork/cheqd-blockchain-api": "6.0.1", "@docknetwork/cheqd-blockchain-modules": "6.0.1", - "@docknetwork/credential-sdk": "0.56.1", + "@docknetwork/credential-sdk": "0.56.2", "@docknetwork/crypto-wasm-ts": "^0.63.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 078551b2c..b06b88c05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7096,6 +7096,21 @@ resolved "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz" integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== +"@sd-jwt/core@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.9.2.tgz#89092457660b45cdd0d9da2b30f2f8ecf19c7ba5" + integrity sha512-AbvID/VIogRtPtCWGmfUlCZczHaBSRFmTwOAiUeuPRjDJTGUQu3gMRKwsfSGRHlfPo0pSaRQ7lqN3N+a5i+SDA== + dependencies: + "@sd-jwt/decode" "0.9.2" + "@sd-jwt/present" "0.9.2" + "@sd-jwt/types" "0.9.2" + "@sd-jwt/utils" "0.9.2" + +"@sd-jwt/crypto-nodejs@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/crypto-nodejs/-/crypto-nodejs-0.9.2.tgz#23ad818b909a6b339d63cb38c1d230bf481ce0a1" + integrity sha512-OL5drLhrsDok60XfLoovAK+DT/6mA1myZe1tkA8JflV8tcx4OZTLi/UMVcW/hc+7nBugYICutVSoIejTYsgbEw== + "@sd-jwt/decode@0.7.2", "@sd-jwt/decode@^0.7.2": version "0.7.2" resolved "https://registry.npmjs.org/@sd-jwt/decode/-/decode-0.7.2.tgz" @@ -7104,6 +7119,32 @@ "@sd-jwt/types" "0.7.2" "@sd-jwt/utils" "0.7.2" +"@sd-jwt/decode@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.9.2.tgz#3f2df7b22bfce1ff4640f46b1e51d513c9c1446d" + integrity sha512-jHY7hqk7EMkp6E2cB13QmXvRZN7njvAveVh+zKKy0kxpAM7DmcR4TqcDA4mc5y4lP8zWFUgbk7oGLCx2wiBq+w== + dependencies: + "@sd-jwt/types" "0.9.2" + "@sd-jwt/utils" "0.9.2" + +"@sd-jwt/jwt-status-list@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/jwt-status-list/-/jwt-status-list-0.9.2.tgz#9af484cad38c137831fd2d836be3789822b3d45b" + integrity sha512-59bPkgSnwvibJgUNVKmSE3SFMQwvDnh3Xr21aUi0bgII7yoHTQ+vqnryVOF8qcjpyICyi/clIlkTw3VWSyAV+g== + dependencies: + "@sd-jwt/types" "0.9.2" + base64url "^3.0.1" + pako "^2.1.0" + +"@sd-jwt/present@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.9.2.tgz#476af63defbe8459cb033a88b0263c78d4251572" + integrity sha512-ozBDYfQxUCwu0N+gbyk6zJDraMslFFuNLW+GxITybdDfVJek65tBtbROB8ZBvkAlEl0PDRKebq+xKOCRmplmWA== + dependencies: + "@sd-jwt/decode" "0.9.2" + "@sd-jwt/types" "0.9.2" + "@sd-jwt/utils" "0.9.2" + "@sd-jwt/present@^0.7.2": version "0.7.2" resolved "https://registry.npmjs.org/@sd-jwt/present/-/present-0.7.2.tgz" @@ -7113,11 +7154,27 @@ "@sd-jwt/types" "0.7.2" "@sd-jwt/utils" "0.7.2" +"@sd-jwt/sd-jwt-vc@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/sd-jwt-vc/-/sd-jwt-vc-0.9.2.tgz#bbb3ea645bd6426866bdcbc024604ee12cd49dd4" + integrity sha512-UOx5ULWyztJv2FbIjVJ0XCDixXwpgFxRCFykhLeRJI0T9+48hi2ucvOdqZqD2X7LkXkjCZNR0kQlJs+/n8gLEg== + dependencies: + "@sd-jwt/core" "0.9.2" + "@sd-jwt/jwt-status-list" "0.9.2" + "@sd-jwt/utils" "0.9.2" + ajv "^8.17.1" + ajv-formats "^3.0.1" + "@sd-jwt/types@0.7.2", "@sd-jwt/types@^0.7.2": version "0.7.2" resolved "https://registry.npmjs.org/@sd-jwt/types/-/types-0.7.2.tgz" integrity sha512-1NRKowiW0ZiB9SGLApLPBH4Xk8gDQJ+nA9NdZ+uy6MmJKLEwjuJxO7yTvRIv/jX/0/Ebh339S7Kq4RD2AiFuRg== +"@sd-jwt/types@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.9.2.tgz#17a9d62e36d9863b2c20a127be22a3e77005a6a8" + integrity sha512-eV/MY80ekeTrqaohzPpkrgwPki6Igx69D6RniduV1Ehv6o/zaJQ2F0hY/RqBAkJhQtBQoOzouwKYHme40k1Dlw== + "@sd-jwt/utils@0.7.2": version "0.7.2" resolved "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.7.2.tgz" @@ -7126,6 +7183,14 @@ "@sd-jwt/types" "0.7.2" js-base64 "^3.7.6" +"@sd-jwt/utils@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.9.2.tgz#7e15091a5b7c636379932014f1fedb1713dc4217" + integrity sha512-GpTD0isav2f+JyMyzeyf6wV3nYcD5e3oL+sVVr/61Y3oaIBGMWLtGGGhBRMCimSnd8kbb0P9jLxvp7ioASk6vw== + dependencies: + "@sd-jwt/types" "0.9.2" + js-base64 "^3.7.6" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -12980,6 +13045,11 @@ pako@^2.0.4: resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== +pako@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.2.0.tgz#246b9d4c841a8d308e484c0d03786651b143e63a" + integrity sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" From 5597d16ff373ae013aa9a35eaa346b3bd84fcb6b Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Sun, 19 Jul 2026 23:20:47 +0100 Subject: [PATCH 3/9] Isomorphic crypto-utils --- packages/crypto-utils/README.md | 10 ++++++++- packages/crypto-utils/jest.config.js | 2 +- packages/crypto-utils/package.json | 8 +++++++ .../crypto-utils/src/utils/types/bytes.js | 16 ++++++++++++-- packages/crypto-utils/src/vc/jwk.js | 21 ++++++------------- packages/crypto-utils/src/vc/sd-jwt.js | 19 ++++++++--------- yarn.lock | 5 +++++ 7 files changed, 52 insertions(+), 29 deletions(-) diff --git a/packages/crypto-utils/README.md b/packages/crypto-utils/README.md index dd0e8c6c5..21819a8cf 100644 --- a/packages/crypto-utils/README.md +++ b/packages/crypto-utils/README.md @@ -31,4 +31,12 @@ the 64-byte `r || s` representation required by JOSE. To verify that JOSE representation with a Dock ECDSA keypair, first convert it to DER with `joseSignatureToDER`. -Subpath exports are available at `keypairs`, `types`, `utils`, and `vc`. +Subpath exports are available at `keypairs`, `types`, `utils`, `key-utils`, and +`vc`. + +## Environments + +Node is the default target. Browser and React Native bundlers resolve +`@sd-jwt/crypto-nodejs` to `@sd-jwt/crypto-browser` via this package's +`browser` / `react-native` fields. Apps still need a `Buffer` polyfill where +the runtime does not provide one. diff --git a/packages/crypto-utils/jest.config.js b/packages/crypto-utils/jest.config.js index 46dd25e9f..464dda3a5 100644 --- a/packages/crypto-utils/jest.config.js +++ b/packages/crypto-utils/jest.config.js @@ -6,7 +6,7 @@ export default { '^.+\\.(ts|js)$': ['babel-jest', { rootMode: 'upward' }], }, transformIgnorePatterns: [ - '/node_modules/(?!@stablelib|uuid|@sd-jwt|@ar-agents/ap2|jose)', + '/node_modules/(?!@stablelib|uuid|@sd-jwt|@ar-agents/ap2|jose|@noble)', ], verbose: true, }; diff --git a/packages/crypto-utils/package.json b/packages/crypto-utils/package.json index e37ce8f8f..334268ba9 100644 --- a/packages/crypto-utils/package.json +++ b/packages/crypto-utils/package.json @@ -57,7 +57,15 @@ "engines": { "node": ">=22.0.0" }, + "browser": { + "@sd-jwt/crypto-nodejs": "@sd-jwt/crypto-browser" + }, + "react-native": { + "@sd-jwt/crypto-nodejs": "@sd-jwt/crypto-browser" + }, "dependencies": { + "@noble/hashes": "^1.8.0", + "@sd-jwt/crypto-browser": "^0.9.2", "@sd-jwt/crypto-nodejs": "^0.9.2", "@sd-jwt/sd-jwt-vc": "^0.9.2", "@stablelib/ed25519": "^1.0.0", diff --git a/packages/crypto-utils/src/utils/types/bytes.js b/packages/crypto-utils/src/utils/types/bytes.js index 50b1c25dc..bc6ba48a6 100644 --- a/packages/crypto-utils/src/utils/types/bytes.js +++ b/packages/crypto-utils/src/utils/types/bytes.js @@ -1,4 +1,3 @@ -import crypto from 'crypto'; import { applyToValue } from '../interfaces'; import { ensureString } from './string'; import { ensureIterable, isIterable } from './iterable'; @@ -204,10 +203,23 @@ export const normalizeOrConvertStringToU8a = (bytesOrString) => (typeof bytesOrS /** * Creates random `Uint8Array` array of supplied byte length. + * Uses `globalThis.crypto.getRandomValues` (Node 22+, browsers, modern RN). * @param {number} length * @returns {Uint8Array} */ -export const randomAsU8a = (length) => u8aToU8a(crypto.randomBytes(length)); +export const randomAsU8a = (length) => { + if ( + globalThis.crypto == null + || typeof globalThis.crypto.getRandomValues !== 'function' + ) { + throw new Error( + 'globalThis.crypto.getRandomValues is required for randomAsU8a', + ); + } + const bytes = new Uint8Array(length); + globalThis.crypto.getRandomValues(bytes); + return bytes; +}; /** * Creates random hex string of supplied byte length. diff --git a/packages/crypto-utils/src/vc/jwk.js b/packages/crypto-utils/src/vc/jwk.js index 2c3060fda..c3b1bddb8 100644 --- a/packages/crypto-utils/src/vc/jwk.js +++ b/packages/crypto-utils/src/vc/jwk.js @@ -1,11 +1,9 @@ -import { ECDH } from 'node:crypto'; - import elliptic from 'elliptic'; import { valueBytes } from '../utils'; -const P256_CURVE = 'prime256v1'; const EC = elliptic.ec; +const secp256r1Curve = new EC('p256'); const secp256k1Curve = new EC('secp256k1'); /** @@ -15,12 +13,8 @@ const secp256k1Curve = new EC('secp256k1'); * @returns {{kty: 'EC', crv: 'P-256', x: string, y: string}} */ export function secp256r1PublicKeyToJwk(publicKey) { - const uncompressed = ECDH.convertKey( - Buffer.from(valueBytes(publicKey)), - P256_CURVE, - undefined, - undefined, - 'uncompressed', + const uncompressed = Buffer.from( + secp256r1Curve.keyFromPublic(valueBytes(publicKey)).getPublic(false, 'array'), ); return { @@ -55,13 +49,10 @@ export function jwkToSecp256r1PublicKey(jwk) { throw new Error('P-256 JWK coordinates must be 32 bytes'); } - return new Uint8Array(ECDH.convertKey( + const point = secp256r1Curve.keyFromPublic( Buffer.concat([Buffer.from([4]), x, y]), - P256_CURVE, - undefined, - undefined, - 'compressed', - )); + ); + return new Uint8Array(point.getPublic(true, 'array')); } /** diff --git a/packages/crypto-utils/src/vc/sd-jwt.js b/packages/crypto-utils/src/vc/sd-jwt.js index bc4e708c4..fd6ebf0d3 100644 --- a/packages/crypto-utils/src/vc/sd-jwt.js +++ b/packages/crypto-utils/src/vc/sd-jwt.js @@ -1,5 +1,4 @@ -import { createHash } from 'node:crypto'; - +import { sha256, sha384, sha512 } from '@noble/hashes/sha2.js'; import { digest, generateSalt, ES256 } from '@sd-jwt/crypto-nodejs'; import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'; import base64url from 'base64url'; @@ -20,9 +19,9 @@ import { } from './jws'; const SD_HASH_ALGORITHMS = { - 'sha-256': 'sha256', - 'sha-384': 'sha384', - 'sha-512': 'sha512', + 'sha-256': sha256, + 'sha-384': sha384, + 'sha-512': sha512, }; function createSdJwtInstance(options = {}) { @@ -329,15 +328,15 @@ export function computeSdHash({ issuerJwt, disclosures = [] }) { const sdAlgorithm = String( Reflect.get(issuerPayload, '_sd_alg') ?? 'sha-256', ).toLowerCase(); - const hashAlgorithm = SD_HASH_ALGORITHMS[sdAlgorithm]; - if (!hashAlgorithm) { + const hash = SD_HASH_ALGORITHMS[sdAlgorithm]; + if (!hash) { throw new Error(`Unsupported SD-JWT hash algorithm: ${sdAlgorithm}`); } const encodedPresentation = `${[issuerJwt, ...disclosures].join('~')}~`; - return createHash(hashAlgorithm) - .update(Buffer.from(encodedPresentation, 'ascii')) - .digest('base64url'); + return Buffer.from(hash(Buffer.from(encodedPresentation, 'ascii'))).toString( + 'base64url', + ); } /** diff --git a/yarn.lock b/yarn.lock index b06b88c05..480880fb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7106,6 +7106,11 @@ "@sd-jwt/types" "0.9.2" "@sd-jwt/utils" "0.9.2" +"@sd-jwt/crypto-browser@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@sd-jwt/crypto-browser/-/crypto-browser-0.9.2.tgz#124d83eba0883bdf3ca4de635197eaeabf224214" + integrity sha512-m/l+z3c6ily0uadGEdg6979h8HXgl975qeM2xn/qC0hXtzypM8XW9C7Ym7M6IaBTUkZoh9RRjD6wO0tEtpPsEg== + "@sd-jwt/crypto-nodejs@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@sd-jwt/crypto-nodejs/-/crypto-nodejs-0.9.2.tgz#23ad818b909a6b339d63cb38c1d230bf481ce0a1" From 105a6a3b5c20b3abc729235511ed08bdbd9f9a57 Mon Sep 17 00:00:00 2001 From: Sam <3194321+cykoder@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:24:37 +0100 Subject: [PATCH 4/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/crypto-utils/src/vc/sd-jwt.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/crypto-utils/src/vc/sd-jwt.js b/packages/crypto-utils/src/vc/sd-jwt.js index fd6ebf0d3..97a08b86e 100644 --- a/packages/crypto-utils/src/vc/sd-jwt.js +++ b/packages/crypto-utils/src/vc/sd-jwt.js @@ -45,11 +45,16 @@ function getDocLoader(documentLoader, resolver) { if (!resolver) { return null; } - return async (uri) => ({ - document: await resolver.resolve(uri), - documentUrl: uri, - contextUrl: null, - }); + return async (uri) => { + if (typeof resolver.supports === 'function' && !resolver.supports(uri)) { + throw new Error(`Resolver does not support: ${uri}`); + } + return { + document: await resolver.resolve(uri), + documentUrl: uri, + contextUrl: null, + }; + }; } function decodeJwtPartAsJson(part, partName) { From 70a309ecbf5aade8561cf312ae17f53cb685a1d8 Mon Sep 17 00:00:00 2001 From: Sam <3194321+cykoder@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:24:57 +0100 Subject: [PATCH 5/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/crypto-utils/src/vc/sd-jwt.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/crypto-utils/src/vc/sd-jwt.js b/packages/crypto-utils/src/vc/sd-jwt.js index 97a08b86e..06571ec44 100644 --- a/packages/crypto-utils/src/vc/sd-jwt.js +++ b/packages/crypto-utils/src/vc/sd-jwt.js @@ -152,10 +152,10 @@ async function resolveIssuerKey({ signAlg, }) { if (typeof keyResolver === 'function') { - return normalizeResolvedKey( - await keyResolver(issuerKey ?? headerKid), - signAlg, - ); + const resolved = await keyResolver(issuerKey ?? headerKid); + if (resolved != null) { + return normalizeResolvedKey(resolved, signAlg); + } } if (!docLoader) { // eslint-disable-next-line sonarjs/no-duplicate-string -- keep dock-compatible raw error text From b7db94d28a4d700aebd388dacddda985d1956e05 Mon Sep 17 00:00:00 2001 From: Sam <3194321+cykoder@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:25:50 +0100 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/crypto-utils/src/vc/sd-jwt.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/crypto-utils/src/vc/sd-jwt.js b/packages/crypto-utils/src/vc/sd-jwt.js index 06571ec44..b3b0c70d6 100644 --- a/packages/crypto-utils/src/vc/sd-jwt.js +++ b/packages/crypto-utils/src/vc/sd-jwt.js @@ -196,9 +196,12 @@ function createPresentationVerifier({ const isJWK = verifierKey.type === 'JsonWebKey2020'; const { verify } = verifierKey.verifier(); if (isJWK) { + const signature = typeof sig === 'string' + ? new Uint8Array(base64url.toBuffer(sig)) + : valueBytes(sig); return verify({ data: Buffer.from(data), - signature: jwtStr.split('~')[0], + signature, }); } const signature = typeof sig === 'string' From 096c6e485a58529c11017550f5e6f1d20225a8e6 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Wed, 22 Jul 2026 21:26:59 +0100 Subject: [PATCH 7/9] Restore keydoc --- packages/credential-sdk/src/vc/status-list2021-credential.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/credential-sdk/src/vc/status-list2021-credential.js b/packages/credential-sdk/src/vc/status-list2021-credential.js index 8b271404a..207cf369c 100644 --- a/packages/credential-sdk/src/vc/status-list2021-credential.js +++ b/packages/credential-sdk/src/vc/status-list2021-credential.js @@ -8,6 +8,7 @@ import { gzip, ungzip } from 'pako'; import { u8aToU8a } from '../utils/types/bytes'; import VerifiableCredential from './verifiable-credential'; import { ensureStatusListId } from '../utils'; +import { KeyDoc } from "./helpers"; // eslint-disable-line /** * Status list 2021 verifiable credential as per https://www.w3.org/TR/vc-status-list/#statuslist2021credential. @@ -43,7 +44,7 @@ export default class StatusList2021Credential extends VerifiableCredential { * To allow unrevoking indices in the future, use `statusPurpose` = `suspension`. * The proof will be generated immediately using supplied `keyDoc`. * - * @param {object} keyDoc - key document used to sign the credential (see `getKeyDoc`) + * @param {KeyDoc} keyDoc * @param {string} id - on-chain hex identifier for the `StatusList2021Credential`. * @param {object} [params={}] * @param {'revocation'|'suspension'} [params.statusPurpose=revocation] - `statusPurpose` of the `StatusList2021Credential`. @@ -78,7 +79,7 @@ export default class StatusList2021Credential extends VerifiableCredential { * the underlying value won't be modified. * Throws an error if the underlying status list can't be decoded or any of the supplied indices is out of range. * - * @param {object} keyDoc - key document used to re-sign the credential (see `getKeyDoc`) + * @param {KeyDoc} keyDoc * @param {object} [update={}] * @param {Iterable} update.revokeIndices - indices to be revoked or suspended * @param {Iterable} update.unsuspendIndices - indices to be unsuspended From 602732eb78a9fa7f83f78b5abec1d44b01d83482 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Wed, 22 Jul 2026 21:28:40 +0100 Subject: [PATCH 8/9] Lint --- .../src/vc/status-list2021-credential.js | 5 ++- packages/crypto-utils/src/vc/sd-jwt.js | 36 ++++++++++--------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/credential-sdk/src/vc/status-list2021-credential.js b/packages/credential-sdk/src/vc/status-list2021-credential.js index 207cf369c..dc820c91a 100644 --- a/packages/credential-sdk/src/vc/status-list2021-credential.js +++ b/packages/credential-sdk/src/vc/status-list2021-credential.js @@ -8,7 +8,10 @@ import { gzip, ungzip } from 'pako'; import { u8aToU8a } from '../utils/types/bytes'; import VerifiableCredential from './verifiable-credential'; import { ensureStatusListId } from '../utils'; -import { KeyDoc } from "./helpers"; // eslint-disable-line + +/** + * @typedef {import('./helpers').KeyDoc} KeyDoc + */ /** * Status list 2021 verifiable credential as per https://www.w3.org/TR/vc-status-list/#statuslist2021credential. diff --git a/packages/crypto-utils/src/vc/sd-jwt.js b/packages/crypto-utils/src/vc/sd-jwt.js index b3b0c70d6..e15ea6953 100644 --- a/packages/crypto-utils/src/vc/sd-jwt.js +++ b/packages/crypto-utils/src/vc/sd-jwt.js @@ -175,8 +175,25 @@ async function resolveIssuerKey({ return { verifierKey, dockPublicKey: verifierKey }; } +function signatureBytes(sig) { + return typeof sig === 'string' + ? new Uint8Array(base64url.toBuffer(sig)) + : valueBytes(sig); +} + +async function verifyWithVerifierKey(verifierKey, data, sig) { + const { verify } = verifierKey.verifier(); + const signature = signatureBytes(sig); + if (verifierKey.type === 'JsonWebKey2020') { + return verify({ + data: Buffer.from(data), + signature, + }); + } + return verify({ data, signature }); +} + function createPresentationVerifier({ - jwtStr, signAlg, getState, }) { @@ -193,21 +210,7 @@ function createPresentationVerifier({ } if (verifierKey && typeof verifierKey.verifier === 'function') { - const isJWK = verifierKey.type === 'JsonWebKey2020'; - const { verify } = verifierKey.verifier(); - if (isJWK) { - const signature = typeof sig === 'string' - ? new Uint8Array(base64url.toBuffer(sig)) - : valueBytes(sig); - return verify({ - data: Buffer.from(data), - signature, - }); - } - const signature = typeof sig === 'string' - ? new Uint8Array(base64url.toBuffer(sig)) - : valueBytes(sig); - return verify({ data, signature }); + return verifyWithVerifierKey(verifierKey, data, sig); } const publicKey = dockPublicKey ?? verifierKey; @@ -428,7 +431,6 @@ export async function verifySDJWTCredential( const sdjwt = createSdJwtInstance({ signAlg, verifier: createPresentationVerifier({ - jwtStr, signAlg, getState: () => state, }), From bf2565d25aff3f552d068ec36a4fdeab6d07f2a8 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Wed, 22 Jul 2026 21:44:01 +0100 Subject: [PATCH 9/9] Fix doc gen --- packages/credential-sdk/src/vc/status-list2021-credential.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/credential-sdk/src/vc/status-list2021-credential.js b/packages/credential-sdk/src/vc/status-list2021-credential.js index dc820c91a..4f7d3dbd0 100644 --- a/packages/credential-sdk/src/vc/status-list2021-credential.js +++ b/packages/credential-sdk/src/vc/status-list2021-credential.js @@ -9,10 +9,6 @@ import { u8aToU8a } from '../utils/types/bytes'; import VerifiableCredential from './verifiable-credential'; import { ensureStatusListId } from '../utils'; -/** - * @typedef {import('./helpers').KeyDoc} KeyDoc - */ - /** * Status list 2021 verifiable credential as per https://www.w3.org/TR/vc-status-list/#statuslist2021credential. */