Skip to content

Commit 82dfdd0

Browse files
authored
Add selective disclosure for JWT credentials (#96)
- Add SD support for FE-sized attribs - Attributes that are larger than a field element can be hashed to fit, then disclosed by revealing the preimage. Completes the core work for selective disclosure. Added support for basic proof specifications encoding a description of attribute data to disclose. - Callers do not have to specify whether a revaled attribute is hashed or not, we can figure that out from the config.json file. - The Groth16 params were being saved twice, once on their own and once in the prover params, as they can be large, we only save them once in the prover params. - Add "prepare" option to command line tool, as a synonym for "prove" - Move presentation message into proof spec - Return Result when creating show proof - Fix JWT samples after API changes. Leaves the functionality of the samples unchanged, just updates them to use the updated Crescent API. -Handle presentation message: Make it a string in the public ProofSpec, convert to byte array in ProofSpecInternal. In CLI tool, allow only one, either from command line, or proof spec file - Add rs256-sd to github CI - Move bls12-381 dependency to dev dependencies, currently only used for tests - Update readme explaining selective disclosure functionality
1 parent 8e901b0 commit 82dfdd0

File tree

27 files changed

+796
-180
lines changed

27 files changed

+796
-180
lines changed

.github/workflows/CI.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ jobs:
5353
cd circuit_setup/scripts
5454
./run_setup.sh rs256
5555
56+
- name: Run circuit setup for rs256-sd
57+
run: |
58+
cd circuit_setup/scripts
59+
./run_setup.sh rs256-sd
60+
61+
5662
- name: Run circuit setup for mDL
5763
run: |
5864
cd circuit_setup/scripts
@@ -89,6 +95,27 @@ jobs:
8995
cd creds
9096
cargo run --bin crescent --release --features print-trace verify --name rs256
9197
98+
# RS256-sd Commands
99+
- name: Run ZKSetup for rs256-sd
100+
run: |
101+
cd creds
102+
cargo run --bin crescent --release --features print-trace zksetup --name rs256-sd
103+
104+
- name: Run Prove for rs256-sd
105+
run: |
106+
cd creds
107+
cargo run --bin crescent --release --features print-trace prove --name rs256-sd
108+
109+
- name: Run Show for rs256-sd
110+
run: |
111+
cd creds
112+
cargo run --bin crescent --release --features print-trace show --name rs256-sd
113+
114+
- name: Run Verify for rs256-sd
115+
run: |
116+
cd creds
117+
cargo run --bin crescent --release --features print-trace verify --name rs256-sd
118+
92119
# mDL Commands
93120
- name: Run ZKSetup for mDL
94121
run: |

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ The `--name` parameter, used in circuit setup and with the command-line tool, sp
8585

8686
Note that the steps have to be run in order, but once the client state is created by `prove`, the `show` and `verify` steps can be run repeatedly.
8787

88+
### Selective Disclosure
89+
The demo generates proofs of fixed statements, for the `rs256` example, the domain of the email address is revealed to the verifier, and for `mdl` the statement is that the holder's age is greater than 18. By default Crescent also proves that the credential is not expired.
90+
91+
The `rs256-sd` example demonstrates how to disclose a subset of the attributes in a credential. The file `creds/test-vectors/rs256-sd/proof_spec.json` contains
92+
```
93+
{
94+
"revealed" : ["family_name", "tenant_ctry", "auth_time", "aud"]
95+
}
96+
```
97+
which means that the proof will disclose those attributes to the verifier. The subset of the attributes that may be revealed in this way is limited to those in `circuit_setup/inputs/rs256-sd/config.json` that have the `reveal` or `reveal_digest` boolean set to `true`.
98+
The `reveal_digest` option is used for values that may be larger than 31 bytes; they will get hashed first. Setting this flag changes how the circuit setup phase handles those attributes, allowing them to be optionally revealed during `show`.
99+
100+
As example ways to experiment with selective disclosure, try removing `aud` from the list of revealed attributes, or adding `given_name` to the list of revealed attributes in the proof specification file.
101+
88102
## Contributing
89103

90104
This project welcomes contributions and suggestions. Most contributions require you to agree to a

circuit_setup/circuits/match_claim.circom

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ template RevealClaimValueBytes(msg_json_len, claim_byte_len, field_byte_len, is_
171171
c += tmp_prod1[i][j] * json_bytes[j];
172172
}
173173
value[i] <-- c;
174-
log(c);
175174
}
176175

177176
component match_substring = MatchSubstring(msg_json_len, claim_byte_len, field_byte_len);
@@ -210,13 +209,9 @@ template RevealClaimValue(msg_json_len, claim_byte_len, field_byte_len, is_numbe
210209
signal intermediate_value[claim_byte_len];
211210

212211
intermediate_value[0] <== reveal_claim.value[0];
213-
log("reveal_claim.value[0] = ", reveal_claim.value[0]);
214-
log("intermediate_value[0] = ", intermediate_value[0]);
215212
var pow256 = 256;
216213
for(var i = 1; i < claim_byte_len; i++) {
217-
log("reveal_claim.value[", i, "] = ", reveal_claim.value[i]);
218214
intermediate_value[i] <== intermediate_value[i-1] + reveal_claim.value[i] * pow256;
219-
log("intermediate_value[", i, "] = ", intermediate_value[i]);
220215
pow256 = pow256*256;
221216
}
222217
value <== intermediate_value[claim_byte_len-1];
@@ -287,26 +282,144 @@ template RevealDomainOnly(msg_json_len, claim_byte_len, field_byte_len, is_numbe
287282
value <== intermediate_value[claim_byte_len-2];
288283
}
289284

285+
template IsZeroMod64(n) {
286+
signal input in;
287+
288+
component n2b = Num2Bits(n);
289+
n2b.in <== in;
290+
for(var i = 0; i < 6; i++) {
291+
n2b.out[i] === 0;
292+
}
293+
}
294+
295+
template CalculatePadding(){
296+
297+
signal input data_len_bytes;
298+
signal output padding_zero_bytes;
299+
300+
// We start by calculating the number of padding bytes required as a var
301+
// then convert that into a 6-bit integer (the max number of zeroes we add is 55).
302+
// Then we convert the bits to a number, and enforce that it's small enough.
303+
304+
var padding_zero_bytes_var = ((data_len_bytes + 1 + 8 + 63)\64)*64 - (data_len_bytes + 1 + 8);
305+
var BITLEN = 6;
306+
var padding_zero_bytes_as_bits[BITLEN];
307+
for(var i = 0; i < BITLEN; i++ ) {
308+
padding_zero_bytes_as_bits[i] = padding_zero_bytes_var >> i & 1;
309+
}
310+
signal pzbb[BITLEN] <-- padding_zero_bytes_as_bits;
311+
for( var i = 0; i < BITLEN; i++) {
312+
(1 - pzbb[i]) * pzbb[i] === 0; // pzbb[i] is a bit
313+
}
314+
315+
component b2n_pad = Bits2Num(BITLEN);
316+
b2n_pad.in <== pzbb;
317+
signal pzb <== b2n_pad.out;
318+
319+
component pzb_check = LessEqThan(BITLEN);
320+
pzb_check.in[0] <== pzb;
321+
pzb_check.in[1] <== 55;
322+
pzb_check.out === 1;
323+
324+
padding_zero_bytes <== pzb;
325+
}
326+
290327
// Hash and Reveal the claim value with claim_byte_len as the maximum length.
291-
template HashRevealClaimValue(msg_json_len, claim_byte_len, field_byte_len, is_number) {
328+
// We do not assume that the claim length is public (only the max)
329+
template HashRevealClaimValue(msg_json_len, max_claim_byte_len, field_byte_len, is_number) {
292330
signal input json_bytes[msg_json_len];
293331
signal input l;
294332
signal input r;
333+
signal output digest;
295334

296-
component reveal_claim = RevealClaimValueBytes(msg_json_len, claim_byte_len, field_byte_len, is_number);
335+
component reveal_claim = RevealClaimValueBytes(msg_json_len, max_claim_byte_len, field_byte_len, is_number);
297336
reveal_claim.json_bytes <== json_bytes;
298337
reveal_claim.l <== l;
299338
reveal_claim.r <== r;
300-
301-
signal output digest;
302339

303-
component mimc_hash = MultiMiMC7(claim_byte_len, 10);
304-
for(var i=0; i<claim_byte_len; i++) {
305-
mimc_hash.in[i] <== reveal_claim.value[i];
340+
// Compute SHA-256 hash of the revealed claim, and truncate it 248-bits so that it fits in a field element
341+
// We need to use the Sha256General gadget (rather than Sha256Bytes) since we need to apply padding here
342+
// Handy site for debugging: https://stepansnigirev.github.io/visual-sha256/
343+
var n_blocks = ((max_claim_byte_len*8 + 1 + 64)\512)+1;
344+
var max_bits_padded = n_blocks * 512;
345+
var max_bytes_padded = max_bits_padded\8;
346+
component sha256 = Sha256General(max_bits_padded);
347+
348+
signal data_len_bytes <== (r - l);
349+
component calculate_padding = CalculatePadding();
350+
calculate_padding.data_len_bytes <== data_len_bytes;
351+
signal padding_zero_bytes <== calculate_padding.padding_zero_bytes;
352+
signal data_len_padded_bytes <== data_len_bytes + 1 + 8 + padding_zero_bytes;
353+
//Enforce data_len_padded_bytes mod 64 = 0 :
354+
component mod64check = IsZeroMod64(32);
355+
mod64check.in <== data_len_padded_bytes;
356+
357+
component padding_indicator = IntervalIndicator(max_bytes_padded);
358+
padding_indicator.l <== data_len_bytes;
359+
padding_indicator.r <== data_len_padded_bytes;
360+
361+
signal padded0[max_bytes_padded];
362+
for(var i = 0; i < max_claim_byte_len; i++){
363+
padded0[i] <== reveal_claim.value[i];
364+
}
365+
for(var i = max_claim_byte_len; i < max_bytes_padded; i++) {
366+
padded0[i] <== 0;
367+
}
368+
369+
// add the byte 128 = 10000000_b
370+
signal padded1[max_bytes_padded];
371+
for(var i = 0; i < max_bytes_padded; i++) {
372+
padded1[i] <== padded0[i] * (1 - padding_indicator.start_indicator[i]) + 128 * padding_indicator.start_indicator[i];
373+
}
374+
375+
// Represent the data length in bits as a 64-bit integer, then convert to bytes
376+
signal data_len_bits <== data_len_bytes*8;
377+
component len_bits = Num2Bits(64);
378+
len_bits.in <== data_len_bits;
379+
component len_byte[8];
380+
signal len_bytes[8];
381+
for(var i = 0; i < 8; i++) {
382+
len_byte[i] = Bits2Num(8);
383+
for(var j = 0; j < 8; j++) {
384+
len_byte[i].in[j] <== len_bits.out[8*i + j];
385+
}
386+
len_bytes[i] <== len_byte[i].out;
387+
}
388+
389+
// Place each length byte at the end of the padded data
390+
signal padded2[8][max_bytes_padded];
391+
component length_indicator[8];
392+
for(var i = 0; i < 8; i++) {
393+
length_indicator[i] = PointIndicator(max_bytes_padded);
394+
length_indicator[i].l <== data_len_padded_bytes - 8 + i;
395+
396+
for(var j = 0; j < max_bytes_padded; j++) {
397+
if(i == 0) {
398+
padded2[i][j] <== length_indicator[i].indicator[j] * len_bytes[7-i] + padded1[j];
399+
} else {
400+
padded2[i][j] <== length_indicator[i].indicator[j] * len_bytes[7-i] + padded2[i-1][j];
401+
}
402+
}
403+
}
404+
signal padded[max_bytes_padded] <== padded2[7];
405+
406+
// Converts bytes to bits and input to SHA gadget
407+
component bits[max_bytes_padded];
408+
for (var i = 0; i < max_bytes_padded; i++) {
409+
bits[i] = Num2Bits(8);
410+
bits[i].in <== padded[i];
411+
for (var j = 0; j < 8; j++) {
412+
sha256.paddedIn[i*8+j] <== bits[i].out[7-j];
413+
}
414+
}
415+
sha256.in_len_padded_bits <== data_len_padded_bytes*8;
416+
417+
component b2n = Bits2Num(248);
418+
for(var i = 0; i < 248; i++) {
419+
b2n.in[i] <== sha256.out[i];
306420
}
307-
mimc_hash.k <== 0;
308421

309-
digest <== mimc_hash.out;
422+
digest <== b2n.out;
310423
}
311424

312425
// Generates constraints to enforce that `msg` has the substring `substr` starting at position `l` and ending at position `r`

circuit_setup/circuits/utils/utils.circom

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ template CalculateTotal(n) {
7878
sum <== sums[n - 1];
7979
}
8080

81-
// Written by us
8281
// n bytes per signal, n = 31 usually
8382
template Packed2Bytes(n){
8483
signal input in; // < 2 ^ (8 * 31)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"acct": 0,
3+
"aud": "12345678-1234-abcd-1234-abcdef124567",
4+
"auth_time": 1725917899,
5+
"email": "matthew@example.com",
6+
"exp": 1759517346,
7+
"family_name": "Matthew",
8+
"given_name": "Matthewson",
9+
"iat": 1728067746,
10+
"ipaddr": "203.0.113.0",
11+
"iss": "https://login.microsoftonline.com/12345678-1234-abcd-1234-abcdef124567/v2.0",
12+
"jti": "AUJNzY3Cwon7pL_3k0-fdw",
13+
"login_hint": "O.aaaaabbbbbbbbbcccccccdddddddeeeeeeeffffffgggggggghhhhhhiiiiiiijjjjjjjkkkkkkklllllllmmmmmmnnnnnnnnnnooooooopppppppqqqqrrrrrrsssssdddd",
14+
"name": "Matthew Matthewson",
15+
"nbf": 1728067746,
16+
"oid": "12345678-1234-abcd-1234-abcdef124567",
17+
"onprem_sid": "S-1-2-34-5678901234-1234567890-1234567890-1234567",
18+
"preferred_username": "matthew@example.com",
19+
"rh": "0.aaaaabbbbbccccddddeeeffff12345gggg12345_124_aaaaaaa.",
20+
"sid": "12345678-1234-abcd-1234-abcdef124567",
21+
"sub": "aaabbbbccccddddeeeeffffgggghhhh123456789012",
22+
"tenant_ctry": "US",
23+
"tenant_region_scope": "WW",
24+
"tid": "12345678-1234-abcd-1234-abcdef124567",
25+
"upn": "matthew@example.com",
26+
"uti": "AAABBBBccccdddd1234567",
27+
"ver": "2.0",
28+
"verified_primary_email": [
29+
"matthew@example.com"
30+
],
31+
"verified_secondary_email": [
32+
"matthew@service.example.com"
33+
],
34+
"xms_pdl": "NAM",
35+
"xms_tpl": "en"
36+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"alg": "RS256",
3+
"exp": {
4+
"type" : "number",
5+
"reveal" : true,
6+
"max_claim_byte_len" : 31
7+
},
8+
"email": {
9+
"type" : "string",
10+
"reveal" : true,
11+
"max_claim_byte_len" : 31
12+
},
13+
"family_name": {
14+
"type" : "string",
15+
"reveal" : true,
16+
"max_claim_byte_len" : 31
17+
},
18+
"given_name": {
19+
"type" : "string",
20+
"reveal" : true,
21+
"max_claim_byte_len" : 31
22+
},
23+
"tenant_ctry": {
24+
"type" : "string",
25+
"reveal" : true,
26+
"max_claim_byte_len" : 31
27+
},
28+
"tenant_region_scope": {
29+
"type" : "string",
30+
"reveal" : true,
31+
"max_claim_byte_len" : 31
32+
},
33+
"aud": {
34+
"type" : "string",
35+
"reveal_digest" : true,
36+
"max_claim_byte_len" : 62
37+
},
38+
"auth_time": {
39+
"type" : "number",
40+
"reveal_digest" : true,
41+
"max_claim_byte_len" : 31
42+
}
43+
44+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"revealed" : ["family_name", "tenant_ctry", "auth_time", "aud"]
3+
}

0 commit comments

Comments
 (0)