@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.
- package/demo_bbs_2023.mjs +203 -0
- package/demo_bbs_ietf.mjs +193 -0
- package/demo_sd_jwt.mjs +148 -0
- package/index.d.ts +3 -1
- package/index.js +3 -1
- package/package.json +7 -7
- package/src/bbs_2023.rs +39 -0
- package/src/sd_jwt.rs +1 -1
- package/test_bbs_2023.mjs +232 -0
- package/test_sd_jwt.mjs +91 -0
|
@@ -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('');
|
package/demo_sd_jwt.mjs
ADDED
|
@@ -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
|
|
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.
|
|
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.
|
|
42
|
-
"@nuggetslife/vc-linux-arm64-gnu": "0.0.
|
|
43
|
-
"@nuggetslife/vc-linux-arm64-musl": "0.0.
|
|
44
|
-
"@nuggetslife/vc-linux-x64-gnu": "0.0.
|
|
45
|
-
"@nuggetslife/vc-linux-x64-musl": "0.0.
|
|
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
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
|
+
});
|