Skip to content

Commit

Permalink
Add selective disclosure for JWT credentials (#96)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
zaverucha authored Feb 20, 2025
1 parent 8e901b0 commit 82dfdd0
Showing 27 changed files with 796 additions and 180 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -53,6 +53,12 @@ jobs:
cd circuit_setup/scripts
./run_setup.sh rs256
- name: Run circuit setup for rs256-sd
run: |
cd circuit_setup/scripts
./run_setup.sh rs256-sd
- name: Run circuit setup for mDL
run: |
cd circuit_setup/scripts
@@ -89,6 +95,27 @@ jobs:
cd creds
cargo run --bin crescent --release --features print-trace verify --name rs256
# RS256-sd Commands
- name: Run ZKSetup for rs256-sd
run: |
cd creds
cargo run --bin crescent --release --features print-trace zksetup --name rs256-sd
- name: Run Prove for rs256-sd
run: |
cd creds
cargo run --bin crescent --release --features print-trace prove --name rs256-sd
- name: Run Show for rs256-sd
run: |
cd creds
cargo run --bin crescent --release --features print-trace show --name rs256-sd
- name: Run Verify for rs256-sd
run: |
cd creds
cargo run --bin crescent --release --features print-trace verify --name rs256-sd
# mDL Commands
- name: Run ZKSetup for mDL
run: |
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -85,6 +85,20 @@ The `--name` parameter, used in circuit setup and with the command-line tool, sp

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.

### Selective Disclosure
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.

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
```
{
"revealed" : ["family_name", "tenant_ctry", "auth_time", "aud"]
}
```
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`.
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`.

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.

## Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a
141 changes: 127 additions & 14 deletions circuit_setup/circuits/match_claim.circom
Original file line number Diff line number Diff line change
@@ -171,7 +171,6 @@ template RevealClaimValueBytes(msg_json_len, claim_byte_len, field_byte_len, is_
c += tmp_prod1[i][j] * json_bytes[j];
}
value[i] <-- c;
log(c);
}

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
signal intermediate_value[claim_byte_len];

intermediate_value[0] <== reveal_claim.value[0];
log("reveal_claim.value[0] = ", reveal_claim.value[0]);
log("intermediate_value[0] = ", intermediate_value[0]);
var pow256 = 256;
for(var i = 1; i < claim_byte_len; i++) {
log("reveal_claim.value[", i, "] = ", reveal_claim.value[i]);
intermediate_value[i] <== intermediate_value[i-1] + reveal_claim.value[i] * pow256;
log("intermediate_value[", i, "] = ", intermediate_value[i]);
pow256 = pow256*256;
}
value <== intermediate_value[claim_byte_len-1];
@@ -287,26 +282,144 @@ template RevealDomainOnly(msg_json_len, claim_byte_len, field_byte_len, is_numbe
value <== intermediate_value[claim_byte_len-2];
}

template IsZeroMod64(n) {
signal input in;

component n2b = Num2Bits(n);
n2b.in <== in;
for(var i = 0; i < 6; i++) {
n2b.out[i] === 0;
}
}

template CalculatePadding(){

signal input data_len_bytes;
signal output padding_zero_bytes;

// We start by calculating the number of padding bytes required as a var
// then convert that into a 6-bit integer (the max number of zeroes we add is 55).
// Then we convert the bits to a number, and enforce that it's small enough.

var padding_zero_bytes_var = ((data_len_bytes + 1 + 8 + 63)\64)*64 - (data_len_bytes + 1 + 8);
var BITLEN = 6;
var padding_zero_bytes_as_bits[BITLEN];
for(var i = 0; i < BITLEN; i++ ) {
padding_zero_bytes_as_bits[i] = padding_zero_bytes_var >> i & 1;
}
signal pzbb[BITLEN] <-- padding_zero_bytes_as_bits;
for( var i = 0; i < BITLEN; i++) {
(1 - pzbb[i]) * pzbb[i] === 0; // pzbb[i] is a bit
}

component b2n_pad = Bits2Num(BITLEN);
b2n_pad.in <== pzbb;
signal pzb <== b2n_pad.out;

component pzb_check = LessEqThan(BITLEN);
pzb_check.in[0] <== pzb;
pzb_check.in[1] <== 55;
pzb_check.out === 1;

padding_zero_bytes <== pzb;
}

// Hash and Reveal the claim value with claim_byte_len as the maximum length.
template HashRevealClaimValue(msg_json_len, claim_byte_len, field_byte_len, is_number) {
// We do not assume that the claim length is public (only the max)
template HashRevealClaimValue(msg_json_len, max_claim_byte_len, field_byte_len, is_number) {
signal input json_bytes[msg_json_len];
signal input l;
signal input r;
signal output digest;

component reveal_claim = RevealClaimValueBytes(msg_json_len, claim_byte_len, field_byte_len, is_number);
component reveal_claim = RevealClaimValueBytes(msg_json_len, max_claim_byte_len, field_byte_len, is_number);
reveal_claim.json_bytes <== json_bytes;
reveal_claim.l <== l;
reveal_claim.r <== r;

signal output digest;

component mimc_hash = MultiMiMC7(claim_byte_len, 10);
for(var i=0; i<claim_byte_len; i++) {
mimc_hash.in[i] <== reveal_claim.value[i];
// Compute SHA-256 hash of the revealed claim, and truncate it 248-bits so that it fits in a field element
// We need to use the Sha256General gadget (rather than Sha256Bytes) since we need to apply padding here
// Handy site for debugging: https://stepansnigirev.github.io/visual-sha256/
var n_blocks = ((max_claim_byte_len*8 + 1 + 64)\512)+1;
var max_bits_padded = n_blocks * 512;
var max_bytes_padded = max_bits_padded\8;
component sha256 = Sha256General(max_bits_padded);

signal data_len_bytes <== (r - l);
component calculate_padding = CalculatePadding();
calculate_padding.data_len_bytes <== data_len_bytes;
signal padding_zero_bytes <== calculate_padding.padding_zero_bytes;
signal data_len_padded_bytes <== data_len_bytes + 1 + 8 + padding_zero_bytes;
//Enforce data_len_padded_bytes mod 64 = 0 :
component mod64check = IsZeroMod64(32);
mod64check.in <== data_len_padded_bytes;

component padding_indicator = IntervalIndicator(max_bytes_padded);
padding_indicator.l <== data_len_bytes;
padding_indicator.r <== data_len_padded_bytes;

signal padded0[max_bytes_padded];
for(var i = 0; i < max_claim_byte_len; i++){
padded0[i] <== reveal_claim.value[i];
}
for(var i = max_claim_byte_len; i < max_bytes_padded; i++) {
padded0[i] <== 0;
}

// add the byte 128 = 10000000_b
signal padded1[max_bytes_padded];
for(var i = 0; i < max_bytes_padded; i++) {
padded1[i] <== padded0[i] * (1 - padding_indicator.start_indicator[i]) + 128 * padding_indicator.start_indicator[i];
}

// Represent the data length in bits as a 64-bit integer, then convert to bytes
signal data_len_bits <== data_len_bytes*8;
component len_bits = Num2Bits(64);
len_bits.in <== data_len_bits;
component len_byte[8];
signal len_bytes[8];
for(var i = 0; i < 8; i++) {
len_byte[i] = Bits2Num(8);
for(var j = 0; j < 8; j++) {
len_byte[i].in[j] <== len_bits.out[8*i + j];
}
len_bytes[i] <== len_byte[i].out;
}

// Place each length byte at the end of the padded data
signal padded2[8][max_bytes_padded];
component length_indicator[8];
for(var i = 0; i < 8; i++) {
length_indicator[i] = PointIndicator(max_bytes_padded);
length_indicator[i].l <== data_len_padded_bytes - 8 + i;

for(var j = 0; j < max_bytes_padded; j++) {
if(i == 0) {
padded2[i][j] <== length_indicator[i].indicator[j] * len_bytes[7-i] + padded1[j];
} else {
padded2[i][j] <== length_indicator[i].indicator[j] * len_bytes[7-i] + padded2[i-1][j];
}
}
}
signal padded[max_bytes_padded] <== padded2[7];

// Converts bytes to bits and input to SHA gadget
component bits[max_bytes_padded];
for (var i = 0; i < max_bytes_padded; i++) {
bits[i] = Num2Bits(8);
bits[i].in <== padded[i];
for (var j = 0; j < 8; j++) {
sha256.paddedIn[i*8+j] <== bits[i].out[7-j];
}
}
sha256.in_len_padded_bits <== data_len_padded_bytes*8;

component b2n = Bits2Num(248);
for(var i = 0; i < 248; i++) {
b2n.in[i] <== sha256.out[i];
}
mimc_hash.k <== 0;

digest <== mimc_hash.out;
digest <== b2n.out;
}

// Generates constraints to enforce that `msg` has the substring `substr` starting at position `l` and ending at position `r`
1 change: 0 additions & 1 deletion circuit_setup/circuits/utils/utils.circom
Original file line number Diff line number Diff line change
@@ -78,7 +78,6 @@ template CalculateTotal(n) {
sum <== sums[n - 1];
}

// Written by us
// n bytes per signal, n = 31 usually
template Packed2Bytes(n){
signal input in; // < 2 ^ (8 * 31)
36 changes: 36 additions & 0 deletions circuit_setup/inputs/rs256-sd/claims.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"acct": 0,
"aud": "12345678-1234-abcd-1234-abcdef124567",
"auth_time": 1725917899,
"email": "matthew@example.com",
"exp": 1759517346,
"family_name": "Matthew",
"given_name": "Matthewson",
"iat": 1728067746,
"ipaddr": "203.0.113.0",
"iss": "https://login.microsoftonline.com/12345678-1234-abcd-1234-abcdef124567/v2.0",
"jti": "AUJNzY3Cwon7pL_3k0-fdw",
"login_hint": "O.aaaaabbbbbbbbbcccccccdddddddeeeeeeeffffffgggggggghhhhhhiiiiiiijjjjjjjkkkkkkklllllllmmmmmmnnnnnnnnnnooooooopppppppqqqqrrrrrrsssssdddd",
"name": "Matthew Matthewson",
"nbf": 1728067746,
"oid": "12345678-1234-abcd-1234-abcdef124567",
"onprem_sid": "S-1-2-34-5678901234-1234567890-1234567890-1234567",
"preferred_username": "matthew@example.com",
"rh": "0.aaaaabbbbbccccddddeeeffff12345gggg12345_124_aaaaaaa.",
"sid": "12345678-1234-abcd-1234-abcdef124567",
"sub": "aaabbbbccccddddeeeeffffgggghhhh123456789012",
"tenant_ctry": "US",
"tenant_region_scope": "WW",
"tid": "12345678-1234-abcd-1234-abcdef124567",
"upn": "matthew@example.com",
"uti": "AAABBBBccccdddd1234567",
"ver": "2.0",
"verified_primary_email": [
"matthew@example.com"
],
"verified_secondary_email": [
"matthew@service.example.com"
],
"xms_pdl": "NAM",
"xms_tpl": "en"
}
44 changes: 44 additions & 0 deletions circuit_setup/inputs/rs256-sd/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"alg": "RS256",
"exp": {
"type" : "number",
"reveal" : true,
"max_claim_byte_len" : 31
},
"email": {
"type" : "string",
"reveal" : true,
"max_claim_byte_len" : 31
},
"family_name": {
"type" : "string",
"reveal" : true,
"max_claim_byte_len" : 31
},
"given_name": {
"type" : "string",
"reveal" : true,
"max_claim_byte_len" : 31
},
"tenant_ctry": {
"type" : "string",
"reveal" : true,
"max_claim_byte_len" : 31
},
"tenant_region_scope": {
"type" : "string",
"reveal" : true,
"max_claim_byte_len" : 31
},
"aud": {
"type" : "string",
"reveal_digest" : true,
"max_claim_byte_len" : 62
},
"auth_time": {
"type" : "number",
"reveal_digest" : true,
"max_claim_byte_len" : 31
}

}
3 changes: 3 additions & 0 deletions circuit_setup/inputs/rs256-sd/proof_spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"revealed" : ["family_name", "tenant_ctry", "auth_time", "aud"]
}
Loading

0 comments on commit 82dfdd0

Please sign in to comment.