UNPKG

@dwn-protocol/id-sdk

Version:

SDK for accessing the features and capabilities

254 lines 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BbsCredential = exports.VC_DATA_FORMAT_LDP = void 0; const uuid_1 = require("uuid"); const bbs_js_1 = require("../crypto/crypto-primitives/bbs.js"); const index_js_1 = require("../common/index.js"); const index_js_2 = require("../dids/index.js"); const validators_js_1 = require("./validators.js"); const utils_js_1 = require("./utils.js"); const credential_js_1 = require("./credential.js"); const didResolver = new index_js_2.DidResolver({ didResolvers: [index_js_2.DidIonMethod, index_js_2.DidKeyMethod, index_js_2.DidDhtMethod] }); exports.VC_DATA_FORMAT_LDP = 'application/vc+ld+json'; const ENCODER = new TextEncoder(); /** * Converts a credential subject's attributes into an ordered array of * messages suitable for BBS+ multi-message signing. * The `id` field (subject DID) is always the first message. */ function credentialSubjectToMessages(credentialSubject) { const keys = []; const messages = []; const sortedEntries = Object.entries(credentialSubject).sort(([a], [b]) => { if (a === 'id') return -1; if (b === 'id') return 1; return a.localeCompare(b); }); for (const [key, value] of sortedEntries) { keys.push(key); const messageStr = typeof value === 'string' ? value : JSON.stringify(value); messages.push(ENCODER.encode(`${key}=${messageStr}`)); } return { messages, keys }; } /** * `BbsCredential` provides methods for creating, signing, and deriving * selective disclosure proofs from Verifiable Credentials using BBS+ * signatures on the BLS12-381 curve. * * Unlike JWT-based credentials that treat the payload as monolithic, * BBS+ signs each credential attribute as a separate message, enabling * zero-knowledge proofs that reveal only chosen attributes. * * Usage flow: * 1. Issuer: `create()` -> `sign()` -> store full credential in holder's DWN * 2. Holder: `deriveProof()` -> selectively disclose attributes to verifier * 3. Verifier: `verifyProof()` -> verify the derived proof */ class BbsCredential { /** * Creates a VC data model suitable for BBS+ signing. */ static create(options) { const { type, issuer, subject, data, issuanceDate, expirationDate } = options; const jsonData = JSON.parse(JSON.stringify(data)); if (typeof jsonData !== 'object') { throw new Error('Expected data to be parseable into a JSON object'); } if (!issuer || !subject) { throw new Error('Issuer and subject must be defined'); } const credentialSubject = Object.assign({ id: subject }, jsonData); const vcDataModel = Object.assign({ '@context': [credential_js_1.DEFAULT_CONTEXT, 'https://w3id.org/security/data-integrity/v2'], type: Array.isArray(type) ? [credential_js_1.DEFAULT_VC_TYPE, ...type] : type ? [credential_js_1.DEFAULT_VC_TYPE, type] : [credential_js_1.DEFAULT_VC_TYPE], id: `urn:uuid:${(0, uuid_1.v4)()}`, issuer, issuanceDate: issuanceDate || (0, utils_js_1.getCurrentXmlSchema112Timestamp)(), credentialSubject }, (expirationDate && { expirationDate })); validators_js_1.VcValidator.validateContext(vcDataModel['@context']); validators_js_1.VcValidator.validateVcType(vcDataModel.type); validators_js_1.VcValidator.validateCredentialSubject(vcDataModel.credentialSubject); return vcDataModel; } /** * Signs a VC with BBS+. Each attribute in `credentialSubject` becomes a * separate BBS+ message, enabling per-attribute selective disclosure. * * @returns A bundle containing the signed credential, message key order, * and base64url-encoded signature. */ static async sign(vcDataModel, signOptions) { const { kid, issuerDid, keyPair } = signOptions; const subject = vcDataModel.credentialSubject; const { messages, keys } = credentialSubjectToMessages(Array.isArray(subject) ? subject[0] : subject); const signature = await bbs_js_1.Bbs.sign({ keyPair, messages }); const signatureBase64Url = index_js_1.Convert.uint8Array(signature).toBase64Url(); const proof = { type: 'DataIntegrityProof', cryptosuite: 'bbs-2023', verificationMethod: `${issuerDid}#${kid}`, proofPurpose: 'assertionMethod', proofValue: signatureBase64Url, created: (0, utils_js_1.getCurrentXmlSchema112Timestamp)(), }; const signedCredential = Object.assign(Object.assign({}, vcDataModel), { proof }); return { credential: signedCredential, messageKeys: keys, signature: signatureBase64Url, }; } /** * Verifies a full BBS+ signed credential (not a derived proof). * Reconstructs the message array from `credentialSubject` and verifies * the signature against the issuer's public key. * * @param credential - The BBS+ signed VC. * @param issuerPublicKey - The issuer's 96-byte BLS12-381 G2 public key. * @returns `true` if the signature is valid. */ static async verify(credential, issuerPublicKey) { const proof = credential.proof; if (proof.cryptosuite !== 'bbs-2023') { throw new Error(`Unsupported cryptosuite: ${proof.cryptosuite}`); } const signature = index_js_1.Convert.base64Url(proof.proofValue).toUint8Array(); const subject = credential.credentialSubject; const { messages } = credentialSubjectToMessages(Array.isArray(subject) ? subject[0] : subject); return bbs_js_1.Bbs.verify({ publicKey: issuerPublicKey, signature, messages, }); } /** * Derives a zero-knowledge selective disclosure proof from a BBS+ signed * credential. The resulting credential contains only the disclosed * attributes and a proof that cryptographically demonstrates the holder * possesses a valid signature over the full attribute set. * * @param bundle - The signed credential bundle from `sign()`. * @param options - Specifies which attributes to reveal and a session nonce. * @returns The derived credential with only disclosed attributes visible. */ static async deriveProof(bundle, options) { const { issuerPublicKey, revealedAttributes, nonce } = options; const { credential, messageKeys, signature: signatureBase64Url } = bundle; const subject = credential.credentialSubject; const flatSubject = Array.isArray(subject) ? subject[0] : subject; const { messages } = credentialSubjectToMessages(flatSubject); // The `id` field (subject DID) is always disclosed const attributesToReveal = new Set(revealedAttributes); attributesToReveal.add('id'); const revealedIndices = []; for (let i = 0; i < messageKeys.length; i++) { if (attributesToReveal.has(messageKeys[i])) { revealedIndices.push(i); } } if (revealedIndices.length === 0) { throw new Error('At least one attribute must be revealed'); } const signatureBytes = index_js_1.Convert.base64Url(signatureBase64Url).toUint8Array(); const nonceBytes = ENCODER.encode(nonce); const proof = await bbs_js_1.Bbs.createProof({ publicKey: issuerPublicKey, signature: signatureBytes, messages, revealed: revealedIndices, nonce: nonceBytes, }); const proofBase64Url = index_js_1.Convert.uint8Array(proof).toBase64Url(); // Build the disclosed credential subject with only revealed attributes const disclosedSubject = {}; const disclosedKeys = []; for (const idx of revealedIndices) { const key = messageKeys[idx]; disclosedSubject[key] = flatSubject[key]; disclosedKeys.push(key); } const derivedProof = { type: 'DataIntegrityProof', cryptosuite: 'bbs-2023', verificationMethod: credential.proof.verificationMethod, proofPurpose: 'assertionMethod', proofValue: proofBase64Url, nonce, created: (0, utils_js_1.getCurrentXmlSchema112Timestamp)(), disclosedIndices: revealedIndices, }; const derivedCredential = Object.assign(Object.assign({ '@context': credential['@context'], type: credential.type, id: credential.id, issuer: credential.issuer, issuanceDate: credential.issuanceDate, credentialSubject: disclosedSubject }, (credential.expirationDate && { expirationDate: credential.expirationDate })), { proof: derivedProof }); return { credential: derivedCredential, disclosedKeys, disclosedIndices: revealedIndices, }; } /** * Verifies a BBS+ selective disclosure proof. Only the disclosed messages * are checked against the proof — the verifier does not learn the values * of undisclosed attributes. * * @param credential - The derived credential containing a selective disclosure proof. * @param issuerPublicKey - The issuer's 96-byte BLS12-381 G2 public key. * @returns `true` if the proof is valid. */ static async verifyProof(credential, issuerPublicKey) { const proof = credential.proof; if (proof.cryptosuite !== 'bbs-2023') { throw new Error(`Unsupported cryptosuite: ${proof.cryptosuite}`); } if (!proof.nonce) { throw new Error('Derived proof must contain a nonce'); } const proofBytes = index_js_1.Convert.base64Url(proof.proofValue).toUint8Array(); const nonceBytes = ENCODER.encode(proof.nonce); const subject = credential.credentialSubject; const flatSubject = Array.isArray(subject) ? subject[0] : subject; const { messages } = credentialSubjectToMessages(flatSubject); return bbs_js_1.Bbs.verifyProof({ publicKey: issuerPublicKey, proof: proofBytes, messages, nonce: nonceBytes, }); } /** * Resolves an issuer DID and attempts to extract the BBS+ public key * from the DID document's verification methods. * * @param issuerDid - The DID of the credential issuer. * @param kid - Optional key ID fragment to match a specific verification method. * @returns The BLS12-381 G2 public key as Uint8Array, or null if not found. */ static async resolveIssuerPublicKey(issuerDid, kid) { var _a; const resolution = await didResolver.resolve(issuerDid); const didDocument = resolution === null || resolution === void 0 ? void 0 : resolution.didDocument; if (!(didDocument === null || didDocument === void 0 ? void 0 : didDocument.verificationMethod)) return null; for (const vm of didDocument.verificationMethod) { const vmId = ((_a = vm.id) === null || _a === void 0 ? void 0 : _a.includes('#')) ? vm.id.split('#')[1] : vm.id; if (kid && vmId !== kid) continue; if (vm.type === 'Bls12381G2Key2020' || vm.type === 'JsonWebKey2020' || vm.type === 'Multikey') { if (vm.publicKeyMultibase) { // Multibase z-prefix = base58btc if (vm.publicKeyMultibase.startsWith('z')) { return index_js_1.Convert.base58Btc(vm.publicKeyMultibase.slice(1)).toUint8Array(); } } if (vm.publicKeyJwk) { const jwk = vm.publicKeyJwk; if (jwk.x) { return index_js_1.Convert.base64Url(jwk.x).toUint8Array(); } } } } return null; } } exports.BbsCredential = BbsCredential; //# sourceMappingURL=credential-bbs.js.map