@coordination-network/circuits 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ethereum Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Nargo.toml ADDED
@@ -0,0 +1,12 @@
1
+ [package]
2
+ name = "credential"
3
+ type = "bin"
4
+ authors = ["Coordination Network"]
5
+
6
+ [dependencies]
7
+ poseidon = { git = "https://github.com/noir-lang/poseidon", tag = "v0.3.0" }
8
+ # ponytail: floating on master — the latest tag (v0.1.3) still uses the removed `ec` stdlib
9
+ # module, and nargo git deps only accept `tag`/branch, no commit pinning. Pin once upstream tags.
10
+ eddsa = { git = "https://github.com/noir-lang/eddsa", tag = "master" }
11
+ binary_merkle_root = { git = "https://github.com/zk-kit/zk-kit.noir", tag = "binary-merkle-root-v0.0.2", directory = "packages/binary-merkle-root" }
12
+ lean_imt_plus = { git = "https://github.com/zk-kit/zk-kit.noir", tag = "lean-imt-plus-v0.0.1", directory = "packages/lean-imt-plus" }
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # Credential circuit
2
+
3
+ The circuit proves that an anonymous approved issuer signed a private,
4
+ version-one credential; the credential is valid for the required programme and
5
+ country; the presenter controls its bound hardware, holder and revocation keys;
6
+ and its revocation commitment is absent from a LeanIMT+ root returned by the
7
+ proof.
8
+
9
+ ## Signed body
10
+
11
+ The credential is the private `Credential` type in `src/types.nr`. The issuer
12
+ signs its fixed, versioned commitment:
13
+
14
+ ```text
15
+ policy = Poseidon4(
16
+ programme_hash,
17
+ country_code,
18
+ not_before,
19
+ expires_at
20
+ )
21
+
22
+ keys = Poseidon3(
23
+ device_key_commitment,
24
+ holder_key_commitment,
25
+ revocation_commitment
26
+ )
27
+
28
+ Poseidon3(
29
+ credential_domain,
30
+ policy,
31
+ keys
32
+ )
33
+ ```
34
+
35
+ The circuit reconstructs this commitment and verifies the issuer's Baby Jubjub
36
+ EdDSA signature. It hashes the same private issuer key into a private binary
37
+ Merkle path with ZK-Kit's `binary-merkle-root` package and returns the resulting
38
+ root publicly, so the verifier can check it against the trusted on-chain
39
+ approved-issuer registry without learning which issuer signed.
40
+
41
+ The hardware P-256 key, Baby Jubjub holder key and separate Baby Jubjub
42
+ revocation key all sign the private session message:
43
+
44
+ ```text
45
+ Poseidon4(presentation_domain, challenge, audience, credential_commitment)
46
+ ```
47
+
48
+ The P-256 signature consumes the canonical 32-byte big-endian encoding of that
49
+ field element as its message digest. It is encoded as compact `r || s` with
50
+ low-`s` normalization, as Noir's P-256 verifier requires. Its public key is
51
+ private and opens the signed `device_key_commitment`, so it does not become a
52
+ presentation identifier.
53
+
54
+ Public inputs are current time, challenge, audience, required programme and
55
+ required country. The circuit returns the proven approved-issuer and revocation
56
+ roots as public outputs. After proof verification, the application must check
57
+ both roots against their trusted on-chain registries.
58
+
59
+ ## Constraints
60
+
61
+ 1. Verify issuer EdDSA over the reconstructed credential commitment.
62
+ 2. Prove the same private issuer key belongs to the approved-issuer root returned publicly, over a private depth witness capped at the circuit's maximum tree depth.
63
+ 3. Check `not_before <= current_time <= expires_at`.
64
+ 4. Check the private programme and country equal the public requirements.
65
+ 5. Open the device-key commitment and verify its private P-256 session signature.
66
+ 6. Open the holder-key commitment and verify its private EdDSA session signature.
67
+ 7. Open the revocation commitment and verify its private EdDSA session signature.
68
+ 8. Prove that commitment is absent from a private LeanIMT+ witness and return
69
+ the resulting root publicly.
70
+
71
+ The canonical empty revocation root is zero and requires a zeroed depth-zero
72
+ witness.
73
+
74
+ ## Toolchain
75
+
76
+ The native toolchain is pinned, and the two halves must match: a circuit
77
+ compiled by one Noir release and proved by a barretenberg built for another
78
+ fails inside bb's deserializer with `Length is too large`.
79
+
80
+ ```sh
81
+ noirup -v 1.0.0-beta.22
82
+ bbup -v 5.0.0
83
+ ```
84
+
85
+ ## Running
86
+
87
+ ```sh
88
+ yarn fixtures
89
+ yarn test
90
+ yarn execute
91
+ yarn test:constraints
92
+ bb prove -b target/credential.json -w target/credential.gz -o target/proof --write_vk --verify
93
+ ```
94
+
95
+ `generate-fixtures.ts` builds the deterministic circuit fixture directly. The
96
+ credential SDK reproduces the same commitments and builds this ABI through
97
+ `buildZkPresentationInputs`; the issuer-service end-to-end test proves and
98
+ verifies the resulting witness.
99
+
100
+ ## What the npm package is for
101
+
102
+ This package publishes the Noir sources so the circuit behind a published proof
103
+ can be read, audited and rebuilt from the same inputs. It is not a Noir
104
+ dependency: the package is a `bin` crate, and nargo rejects a dependency on one.
105
+
106
+ ## Published artifacts
107
+
108
+ `nargo compile` writes to `target/`, which is not published. The compiled
109
+ circuit and its verification key ship separately, in
110
+ [`@coordination-network/zk-artifacts`](../zk-artifacts). Regenerate them from
111
+ this circuit, and commit the result, with:
112
+
113
+ ```sh
114
+ yarn sync:zk-artifacts
115
+ ```
116
+
117
+ ## Runtime proofs
118
+
119
+ The credential package exposes `ZkCredentialProofSystem`, plus one-shot
120
+ `generateZkPresentationProof` and `verifyZkPresentationProof` helpers. Callers
121
+ provide the trusted compiled circuit artifact, which
122
+ `@coordination-network/zk-artifacts` publishes as
123
+ `credential/circuit.json`. The verifier checks the proof, independently binds
124
+ all five public parameters to its expected time, challenge, audience, programme
125
+ and country, and returns both proven roots to the application for its on-chain
126
+ registry checks.
127
+
128
+ The JavaScript runtime versions are pinned to the native toolchain:
129
+ `@noir-lang/noir_js@1.0.0-beta.22` and `@aztec/bb.js@5.0.0`. A circuit upgrade
130
+ must update these versions and regenerate the artifacts together.
131
+
132
+ Nargo 1.0.0-beta.22 reports a Brillig coverage warning in `noir-edwards`
133
+ `dbl_internal`. The dependency constrains both returned coordinates, so this is
134
+ a compiler false positive in `noir-edwards` v0.2.5.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@coordination-network/circuits",
3
+ "version": "0.0.1",
4
+ "description": "The coordination network's Noir circuits: the credential presentation circuit proving an approved issuer's signature, the credential policy, holder, device and revocation key possession, and revocation non-membership.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/zk-kit/coordination-network.git",
9
+ "directory": "packages/circuits"
10
+ },
11
+ "homepage": "https://github.com/zk-kit/coordination-network/tree/main/packages/circuits",
12
+ "bugs": "https://github.com/zk-kit/coordination-network/issues",
13
+ "files": [
14
+ "Nargo.toml",
15
+ "src/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "keywords": [
20
+ "noir",
21
+ "nargo",
22
+ "barretenberg",
23
+ "ultrahonk",
24
+ "zero-knowledge",
25
+ "circuit",
26
+ "verifiable-credentials",
27
+ "revocation",
28
+ "non-membership-proof"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "compile": "nargo compile",
35
+ "execute": "nargo execute",
36
+ "fixtures": "tsx scripts/generate-fixtures.ts",
37
+ "test": "nargo test",
38
+ "test:constraints": "tsx scripts/test-constraints.ts"
39
+ },
40
+ "devDependencies": {
41
+ "@noble/curves": "^1.9.7",
42
+ "@noble/hashes": "^1.8.0",
43
+ "@semaphore-protocol/identity": "^4.14.3",
44
+ "poseidon-lite": "0.3.0",
45
+ "tsx": "^4.23.0",
46
+ "typescript": "^6.0.3",
47
+ "viem": "^2.55.11"
48
+ }
49
+ }
@@ -0,0 +1,44 @@
1
+ use crate::types::Credential;
2
+ use poseidon::poseidon::bn254::{hash_3, hash_4, hash_5};
3
+
4
+ // keccak256(utf8("urn:cn:credential:v1")) >> 8
5
+ global CREDENTIAL_DOMAIN: Field =
6
+ 363566488821926773809509531830640889725124777157045803797738936823559174026;
7
+ // Domain separator for Poseidon5(device public-key limbs).
8
+ global DEVICE_KEY_DOMAIN: Field =
9
+ 320507632879070882430325612169297945123468706597652794395401074724051126576;
10
+
11
+ pub(crate) fn commitment(credential: Credential) -> Field {
12
+ let policy = hash_4([
13
+ credential.programme_hash,
14
+ credential.country_code,
15
+ credential.not_before as Field,
16
+ credential.expires_at as Field,
17
+ ]);
18
+ let keys = hash_3([
19
+ credential.device_key_commitment,
20
+ credential.holder_key_commitment,
21
+ credential.revocation_commitment,
22
+ ]);
23
+
24
+ hash_3([CREDENTIAL_DOMAIN, policy, keys])
25
+ }
26
+
27
+ pub(crate) fn device_key_commitment(public_key_x: [u8; 32], public_key_y: [u8; 32]) -> Field {
28
+ let x = coordinate_limbs(public_key_x);
29
+ let y = coordinate_limbs(public_key_y);
30
+
31
+ hash_5([DEVICE_KEY_DOMAIN, x[0], x[1], y[0], y[1]])
32
+ }
33
+
34
+ fn coordinate_limbs(coordinate: [u8; 32]) -> [Field; 2] {
35
+ let mut high: [u8; 16] = [0; 16];
36
+ let mut low: [u8; 16] = [0; 16];
37
+
38
+ for i in 0..16 {
39
+ high[i] = coordinate[i];
40
+ low[i] = coordinate[i + 16];
41
+ }
42
+
43
+ [Field::from_be_bytes(high), Field::from_be_bytes(low)]
44
+ }
package/src/issuer.nr ADDED
@@ -0,0 +1,156 @@
1
+ use binary_merkle_root::binary_merkle_root;
2
+ use poseidon::poseidon::bn254::{hash_2, hash_3};
3
+
4
+ pub(crate) global MAX_DEPTH: u32 = 20;
5
+
6
+ // Domain separator for Poseidon3(issuer public key).
7
+ global LEAF_DOMAIN: Field =
8
+ 185404551131306766954100762329350135288498146007932686448214248919067924295;
9
+
10
+ pub(crate) fn root(
11
+ public_key_x: Field,
12
+ public_key_y: Field,
13
+ depth: u32,
14
+ indices: [bool; MAX_DEPTH],
15
+ siblings: [Field; MAX_DEPTH],
16
+ ) -> Field {
17
+ assert(depth <= MAX_DEPTH, "issuer path depth exceeds the circuit maximum");
18
+ let leaf = hash_3([LEAF_DOMAIN, public_key_x, public_key_y]);
19
+ binary_merkle_root(hash_2, leaf, depth, indices, siblings)
20
+ }
21
+
22
+ // Pinned cross-language: the same leaves, roots and witnesses `packages/issuer-set/tests/cross-language.test.ts`
23
+ // and `packages/contracts/test/integration/IssuerSetCrossLanguageRoot.t.sol` pin, computed three
24
+ // independent ways before being copied here.
25
+ mod tests {
26
+ use super::{root, MAX_DEPTH};
27
+ use poseidon::poseidon::bn254::hash_2;
28
+
29
+ // `k * Base8` on Baby Jubjub, for k = 1, 2, 5.
30
+ global KEY_1_X: Field =
31
+ 5299619240641551281634865583518297030282874472190772894086521144482721001553;
32
+ global KEY_1_Y: Field =
33
+ 16950150798460657717958625567821834550301663161624707787222815936182638968203;
34
+ global KEY_2_X: Field =
35
+ 10031262171927540148667355526369034398030886437092045105752248699557385197826;
36
+ global KEY_2_Y: Field =
37
+ 633281375905621697187330766174974863687049529291089048651929454608812697683;
38
+ global KEY_5_X: Field =
39
+ 11480966271046430430613841218147196773252373073876138147006741179837832100836;
40
+ global KEY_5_Y: Field =
41
+ 15148236048131954717802795400425086368006776860859772698778589175317365693546;
42
+
43
+ global LEAF_OF_KEY_1: Field =
44
+ 2493720610778676091149258492386261697559719708118160248500693563612404203088;
45
+ global LEAF_OF_KEY_2: Field =
46
+ 15278273562238927018842798161288898682472598529198791544673021966310311323458;
47
+ global LEAF_OF_KEY_3: Field =
48
+ 20980331097734099105425623618460522669974741732319895218846623662978431137317;
49
+
50
+ global ROOT_OF_KEYS_1_TO_2: Field =
51
+ 15680708859485847685012924683010191875504585065894243099617821723863156328943;
52
+ global ROOT_OF_KEYS_1_2_3: Field =
53
+ 11334223563046274794300795549267186709638286758254539923752495560734019145426;
54
+ global ROOT_OF_KEYS_1_TO_4: Field =
55
+ 1012468898015973113393162930052218519362020364191895474978289263522471748684;
56
+ global ROOT_OF_KEYS_1_TO_5: Field =
57
+ 1509212439766155221370997401451147889947558708210956682611194460683115030117;
58
+
59
+ #[test]
60
+ fn one_key_root_is_the_leaf() {
61
+ let indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
62
+ let siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
63
+
64
+ assert(root(KEY_1_X, KEY_1_Y, 0, indices, siblings) == LEAF_OF_KEY_1);
65
+ }
66
+
67
+ #[test]
68
+ fn two_keys_hash_once() {
69
+ let indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
70
+ let mut siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
71
+ siblings[0] = LEAF_OF_KEY_2;
72
+
73
+ assert(root(KEY_1_X, KEY_1_Y, 1, indices, siblings) == ROOT_OF_KEYS_1_TO_2);
74
+ }
75
+
76
+ #[test]
77
+ fn key_2_opens_root_of_keys_1_2_3() {
78
+ let mut indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
79
+ let mut siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
80
+ indices[0] = true;
81
+ siblings[0] = LEAF_OF_KEY_1;
82
+ siblings[1] = LEAF_OF_KEY_3;
83
+
84
+ assert(root(KEY_2_X, KEY_2_Y, 2, indices, siblings) == ROOT_OF_KEYS_1_2_3);
85
+ }
86
+
87
+ // Slot 4 of a five-slot set: levels 0 and 1 are promoted (no sibling), so the witness holds
88
+ // only the one sibling at the level that does have one.
89
+ #[test]
90
+ fn promoted_levels_are_absent_from_the_witness() {
91
+ let mut indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
92
+ let mut siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
93
+ indices[0] = true;
94
+ siblings[0] = ROOT_OF_KEYS_1_TO_4;
95
+
96
+ assert(root(KEY_5_X, KEY_5_Y, 1, indices, siblings) == ROOT_OF_KEYS_1_TO_5);
97
+ }
98
+
99
+ #[test]
100
+ fn padding_tail_is_ignored() {
101
+ let mut indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
102
+ let mut siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
103
+ siblings[0] = LEAF_OF_KEY_2;
104
+ // Garbage past `depth`: binary_merkle_root only folds the first `depth` levels.
105
+ indices[7] = true;
106
+ siblings[3] = 999;
107
+
108
+ assert(root(KEY_1_X, KEY_1_Y, 1, indices, siblings) == ROOT_OF_KEYS_1_TO_2);
109
+ }
110
+
111
+ #[test]
112
+ fn depth_off_by_one_changes_root() {
113
+ let mut indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
114
+ let mut siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
115
+ indices[0] = true;
116
+ siblings[0] = LEAF_OF_KEY_1;
117
+ siblings[1] = LEAF_OF_KEY_3;
118
+
119
+ // The true witness needs depth 2; stopping one level short must not silently agree.
120
+ assert(root(KEY_2_X, KEY_2_Y, 1, indices, siblings) != ROOT_OF_KEYS_1_2_3);
121
+ }
122
+
123
+ #[test(should_fail_with = "issuer path depth exceeds the circuit maximum")]
124
+ fn rejects_depth_above_max() {
125
+ let indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
126
+ let siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
127
+
128
+ let _ = root(KEY_1_X, KEY_1_Y, MAX_DEPTH + 1, indices, siblings);
129
+ }
130
+
131
+ #[test]
132
+ fn accepts_depth_at_max() {
133
+ let indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
134
+ let siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
135
+
136
+ let mut expected = LEAF_OF_KEY_1;
137
+ for _ in 0..MAX_DEPTH {
138
+ expected = hash_2([expected, 0]);
139
+ }
140
+
141
+ assert(root(KEY_1_X, KEY_1_Y, MAX_DEPTH, indices, siblings) == expected);
142
+ }
143
+
144
+ // key_2_opens_root_of_keys_1_2_3's own witness is only valid at depth 2; running it one level
145
+ // too deep must not silently still agree with ROOT_OF_KEYS_1_2_3.
146
+ #[test]
147
+ fn depth_above_the_true_one_changes_root() {
148
+ let mut indices: [bool; MAX_DEPTH] = [false; MAX_DEPTH];
149
+ let mut siblings: [Field; MAX_DEPTH] = [0; MAX_DEPTH];
150
+ indices[0] = true;
151
+ siblings[0] = LEAF_OF_KEY_1;
152
+ siblings[1] = LEAF_OF_KEY_3;
153
+
154
+ assert(root(KEY_2_X, KEY_2_Y, 3, indices, siblings) != ROOT_OF_KEYS_1_2_3);
155
+ }
156
+ }
package/src/main.nr ADDED
@@ -0,0 +1,159 @@
1
+ mod credential;
2
+ mod issuer;
3
+ mod types;
4
+
5
+ use eddsa::eddsa_verify;
6
+ use issuer::MAX_DEPTH as ISSUER_SET_MAX_DEPTH;
7
+ use lean_imt_plus::lean_imt_plus_root;
8
+ use poseidon::poseidon::bn254::{hash_2, hash_3, hash_4};
9
+ use poseidon::poseidon::PoseidonHasher;
10
+ use types::Credential;
11
+
12
+ global REVOCATION_MAX_DEPTH: u32 = 20;
13
+ // keccak256(utf8("urn:cn:presentation:v1")) >> 8
14
+ global PRESENTATION_DOMAIN: Field =
15
+ 47717037732995119958831198142288202228464361330145547649000821135537239485;
16
+
17
+ fn main(
18
+ current_time: pub u64,
19
+ challenge: pub Field,
20
+ audience: pub Field,
21
+ required_programme_hash: pub Field,
22
+ required_country_code: pub Field,
23
+ credential: Credential,
24
+ issuer_pub_x: Field,
25
+ issuer_pub_y: Field,
26
+ issuer_sig_r8_x: Field,
27
+ issuer_sig_r8_y: Field,
28
+ issuer_sig_s: Field,
29
+ issuer_depth: u32,
30
+ issuer_indices: [bool; ISSUER_SET_MAX_DEPTH],
31
+ issuer_siblings: [Field; ISSUER_SET_MAX_DEPTH],
32
+ device_pub_x: [u8; 32],
33
+ device_pub_y: [u8; 32],
34
+ device_signature: [u8; 64],
35
+ identity_pub_x: Field,
36
+ identity_pub_y: Field,
37
+ identity_sig_r8_x: Field,
38
+ identity_sig_r8_y: Field,
39
+ identity_sig_s: Field,
40
+ revocation_pub_x: Field,
41
+ revocation_pub_y: Field,
42
+ revocation_sig_r8_x: Field,
43
+ revocation_sig_r8_y: Field,
44
+ revocation_sig_s: Field,
45
+ revocation_low_leaf_value: Field,
46
+ revocation_low_leaf_next: Field,
47
+ revocation_depth: u32,
48
+ revocation_indices: [bool; REVOCATION_MAX_DEPTH],
49
+ revocation_siblings: [Field; REVOCATION_MAX_DEPTH],
50
+ ) -> pub (Field, Field) {
51
+ let credential_message = credential::commitment(credential);
52
+
53
+ assert(
54
+ eddsa_verify::<PoseidonHasher>(
55
+ issuer_pub_x,
56
+ issuer_pub_y,
57
+ issuer_sig_s,
58
+ issuer_sig_r8_x,
59
+ issuer_sig_r8_y,
60
+ credential_message,
61
+ ),
62
+ "invalid issuer signature",
63
+ );
64
+ let approved_issuer_root =
65
+ issuer::root(issuer_pub_x, issuer_pub_y, issuer_depth, issuer_indices, issuer_siblings);
66
+
67
+ assert(credential.not_before <= current_time, "credential is not yet valid");
68
+ assert(current_time <= credential.expires_at, "credential has expired");
69
+ assert_eq(credential.programme_hash, required_programme_hash, "programme does not match");
70
+ assert_eq(credential.country_code, required_country_code, "country does not match");
71
+
72
+ let session_message = hash_4(
73
+ [PRESENTATION_DOMAIN, challenge, audience, credential_message],
74
+ );
75
+ let session_message_bytes: [u8; 32] = session_message.to_be_bytes();
76
+ assert_eq(
77
+ credential::device_key_commitment(device_pub_x, device_pub_y),
78
+ credential.device_key_commitment,
79
+ "device key does not open the credential commitment",
80
+ );
81
+ assert(
82
+ std::ecdsa_secp256r1::verify_signature(
83
+ device_pub_x,
84
+ device_pub_y,
85
+ device_signature,
86
+ session_message_bytes,
87
+ ),
88
+ "invalid device signature",
89
+ );
90
+
91
+ assert_eq(
92
+ hash_2([identity_pub_x, identity_pub_y]),
93
+ credential.holder_key_commitment,
94
+ "identity key does not open the credential commitment",
95
+ );
96
+ assert(
97
+ eddsa_verify::<PoseidonHasher>(
98
+ identity_pub_x,
99
+ identity_pub_y,
100
+ identity_sig_s,
101
+ identity_sig_r8_x,
102
+ identity_sig_r8_y,
103
+ session_message,
104
+ ),
105
+ "invalid identity signature",
106
+ );
107
+
108
+ assert_eq(
109
+ hash_2([revocation_pub_x, revocation_pub_y]),
110
+ credential.revocation_commitment,
111
+ "revocation key does not open the credential commitment",
112
+ );
113
+ assert(
114
+ eddsa_verify::<PoseidonHasher>(
115
+ revocation_pub_x,
116
+ revocation_pub_y,
117
+ revocation_sig_s,
118
+ revocation_sig_r8_x,
119
+ revocation_sig_r8_y,
120
+ session_message,
121
+ ),
122
+ "invalid revocation-key signature",
123
+ );
124
+
125
+ let revocation_root = lean_imt_plus_root(
126
+ hash_3,
127
+ hash_2,
128
+ true,
129
+ credential.revocation_commitment,
130
+ revocation_low_leaf_value,
131
+ revocation_low_leaf_next,
132
+ revocation_depth,
133
+ revocation_indices,
134
+ revocation_siblings,
135
+ );
136
+ let empty_registry = revocation_depth == 0;
137
+ assert(!empty_registry | (revocation_low_leaf_value == 0), "invalid empty revocation witness");
138
+ assert(!empty_registry | (revocation_low_leaf_next == 0), "invalid empty revocation witness");
139
+ for i in 0..REVOCATION_MAX_DEPTH {
140
+ assert(!empty_registry | !revocation_indices[i], "invalid empty revocation witness");
141
+ assert(!empty_registry | (revocation_siblings[i] == 0), "invalid empty revocation witness");
142
+ }
143
+
144
+ let canonical_revocation_root = if empty_registry { 0 } else { revocation_root };
145
+
146
+ (approved_issuer_root, canonical_revocation_root)
147
+ }
148
+
149
+ #[test]
150
+ fn accepts_zk_kit_vector() {
151
+ let pub_x = 17145850796026918084964381264224969058280635207741849259441085610619323268793;
152
+ let pub_y = 12739136082614649000971099828843750903333050161922102230327542411762042864532;
153
+ let r8_x = 15524625270731079305966391574257085402945890677596245735016312557711871251745;
154
+ let r8_y = 21851585646035144628054782655502377976073920894610191532514047740161645049816;
155
+ let s = 546000471566828313746526008704952611728577210855060032957183501675256683918;
156
+
157
+ assert(eddsa_verify::<PoseidonHasher>(pub_x, pub_y, s, r8_x, r8_y, 42));
158
+ assert(!eddsa_verify::<PoseidonHasher>(pub_x, pub_y, s, r8_x, r8_y, 43));
159
+ }
package/src/types.nr ADDED
@@ -0,0 +1,9 @@
1
+ pub struct Credential {
2
+ pub(crate) programme_hash: Field,
3
+ pub(crate) country_code: Field,
4
+ pub(crate) not_before: u64,
5
+ pub(crate) expires_at: u64,
6
+ pub(crate) device_key_commitment: Field,
7
+ pub(crate) holder_key_commitment: Field,
8
+ pub(crate) revocation_commitment: Field,
9
+ }