@nuggetslife/vc 0.0.24 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ /**
2
+ * BBS-2023 Data Integrity Demo — W3C Verifiable Credentials with Selective Disclosure
3
+ *
4
+ * This demo walks through the full BBS-2023 cryptosuite lifecycle:
5
+ * 1. Issuer signs a JSON-LD Verifiable Credential with a DataIntegrityProof
6
+ * 2. Holder derives a selective disclosure proof (reveals only chosen fields)
7
+ * 3. Verifier checks the derived proof
8
+ * 4. Bonus: Blind signing with holder commitment (holder binding)
9
+ *
10
+ * BBS-2023 replaces BbsBlsSignature2020 for W3C Data Integrity proofs.
11
+ *
12
+ * Key differences from BbsBlsSignature2020:
13
+ * OLD: JSON-LD framing with @explicit reveal documents, base58 key pairs,
14
+ * BbsBlsSignature2020/BbsBlsSignatureProof2020 proof types,
15
+ * class-based API (new BbsBlsSignature2020({key})), async operations
16
+ * NEW: JSON Pointers for field selection, hex-encoded BBS keys,
17
+ * single "DataIntegrityProof" type with cryptosuite "bbs-2023",
18
+ * function-based API, mandatory vs selective pointer split
19
+ */
20
+
21
+ import {
22
+ bbsIetfKeygen,
23
+ bbs2023Sign,
24
+ bbs2023Derive,
25
+ bbs2023Verify,
26
+ bbs2023HolderCommit,
27
+ bbs2023BlindSign,
28
+ } from './index.js';
29
+
30
+ function separator(title) {
31
+ console.log('\n' + '='.repeat(70));
32
+ console.log(` ${title}`);
33
+ console.log('='.repeat(70) + '\n');
34
+ }
35
+
36
+ function printProof(doc) {
37
+ const { proof, ...rest } = doc;
38
+ console.log('Document (without proof):', JSON.stringify(rest, null, 2));
39
+ console.log('Proof:');
40
+ console.log(' type:', proof.type);
41
+ console.log(' cryptosuite:', proof.cryptosuite);
42
+ console.log(' proofValue:', proof.proofValue.substring(0, 60) + '...');
43
+ if (proof.verificationMethod) console.log(' verificationMethod:', proof.verificationMethod);
44
+ }
45
+
46
+ // ── Key Setup ────────────────────────────────────────────────────────
47
+
48
+ separator('Step 0: Generate BBS Key Pair');
49
+
50
+ const kp = bbsIetfKeygen();
51
+ console.log('BBS key pair (IETF draft-irtf-cfrg-bbs-signatures):');
52
+ console.log(' Secret key (hex, 32 bytes):', kp.secretKey.substring(0, 20) + '...');
53
+ console.log(' Public key (hex, 96 bytes):', kp.publicKey.substring(0, 20) + '...');
54
+
55
+ // ── Sample Credential ────────────────────────────────────────────────
56
+
57
+ const credential = {
58
+ '@context': [
59
+ 'https://www.w3.org/2018/credentials/v1',
60
+ {
61
+ // Inline context defining terms (in production, use published contexts)
62
+ givenName: 'http://schema.org/givenName',
63
+ familyName: 'http://schema.org/familyName',
64
+ email: 'http://schema.org/email',
65
+ birthDate: 'http://schema.org/birthDate',
66
+ alumniOf: 'http://schema.org/alumniOf',
67
+ },
68
+ ],
69
+ type: ['VerifiableCredential'],
70
+ issuer: 'did:example:issuer',
71
+ issuanceDate: '2025-01-01T00:00:00Z',
72
+ credentialSubject: {
73
+ id: 'did:example:alice',
74
+ givenName: 'Alice',
75
+ familyName: 'Smith',
76
+ email: 'alice@example.com',
77
+ birthDate: '1990-01-15',
78
+ alumniOf: 'Example University',
79
+ },
80
+ };
81
+
82
+ // ── 1. Sign ──────────────────────────────────────────────────────────
83
+
84
+ separator('Step 1: Issuer signs the credential (base proof)');
85
+
86
+ console.log('Mandatory pointers (always revealed): /issuer, /issuanceDate');
87
+ console.log('Everything else can be selectively disclosed by the holder.\n');
88
+
89
+ const signed = bbs2023Sign({
90
+ document: credential,
91
+ secretKey: kp.secretKey,
92
+ publicKey: kp.publicKey,
93
+ mandatoryPointers: ['/issuer', '/issuanceDate'],
94
+ verificationMethod: 'did:example:issuer#bbs-key-1',
95
+ proofPurpose: 'assertionMethod',
96
+ });
97
+
98
+ printProof(signed);
99
+
100
+ console.log('\nCompare with old BbsBlsSignature2020:');
101
+ console.log(' OLD: proof.type = "BbsBlsSignature2020"');
102
+ console.log(' NEW: proof.type = "DataIntegrityProof", proof.cryptosuite = "bbs-2023"');
103
+
104
+ // Verify the base proof
105
+ const baseResult = bbs2023Verify(signed, kp.publicKey);
106
+ console.log('\nBase proof verified:', baseResult.verified);
107
+
108
+ // ── 2. Derive ────────────────────────────────────────────────────────
109
+
110
+ separator('Step 2: Holder derives selective disclosure proof');
111
+
112
+ console.log('Selective pointers (holder chooses to reveal): /credentialSubject/givenName');
113
+ console.log('Hidden: familyName, email, birthDate, alumniOf\n');
114
+
115
+ console.log('Compare with old BbsBlsSignature2020:');
116
+ console.log(' OLD: revealDocument = { credentialSubject: { "@explicit": true, givenName: {} } }');
117
+ console.log(' NEW: selectivePointers = ["/credentialSubject/givenName"]\n');
118
+
119
+ const derived = bbs2023Derive({
120
+ document: signed,
121
+ selectivePointers: ['/credentialSubject/givenName'],
122
+ });
123
+
124
+ printProof(derived);
125
+
126
+ console.log('\nNote: the derived proof only covers mandatory + selected fields.');
127
+ console.log('The proof will not verify claims outside those sets.');
128
+ console.log('In practice, the holder strips undisclosed fields before sharing.');
129
+
130
+ // ── 3. Verify ────────────────────────────────────────────────────────
131
+
132
+ separator('Step 3: Verifier checks the derived proof');
133
+
134
+ const derivedResult = bbs2023Verify(derived, kp.publicKey);
135
+ console.log('Derived proof verified:', derivedResult.verified);
136
+ console.log('Error:', derivedResult.error ?? 'none');
137
+
138
+ // Show what's cryptographically attested
139
+ console.log('\nCryptographically attested fields (mandatory + selective pointers):');
140
+ console.log(' issuer:', derived.issuer, '(mandatory)');
141
+ console.log(' issuanceDate:', derived.issuanceDate, '(mandatory)');
142
+ console.log(' credentialSubject.givenName:', derived.credentialSubject?.givenName, '(selective)');
143
+ console.log(' credentialSubject.familyName: present but NOT attested by derived proof');
144
+ console.log(' credentialSubject.email: present but NOT attested by derived proof');
145
+
146
+ // Wrong key should fail
147
+ const wrongKp = bbsIetfKeygen();
148
+ const wrongResult = bbs2023Verify(derived, wrongKp.publicKey);
149
+ console.log('\nVerify with wrong key:', wrongResult.verified, '(expected false)');
150
+
151
+ // ── 4. Blind Signing (Holder Commitment) ─────────────────────────────
152
+
153
+ separator('Step 4: Blind Signing — Holder Binding');
154
+
155
+ console.log('Blind signing lets the holder commit to secret fields before the issuer signs.');
156
+ console.log('The issuer signs without seeing the committed values.');
157
+ console.log('This has no equivalent in BbsBlsSignature2020.\n');
158
+
159
+ // 4a. Holder creates a commitment
160
+ const commitment = bbs2023HolderCommit({
161
+ document: credential,
162
+ publicKey: kp.publicKey,
163
+ mandatoryPointers: ['/issuer', '/issuanceDate'],
164
+ committedPointers: ['/credentialSubject/email'],
165
+ });
166
+
167
+ console.log('Holder commitment:');
168
+ console.log(' commitment (hex):', commitment.commitment.substring(0, 40) + '...');
169
+ console.log(' blindFactor (hex):', commitment.blindFactor.substring(0, 40) + '...');
170
+ console.log(' committedIndices:', commitment.committedIndices);
171
+
172
+ // 4b. Issuer blind-signs with the commitment
173
+ const blindSigned = bbs2023BlindSign({
174
+ document: credential,
175
+ secretKey: kp.secretKey,
176
+ publicKey: kp.publicKey,
177
+ commitment: commitment.commitment,
178
+ hmacKey: commitment.hmacKey,
179
+ committedIndices: commitment.committedIndices,
180
+ mandatoryPointers: ['/issuer', '/issuanceDate'],
181
+ verificationMethod: 'did:example:issuer#bbs-key-1',
182
+ });
183
+
184
+ console.log('\nBlind-signed proof created:', blindSigned.proof.type, blindSigned.proof.cryptosuite);
185
+
186
+ // 4c. Holder derives (must include blindFactor)
187
+ const blindDerived = bbs2023Derive({
188
+ document: blindSigned,
189
+ selectivePointers: ['/credentialSubject/givenName', '/credentialSubject/email'],
190
+ blindFactor: commitment.blindFactor,
191
+ });
192
+
193
+ // 4d. Verify
194
+ const blindResult = bbs2023Verify(blindDerived, kp.publicKey);
195
+ console.log('Blind-signed derived proof verified:', blindResult.verified);
196
+
197
+ separator('Demo Complete');
198
+ console.log('BBS-2023 provides W3C-standard selective disclosure with:');
199
+ console.log(' - JSON Pointers instead of JSON-LD framing');
200
+ console.log(' - DataIntegrityProof type (not BbsBlsSignature2020)');
201
+ console.log(' - Mandatory vs selective pointer split');
202
+ console.log(' - Blind signing for holder binding (new capability)');
203
+ console.log('');
@@ -0,0 +1,193 @@
1
+ /**
2
+ * BBS IETF Demo — Low-Level BBS Signatures (draft-irtf-cfrg-bbs-signatures)
3
+ *
4
+ * This demo shows the raw BBS cryptographic primitives:
5
+ * 1. Key generation
6
+ * 2. Sign a set of messages
7
+ * 3. Verify the signature
8
+ * 4. Generate a selective disclosure proof (reveal only some messages)
9
+ * 5. Verify the proof
10
+ *
11
+ * This is the low-level layer that BBS-2023 is built on top of.
12
+ * Most developers will use BBS-2023 (for VCs) or SD-JWT (for plain claims)
13
+ * instead of calling these directly. But understanding the primitives helps.
14
+ *
15
+ * Key concepts:
16
+ * - BBS signs a list of messages as a single signature
17
+ * - A holder can create a zero-knowledge proof revealing only some messages
18
+ * - The verifier learns nothing about hidden messages
19
+ * - Two ciphersuites: SHA-256 (default) and SHAKE-256
20
+ */
21
+
22
+ import {
23
+ bbsIetfKeygen,
24
+ bbsIetfSign,
25
+ bbsIetfVerify,
26
+ bbsIetfProofGen,
27
+ bbsIetfProofVerify,
28
+ } from './index.js';
29
+
30
+ function separator(title) {
31
+ console.log('\n' + '='.repeat(70));
32
+ console.log(` ${title}`);
33
+ console.log('='.repeat(70) + '\n');
34
+ }
35
+
36
+ // ── 1. Key Generation ────────────────────────────────────────────────
37
+
38
+ separator('Step 1: Key Generation');
39
+
40
+ // Generate a random BBS key pair
41
+ const kp = bbsIetfKeygen();
42
+ console.log('Random BBS key pair:');
43
+ console.log(' Secret key (32 bytes hex):', kp.secretKey);
44
+ console.log(' Public key (96 bytes hex):', kp.publicKey.substring(0, 40) + '...');
45
+
46
+ // Deterministic keygen with IKM (Input Key Material)
47
+ const ikm = Buffer.alloc(32);
48
+ for (let i = 0; i < 32; i++) ikm[i] = i + 1;
49
+ const kpDet = bbsIetfKeygen(ikm);
50
+ const kpDet2 = bbsIetfKeygen(ikm);
51
+ console.log('\nDeterministic keygen (same IKM = same keys):');
52
+ console.log(' Keys match:', kpDet.secretKey === kpDet2.secretKey);
53
+
54
+ // ── 2. Sign Messages ────────────────────────────────────────────────
55
+
56
+ separator('Step 2: Sign a Set of Messages');
57
+
58
+ // BBS signs an ordered list of messages in a single operation.
59
+ // Each message is a string; the library handles hashing to curve scalars.
60
+ const messages = [
61
+ 'given_name: Alice',
62
+ 'family_name: Smith',
63
+ 'date_of_birth: 1990-01-15',
64
+ 'email: alice@example.com',
65
+ 'employee_id: EMP-42',
66
+ ];
67
+
68
+ console.log('Messages to sign:');
69
+ messages.forEach((m, i) => console.log(` [${i}] ${m}`));
70
+
71
+ const sk = Buffer.from(kp.secretKey, 'hex');
72
+ const pk = Buffer.from(kp.publicKey, 'hex');
73
+ const header = Buffer.from('credential-context-v1');
74
+
75
+ const signature = bbsIetfSign(sk, pk, header, messages, 'SHA-256');
76
+ console.log('\nSignature (80 bytes):', signature.toString('hex').substring(0, 40) + '...');
77
+
78
+ // ── 3. Verify Signature ─────────────────────────────────────────────
79
+
80
+ separator('Step 3: Verify the Signature');
81
+
82
+ const isValid = bbsIetfVerify(pk, signature, header, messages, 'SHA-256');
83
+ console.log('Signature valid:', isValid);
84
+
85
+ // Wrong key
86
+ const wrongKp = bbsIetfKeygen();
87
+ const wrongPk = Buffer.from(wrongKp.publicKey, 'hex');
88
+ const isValidWrong = bbsIetfVerify(wrongPk, signature, header, messages, 'SHA-256');
89
+ console.log('Wrong key valid:', isValidWrong, '(expected false)');
90
+
91
+ // Tampered message
92
+ const tampered = [...messages];
93
+ tampered[2] = 'date_of_birth: 2000-01-01';
94
+ const isValidTampered = bbsIetfVerify(pk, signature, header, tampered, 'SHA-256');
95
+ console.log('Tampered message valid:', isValidTampered, '(expected false)');
96
+
97
+ // ── 4. Selective Disclosure Proof ────────────────────────────────────
98
+
99
+ separator('Step 4: Generate Selective Disclosure Proof');
100
+
101
+ // The holder wants to prove they have a valid credential but only
102
+ // reveal their name and employee ID (indices 0, 1, 4).
103
+ // date_of_birth (2) and email (3) stay hidden.
104
+
105
+ const disclosedIndices = [0, 1, 4];
106
+ const presentationHeader = Buffer.from('verifier-challenge-xyz');
107
+
108
+ console.log('Disclosed messages (what the verifier will see):');
109
+ for (const idx of disclosedIndices) {
110
+ console.log(` [${idx}] ${messages[idx]}`);
111
+ }
112
+ console.log('Hidden messages (verifier learns nothing about these):');
113
+ for (let i = 0; i < messages.length; i++) {
114
+ if (!disclosedIndices.includes(i)) {
115
+ console.log(` [${i}] ${messages[i]}`);
116
+ }
117
+ }
118
+
119
+ const proof = bbsIetfProofGen(
120
+ pk,
121
+ signature,
122
+ header,
123
+ presentationHeader,
124
+ messages,
125
+ disclosedIndices,
126
+ 'SHA-256',
127
+ );
128
+
129
+ console.log(`\nProof generated (${proof.length} bytes)`);
130
+
131
+ // ── 5. Verify Proof ─────────────────────────────────────────────────
132
+
133
+ separator('Step 5: Verify the Selective Disclosure Proof');
134
+
135
+ // The verifier only has: public key, proof, disclosed messages, total count
136
+ // They do NOT have the original signature or hidden messages.
137
+ const disclosedMessages = {};
138
+ for (const idx of disclosedIndices) {
139
+ disclosedMessages[idx] = messages[idx];
140
+ }
141
+
142
+ console.log('Verifier inputs:');
143
+ console.log(' Public key: (96 bytes)');
144
+ console.log(' Proof: (' + proof.length + ' bytes)');
145
+ console.log(' Disclosed messages:', JSON.stringify(disclosedMessages, null, 4));
146
+ console.log(' Total message count:', messages.length);
147
+
148
+ const proofValid = bbsIetfProofVerify(
149
+ pk,
150
+ proof,
151
+ header,
152
+ presentationHeader,
153
+ disclosedMessages,
154
+ messages.length,
155
+ 'SHA-256',
156
+ );
157
+
158
+ console.log('\nProof valid:', proofValid);
159
+
160
+ // Try with wrong disclosed message
161
+ const wrongDisclosed = { ...disclosedMessages, 0: 'given_name: Bob' };
162
+ const proofValidWrong = bbsIetfProofVerify(
163
+ pk, proof, header, presentationHeader,
164
+ wrongDisclosed, messages.length, 'SHA-256',
165
+ );
166
+ console.log('Wrong disclosed message valid:', proofValidWrong, '(expected false)');
167
+
168
+ // ── 6. Ciphersuites ─────────────────────────────────────────────────
169
+
170
+ separator('Step 6: Ciphersuite Comparison');
171
+
172
+ console.log('Two ciphersuites available:');
173
+ console.log(' SHA-256 — default, widely compatible');
174
+ console.log(' SHAKE-256 — alternative hash, same security level\n');
175
+
176
+ const shakekp = bbsIetfKeygen(ikm, null, 'SHAKE-256');
177
+ const shakeSk = Buffer.from(shakekp.secretKey, 'hex');
178
+ const shakePk = Buffer.from(shakekp.publicKey, 'hex');
179
+
180
+ const shakeSig = bbsIetfSign(shakeSk, shakePk, null, ['test'], 'SHAKE-256');
181
+ const shakeValid = bbsIetfVerify(shakePk, shakeSig, null, ['test'], 'SHAKE-256');
182
+ console.log('SHAKE-256 sign + verify:', shakeValid);
183
+
184
+ // Cross-ciphersuite: should NOT work
185
+ const crossValid = bbsIetfVerify(shakePk, shakeSig, null, ['test'], 'SHA-256');
186
+ console.log('SHA-256 verify of SHAKE-256 sig:', crossValid, '(expected false)');
187
+
188
+ separator('Demo Complete');
189
+ console.log('BBS IETF provides the cryptographic foundation for:');
190
+ console.log(' - BBS-2023 Data Integrity proofs (high-level VC API)');
191
+ console.log(' - Any application needing multi-message signatures with selective disclosure');
192
+ console.log(' - Zero-knowledge proofs: verifier learns nothing about hidden messages');
193
+ console.log('');
@@ -0,0 +1,148 @@
1
+ /**
2
+ * SD-JWT Demo — Selective Disclosure using JSON Web Tokens
3
+ *
4
+ * This demo walks through the full SD-JWT lifecycle:
5
+ * 1. Issuer creates a credential with selectively-disclosable claims
6
+ * 2. Holder creates a presentation revealing only chosen claims
7
+ * 3. Verifier checks the presentation
8
+ *
9
+ * SD-JWT (RFC 9901) is a simpler alternative to BBS+ for selective disclosure.
10
+ * It works with standard JWTs (ES256, EdDSA, etc.) — no pairing-based crypto needed.
11
+ *
12
+ * Compare with BbsBlsSignature2020:
13
+ * OLD: Requires JSON-LD framing, BLS key pairs, reveal documents, linked data suites
14
+ * NEW: Plain JSON claims, standard JWK keys, just list which fields are disclosable
15
+ */
16
+
17
+ import {
18
+ JoseNamedCurve,
19
+ generateJwk,
20
+ sdJwtIssue,
21
+ sdJwtPresent,
22
+ sdJwtVerify,
23
+ } from './index.js';
24
+
25
+ function separator(title) {
26
+ console.log('\n' + '='.repeat(70));
27
+ console.log(` ${title}`);
28
+ console.log('='.repeat(70) + '\n');
29
+ }
30
+
31
+ // ── Key Setup ────────────────────────────────────────────────────────
32
+
33
+ separator('Step 0: Generate Keys');
34
+
35
+ const issuerJwk = generateJwk(JoseNamedCurve.P256);
36
+ const { d: _issD, ...issuerPublicJwk } = issuerJwk;
37
+ console.log('Issuer key (ES256/P-256):');
38
+ console.log(' Public:', JSON.stringify(issuerPublicJwk));
39
+
40
+ const holderJwk = generateJwk(JoseNamedCurve.P256);
41
+ const { d: _holD, ...holderPublicJwk } = holderJwk;
42
+ console.log('Holder key (ES256/P-256):');
43
+ console.log(' Public:', JSON.stringify(holderPublicJwk));
44
+
45
+ // ── 1. Issue ─────────────────────────────────────────────────────────
46
+
47
+ separator('Step 1: Issuer creates SD-JWT credential');
48
+
49
+ const claims = {
50
+ iss: 'https://issuer.example.com',
51
+ sub: 'did:example:user123',
52
+ given_name: 'Alice',
53
+ family_name: 'Smith',
54
+ email: 'alice@example.com',
55
+ birthdate: '1990-01-15',
56
+ nationalities: ['US', 'DE'],
57
+ };
58
+
59
+ // Mark which claims the holder can choose to reveal or hide.
60
+ // Non-listed claims (iss, sub) are always visible.
61
+ const disclosable = [
62
+ 'given_name',
63
+ 'family_name',
64
+ 'email',
65
+ 'birthdate',
66
+ 'nationalities[0]', // Individual array elements can be disclosable too
67
+ 'nationalities[1]',
68
+ ];
69
+
70
+ console.log('Claims:', JSON.stringify(claims, null, 2));
71
+ console.log('Disclosable fields:', disclosable);
72
+
73
+ const issued = sdJwtIssue(
74
+ claims,
75
+ disclosable,
76
+ issuerJwk, // Issuer's private key (signs the JWT)
77
+ 'ES256', // Standard JWT algorithm
78
+ holderPublicJwk, // Optional: bind to holder's key (Key Binding)
79
+ 2, // Optional: add 2 decoy digests (privacy)
80
+ );
81
+
82
+ console.log('\nSD-JWT token (truncated):');
83
+ console.log(' ' + issued.sdJwt.substring(0, 80) + '...');
84
+ console.log(`\n${issued.disclosures.length} disclosures created:`);
85
+ for (const d of issued.disclosures) {
86
+ const label = d.claimName || '(array element)';
87
+ console.log(` - ${label}: ${JSON.stringify(d.claimValue)}`);
88
+ }
89
+
90
+ // ── 2. Present (selective disclosure) ────────────────────────────────
91
+
92
+ separator('Step 2: Holder creates presentation (selective disclosure)');
93
+
94
+ // The holder chooses which disclosures to reveal.
95
+ // Here: reveal name but hide email, birthdate, and one nationality.
96
+ const nameDisclosures = issued.disclosures.filter(
97
+ d => d.claimName === 'given_name' || d.claimName === 'family_name'
98
+ );
99
+ const usDisclosure = issued.disclosures.find(
100
+ d => !d.claimName && d.claimValue === 'US'
101
+ );
102
+ const toReveal = [...nameDisclosures, usDisclosure].filter(Boolean).map(d => d.encoded);
103
+
104
+ console.log('Revealing:', nameDisclosures.map(d => d.claimName).join(', '), '+ US nationality');
105
+ console.log('Hiding: email, birthdate, DE nationality');
106
+
107
+ const presentation = sdJwtPresent(
108
+ issued.sdJwt,
109
+ toReveal,
110
+ holderJwk, // Holder's private key (for Key Binding JWT)
111
+ 'ES256',
112
+ 'https://verifier.example.com', // Audience
113
+ 'nonce-abc-123', // Nonce from verifier
114
+ );
115
+
116
+ console.log('\nPresentation token (truncated):');
117
+ console.log(' ' + presentation.presentation.substring(0, 80) + '...');
118
+
119
+ // ── 3. Verify ────────────────────────────────────────────────────────
120
+
121
+ separator('Step 3: Verifier checks the presentation');
122
+
123
+ const result = sdJwtVerify(
124
+ presentation.presentation,
125
+ issuerPublicJwk, // Issuer's public key
126
+ holderPublicJwk, // Holder's public key (verify Key Binding)
127
+ 'https://verifier.example.com', // Expected audience
128
+ 'nonce-abc-123', // Expected nonce
129
+ );
130
+
131
+ console.log('Verified:', result.verified);
132
+ console.log('Disclosed claims:', JSON.stringify(result.claims, null, 2));
133
+ console.log('');
134
+
135
+ // Show what the verifier can and cannot see
136
+ console.log('What the verifier sees:');
137
+ console.log(' iss:', result.claims.iss, '(always visible)');
138
+ console.log(' sub:', result.claims.sub, '(always visible)');
139
+ console.log(' given_name:', result.claims.given_name ?? '(hidden)');
140
+ console.log(' family_name:', result.claims.family_name ?? '(hidden)');
141
+ console.log(' email:', result.claims.email ?? '(hidden)');
142
+ console.log(' birthdate:', result.claims.birthdate ?? '(hidden)');
143
+ console.log(' nationalities:', JSON.stringify(result.claims.nationalities));
144
+
145
+ separator('Demo Complete');
146
+ console.log('SD-JWT provides selective disclosure with standard JWT infrastructure.');
147
+ console.log('No JSON-LD, no BLS keys, no linked data suites required.');
148
+ console.log('');
package/index.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface Bbs2023VerifyOutput {
9
9
  }
10
10
  export declare function bbs2023Sign(options: any): any
11
11
  export declare function bbs2023Derive(options: any): any
12
+ export declare function bbs2023HolderCommit(options: any): any
13
+ export declare function bbs2023BlindSign(options: any): any
12
14
  export declare function bbs2023Verify(document: any, publicKey?: string | undefined | null): Bbs2023VerifyOutput
13
15
  export interface BbsIetfKeyPair {
14
16
  /** Hex-encoded secret key (32 bytes) */
@@ -356,7 +358,7 @@ export declare function unblindSignature(blindSignature: Uint8Array, blindingFac
356
358
  export declare function deriveProofHolderBound(proofDocument: any, revealDocument: any, options: any): Promise<any>
357
359
  export interface SdJwtDisclosure {
358
360
  salt: string
359
- claimName: string
361
+ claimName?: string
360
362
  claimValue: any
361
363
  encoded: string
362
364
  digest: string
package/index.js CHANGED
@@ -310,10 +310,12 @@ if (!nativeBinding) {
310
310
  throw new Error(`Failed to load native binding`)
311
311
  }
312
312
 
313
- const { bbs2023Sign, bbs2023Derive, bbs2023Verify, bbsIetfKeygen, bbsIetfSign, bbsIetfVerify, bbsIetfProofGen, bbsIetfProofVerify, BbsBlsHolderBoundSignature2022, BbsBlsHolderBoundSignatureProof2022, BbsBlsSignature2020, BbsBlsSignatureProof2020, Bls12381G2KeyPair, KeyPairSigner, KeyPairVerifier, BoundBls12381G2KeyPair, JoseNamedCurve, JoseContentEncryption, JoseKeyEncryption, JoseSigningAlgorithm, generateJwk, generateKeyPair, joseEncrypt, joseDecrypt, generalEncryptJson, decryptJson, compactSignJson, compactJsonVerify, flattenedSignJson, jsonVerify, generalSignJson, JsonLd, ldSign, ldVerify, ldDeriveProof, deriveProof, createCommitment, verifyCommitment, unblindSignature, deriveProofHolderBound, sdJwtIssue, sdJwtPresent, sdJwtVerify } = nativeBinding
313
+ const { bbs2023Sign, bbs2023Derive, bbs2023HolderCommit, bbs2023BlindSign, bbs2023Verify, bbsIetfKeygen, bbsIetfSign, bbsIetfVerify, bbsIetfProofGen, bbsIetfProofVerify, BbsBlsHolderBoundSignature2022, BbsBlsHolderBoundSignatureProof2022, BbsBlsSignature2020, BbsBlsSignatureProof2020, Bls12381G2KeyPair, KeyPairSigner, KeyPairVerifier, BoundBls12381G2KeyPair, JoseNamedCurve, JoseContentEncryption, JoseKeyEncryption, JoseSigningAlgorithm, generateJwk, generateKeyPair, joseEncrypt, joseDecrypt, generalEncryptJson, decryptJson, compactSignJson, compactJsonVerify, flattenedSignJson, jsonVerify, generalSignJson, JsonLd, ldSign, ldVerify, ldDeriveProof, deriveProof, createCommitment, verifyCommitment, unblindSignature, deriveProofHolderBound, sdJwtIssue, sdJwtPresent, sdJwtVerify } = nativeBinding
314
314
 
315
315
  module.exports.bbs2023Sign = bbs2023Sign
316
316
  module.exports.bbs2023Derive = bbs2023Derive
317
+ module.exports.bbs2023HolderCommit = bbs2023HolderCommit
318
+ module.exports.bbs2023BlindSign = bbs2023BlindSign
317
319
  module.exports.bbs2023Verify = bbs2023Verify
318
320
  module.exports.bbsIetfKeygen = bbsIetfKeygen
319
321
  module.exports.bbsIetfSign = bbsIetfSign
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuggetslife/vc",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "napi": {
@@ -32,17 +32,17 @@
32
32
  "build": "napi build --platform --release",
33
33
  "build:debug": "napi build --platform",
34
34
  "prepublishOnly": "napi prepublish -t npm",
35
- "test": "node test.mjs && node test_jose.mjs && node test_sd_jwt.mjs && node test_jsonld_crossverify.mjs && node test_backward_compat.mjs",
35
+ "test": "node test.mjs && node test_jose.mjs && node test_sd_jwt.mjs && node test_bbs_ietf.mjs && node test_bbs_2023.mjs && node test_jsonld_crossverify.mjs && node test_backward_compat.mjs",
36
36
  "universal": "napi universal",
37
37
  "version": "napi version"
38
38
  },
39
39
  "packageManager": "yarn@4.3.1",
40
40
  "optionalDependencies": {
41
- "@nuggetslife/vc-darwin-arm64": "0.0.24",
42
- "@nuggetslife/vc-linux-arm64-gnu": "0.0.24",
43
- "@nuggetslife/vc-linux-arm64-musl": "0.0.24",
44
- "@nuggetslife/vc-linux-x64-gnu": "0.0.24",
45
- "@nuggetslife/vc-linux-x64-musl": "0.0.24"
41
+ "@nuggetslife/vc-darwin-arm64": "0.0.25",
42
+ "@nuggetslife/vc-linux-arm64-gnu": "0.0.25",
43
+ "@nuggetslife/vc-linux-arm64-musl": "0.0.25",
44
+ "@nuggetslife/vc-linux-x64-gnu": "0.0.25",
45
+ "@nuggetslife/vc-linux-x64-musl": "0.0.25"
46
46
  },
47
47
  "dependencies": {}
48
48
  }
package/src/bbs_2023.rs CHANGED
@@ -48,6 +48,45 @@ pub fn bbs2023_derive(options: Value) -> napi::Result<Value> {
48
48
  Ok(result)
49
49
  }
50
50
 
51
+ // ---------------------------------------------------------------------------
52
+ // bbs2023HolderCommit — holder creates Pedersen commitment for blind signing
53
+ // ---------------------------------------------------------------------------
54
+
55
+ #[napi(js_name = "bbs2023HolderCommit")]
56
+ pub fn bbs2023_holder_commit(options: Value) -> napi::Result<Value> {
57
+ let commit_opts: vc::bbs_2023::HolderCommitOptions = serde_json::from_value(options)
58
+ .map_err(|e| napi::Error::from_reason(format!("bbs2023HolderCommit parse options: {e}")))?;
59
+
60
+ let rt = tokio::runtime::Runtime::new()
61
+ .map_err(|e| napi::Error::from_reason(format!("bbs2023HolderCommit runtime: {e}")))?;
62
+
63
+ let result = rt
64
+ .block_on(vc::bbs_2023::create_holder_commitment(commit_opts))
65
+ .map_err(|e| napi::Error::from_reason(format!("bbs2023HolderCommit failed: {e}")))?;
66
+
67
+ serde_json::to_value(&result)
68
+ .map_err(|e| napi::Error::from_reason(format!("bbs2023HolderCommit serialize: {e}")))
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // bbs2023BlindSign — issuer creates blind base proof with holder's commitment
73
+ // ---------------------------------------------------------------------------
74
+
75
+ #[napi(js_name = "bbs2023BlindSign")]
76
+ pub fn bbs2023_blind_sign(options: Value) -> napi::Result<Value> {
77
+ let blind_opts: vc::bbs_2023::BlindSignOptions = serde_json::from_value(options)
78
+ .map_err(|e| napi::Error::from_reason(format!("bbs2023BlindSign parse options: {e}")))?;
79
+
80
+ let rt = tokio::runtime::Runtime::new()
81
+ .map_err(|e| napi::Error::from_reason(format!("bbs2023BlindSign runtime: {e}")))?;
82
+
83
+ let result = rt
84
+ .block_on(vc::bbs_2023::create_blind_base_proof(blind_opts))
85
+ .map_err(|e| napi::Error::from_reason(format!("bbs2023BlindSign failed: {e}")))?;
86
+
87
+ Ok(result)
88
+ }
89
+
51
90
  // ---------------------------------------------------------------------------
52
91
  // bbs2023Verify — verify a base or derived proof
53
92
  // ---------------------------------------------------------------------------
package/src/sd_jwt.rs CHANGED
@@ -11,7 +11,7 @@ use vc::sd_jwt::verifier::sd_jwt_verify;
11
11
  #[napi(object)]
12
12
  pub struct SdJwtDisclosure {
13
13
  pub salt: String,
14
- pub claim_name: String,
14
+ pub claim_name: Option<String>,
15
15
  pub claim_value: Value,
16
16
  pub encoded: String,
17
17
  pub digest: String,
package/test_bbs_2023.mjs CHANGED
@@ -1,12 +1,19 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
3
6
  import {
4
7
  bbsIetfKeygen,
5
8
  bbs2023Sign,
6
9
  bbs2023Derive,
7
10
  bbs2023Verify,
11
+ bbs2023HolderCommit,
12
+ bbs2023BlindSign,
8
13
  } from './index.js';
9
14
 
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+
10
17
  // =====================================================================
11
18
  // Helpers
12
19
  // =====================================================================
@@ -193,3 +200,228 @@ test('bbs-2023: sign with no mandatory pointers', () => {
193
200
  const result = bbs2023Verify(signed, kp.publicKey);
194
201
  assert.ok(result.verified, 'base proof with no mandatory pointers verified');
195
202
  });
203
+
204
+ // =====================================================================
205
+ // PRC (PermanentResidentCard) — Real JSON-LD VC Tests
206
+ // =====================================================================
207
+
208
+ function loadPRC() {
209
+ const raw = readFileSync(join(__dirname, 'test-data', 'inputDocument.json'), 'utf8');
210
+ return JSON.parse(raw);
211
+ }
212
+
213
+ test('bbs-2023: PRC sign + verify base proof', () => {
214
+ const kp = makeKeyPair();
215
+ const doc = loadPRC();
216
+
217
+ const signed = bbs2023Sign({
218
+ document: doc,
219
+ secretKey: kp.secretKey,
220
+ publicKey: kp.publicKey,
221
+ mandatoryPointers: ['/issuer', '/issuanceDate'],
222
+ verificationMethod: 'did:example:489398593#bbs-key-1',
223
+ proofPurpose: 'assertionMethod',
224
+ });
225
+
226
+ assert.ok(signed.proof, 'PRC signed document has proof');
227
+ assert.equal(signed.proof.cryptosuite, 'bbs-2023');
228
+
229
+ const result = bbs2023Verify(signed, kp.publicKey);
230
+ assert.ok(result.verified, 'PRC base proof verified');
231
+ assert.equal(result.error, null);
232
+ });
233
+
234
+ test('bbs-2023: PRC sign → derive → verify', () => {
235
+ const kp = makeKeyPair();
236
+ const doc = loadPRC();
237
+
238
+ const signed = bbs2023Sign({
239
+ document: doc,
240
+ secretKey: kp.secretKey,
241
+ publicKey: kp.publicKey,
242
+ mandatoryPointers: ['/issuer', '/issuanceDate'],
243
+ });
244
+
245
+ const derived = bbs2023Derive({
246
+ document: signed,
247
+ selectivePointers: ['/credentialSubject/givenName', '/credentialSubject/familyName'],
248
+ });
249
+
250
+ assert.ok(derived.proof, 'PRC derived document has proof');
251
+
252
+ const result = bbs2023Verify(derived, kp.publicKey);
253
+ assert.ok(result.verified, 'PRC derived proof verified');
254
+ assert.equal(result.error, null);
255
+ });
256
+
257
+ test('bbs-2023: PRC derive hiding most fields', () => {
258
+ const kp = makeKeyPair();
259
+ const doc = loadPRC();
260
+
261
+ const signed = bbs2023Sign({
262
+ document: doc,
263
+ secretKey: kp.secretKey,
264
+ publicKey: kp.publicKey,
265
+ mandatoryPointers: ['/issuer', '/issuanceDate', '/credentialSubject/type'],
266
+ });
267
+
268
+ // Only reveal lprNumber beyond mandatory
269
+ const derived = bbs2023Derive({
270
+ document: signed,
271
+ selectivePointers: ['/credentialSubject/lprNumber'],
272
+ });
273
+
274
+ const result = bbs2023Verify(derived, kp.publicKey);
275
+ assert.ok(result.verified, 'PRC minimal-reveal derived proof verified');
276
+ assert.equal(result.error, null);
277
+ });
278
+
279
+ // =====================================================================
280
+ // Blind BBS Signing Tests
281
+ // =====================================================================
282
+
283
+ test('bbs-2023: blind sign → derive → verify roundtrip', () => {
284
+ const kp = makeKeyPair();
285
+ const doc = sampleCredential();
286
+
287
+ // 1. Holder creates commitment for their secret fields
288
+ const commitment = bbs2023HolderCommit({
289
+ document: doc,
290
+ publicKey: kp.publicKey,
291
+ mandatoryPointers: ['/issuer', '/issuanceDate'],
292
+ committedPointers: ['/credentialSubject/email'],
293
+ });
294
+
295
+ assert.ok(commitment.commitment, 'commitment is present');
296
+ assert.ok(commitment.blindFactor, 'blindFactor is present');
297
+ assert.ok(commitment.hmacKey, 'hmacKey is present');
298
+ assert.ok(Array.isArray(commitment.committedIndices), 'committedIndices is array');
299
+
300
+ // 2. Issuer blind-signs with the holder's commitment
301
+ const signed = bbs2023BlindSign({
302
+ document: doc,
303
+ secretKey: kp.secretKey,
304
+ publicKey: kp.publicKey,
305
+ commitment: commitment.commitment,
306
+ hmacKey: commitment.hmacKey,
307
+ committedIndices: commitment.committedIndices,
308
+ mandatoryPointers: ['/issuer', '/issuanceDate'],
309
+ verificationMethod: 'did:example:issuer#bbs-key-1',
310
+ });
311
+
312
+ assert.ok(signed.proof, 'blind signed document has proof');
313
+ assert.equal(signed.proof.cryptosuite, 'bbs-2023');
314
+
315
+ // 3. Holder derives selective disclosure (must provide blindFactor for blind-signed proofs)
316
+ const derived = bbs2023Derive({
317
+ document: signed,
318
+ selectivePointers: ['/credentialSubject/name'],
319
+ blindFactor: commitment.blindFactor,
320
+ });
321
+
322
+ assert.ok(derived.proof, 'derived document has proof');
323
+
324
+ // 4. Verifier verifies
325
+ const result = bbs2023Verify(derived, kp.publicKey);
326
+ assert.ok(result.verified, 'blind-signed derived proof verified');
327
+ assert.equal(result.error, null);
328
+ });
329
+
330
+ test('bbs-2023: blind sign with signer_blind', () => {
331
+ const kp = makeKeyPair();
332
+ const doc = sampleCredential();
333
+
334
+ const commitment = bbs2023HolderCommit({
335
+ document: doc,
336
+ publicKey: kp.publicKey,
337
+ mandatoryPointers: ['/issuer'],
338
+ committedPointers: ['/credentialSubject/age'],
339
+ });
340
+
341
+ // Use a signer_blind (32 bytes as hex)
342
+ const signerBlind = '0000000000000000000000000000000000000000000000000000000000000001';
343
+
344
+ const signed = bbs2023BlindSign({
345
+ document: doc,
346
+ secretKey: kp.secretKey,
347
+ publicKey: kp.publicKey,
348
+ commitment: commitment.commitment,
349
+ hmacKey: commitment.hmacKey,
350
+ committedIndices: commitment.committedIndices,
351
+ mandatoryPointers: ['/issuer'],
352
+ signerBlind,
353
+ });
354
+
355
+ assert.ok(signed.proof, 'blind signed with signerBlind has proof');
356
+
357
+ const derived = bbs2023Derive({
358
+ document: signed,
359
+ selectivePointers: ['/credentialSubject/name'],
360
+ blindFactor: commitment.blindFactor,
361
+ signerBlind,
362
+ });
363
+
364
+ const result = bbs2023Verify(derived, kp.publicKey);
365
+ assert.ok(result.verified, 'blind-signed with signerBlind derived proof verified');
366
+ });
367
+
368
+ test('bbs-2023: blind sign — wrong key fails verification', () => {
369
+ const kp = makeKeyPair();
370
+ const wrongKp = makeKeyPair();
371
+ const doc = sampleCredential();
372
+
373
+ const commitment = bbs2023HolderCommit({
374
+ document: doc,
375
+ publicKey: kp.publicKey,
376
+ mandatoryPointers: ['/issuer'],
377
+ committedPointers: ['/credentialSubject/email'],
378
+ });
379
+
380
+ const signed = bbs2023BlindSign({
381
+ document: doc,
382
+ secretKey: kp.secretKey,
383
+ publicKey: kp.publicKey,
384
+ commitment: commitment.commitment,
385
+ hmacKey: commitment.hmacKey,
386
+ committedIndices: commitment.committedIndices,
387
+ mandatoryPointers: ['/issuer'],
388
+ });
389
+
390
+ const result = bbs2023Verify(signed, wrongKp.publicKey);
391
+ assert.equal(result.verified, false, 'should not verify with wrong key');
392
+ });
393
+
394
+ test('bbs-2023: blind sign with multiple committed fields', () => {
395
+ const kp = makeKeyPair();
396
+ const doc = sampleCredential();
397
+
398
+ const commitment = bbs2023HolderCommit({
399
+ document: doc,
400
+ publicKey: kp.publicKey,
401
+ mandatoryPointers: ['/issuer'],
402
+ committedPointers: ['/credentialSubject/email', '/credentialSubject/age'],
403
+ });
404
+
405
+ assert.ok(commitment.committedIndices.length >= 1, 'at least 1 committed index');
406
+
407
+ const signed = bbs2023BlindSign({
408
+ document: doc,
409
+ secretKey: kp.secretKey,
410
+ publicKey: kp.publicKey,
411
+ commitment: commitment.commitment,
412
+ hmacKey: commitment.hmacKey,
413
+ committedIndices: commitment.committedIndices,
414
+ mandatoryPointers: ['/issuer'],
415
+ });
416
+
417
+ assert.ok(signed.proof, 'signed document with multiple committed fields');
418
+
419
+ const derived = bbs2023Derive({
420
+ document: signed,
421
+ selectivePointers: ['/credentialSubject/name', '/credentialSubject/alumniOf'],
422
+ blindFactor: commitment.blindFactor,
423
+ });
424
+
425
+ const result = bbs2023Verify(derived, kp.publicKey);
426
+ assert.ok(result.verified, 'multi-committed blind-signed proof verified');
427
+ });
package/test_sd_jwt.mjs CHANGED
@@ -195,3 +195,94 @@ test('SD-JWT with decoy digests', () => {
195
195
  assert.ok(verified.verified);
196
196
  assert.equal(verified.claims.name, 'Alice');
197
197
  });
198
+
199
+ // =====================================================================
200
+ // Array Element Disclosure
201
+ // =====================================================================
202
+
203
+ test('SD-JWT array element disclosure roundtrip', () => {
204
+ const issuer = makeKeyPair();
205
+
206
+ const claims = {
207
+ sub: 'user123',
208
+ nationalities: ['US', 'DE', 'FR'],
209
+ };
210
+ const result = sdJwtIssue(
211
+ claims,
212
+ ['nationalities[0]', 'nationalities[1]', 'nationalities[2]'],
213
+ issuer.privateJwk,
214
+ 'ES256',
215
+ );
216
+
217
+ assert.equal(result.disclosures.length, 3, 'three array disclosures');
218
+ // Array element disclosures have null claimName
219
+ for (const d of result.disclosures) {
220
+ assert.equal(d.claimName, null, 'array disclosure has null claimName');
221
+ }
222
+
223
+ // Present all disclosures
224
+ const allEncoded = result.disclosures.map(d => d.encoded);
225
+ const presentation = sdJwtPresent(result.sdJwt, allEncoded);
226
+ const verified = sdJwtVerify(presentation.presentation, issuer.publicJwk);
227
+
228
+ assert.ok(verified.verified);
229
+ assert.deepStrictEqual(verified.claims.nationalities, ['US', 'DE', 'FR']);
230
+ });
231
+
232
+ test('SD-JWT array element selective reveal', () => {
233
+ const issuer = makeKeyPair();
234
+
235
+ const claims = {
236
+ sub: 'user123',
237
+ nationalities: ['US', 'DE', 'FR'],
238
+ };
239
+ const result = sdJwtIssue(
240
+ claims,
241
+ ['nationalities[0]', 'nationalities[1]', 'nationalities[2]'],
242
+ issuer.privateJwk,
243
+ 'ES256',
244
+ );
245
+
246
+ // Only reveal "US"
247
+ const usDisclosure = result.disclosures.find(d =>
248
+ JSON.stringify(d.claimValue) === '"US"'
249
+ );
250
+ const presentation = sdJwtPresent(result.sdJwt, [usDisclosure.encoded]);
251
+ const verified = sdJwtVerify(presentation.presentation, issuer.publicJwk);
252
+
253
+ assert.ok(verified.verified);
254
+ assert.deepStrictEqual(
255
+ verified.claims.nationalities,
256
+ ['US'],
257
+ 'only disclosed element present',
258
+ );
259
+ });
260
+
261
+ test('SD-JWT mixed object and array disclosure', () => {
262
+ const issuer = makeKeyPair();
263
+
264
+ const claims = {
265
+ sub: 'user123',
266
+ name: 'Alice',
267
+ nationalities: ['US', 'DE'],
268
+ age: 30,
269
+ };
270
+ const result = sdJwtIssue(
271
+ claims,
272
+ ['name', 'nationalities[0]', 'nationalities[1]'],
273
+ issuer.privateJwk,
274
+ 'ES256',
275
+ );
276
+
277
+ assert.equal(result.disclosures.length, 3, 'three disclosures total');
278
+
279
+ // Present all
280
+ const allEncoded = result.disclosures.map(d => d.encoded);
281
+ const presentation = sdJwtPresent(result.sdJwt, allEncoded);
282
+ const verified = sdJwtVerify(presentation.presentation, issuer.publicJwk);
283
+
284
+ assert.ok(verified.verified);
285
+ assert.equal(verified.claims.name, 'Alice');
286
+ assert.equal(verified.claims.age, 30, 'non-disclosable always present');
287
+ assert.deepStrictEqual(verified.claims.nationalities, ['US', 'DE']);
288
+ });