@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 +21 -0
- package/Nargo.toml +12 -0
- package/README.md +134 -0
- package/package.json +49 -0
- package/src/credential.nr +44 -0
- package/src/issuer.nr +156 -0
- package/src/main.nr +159 -0
- package/src/types.nr +9 -0
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
|
+
}
|