@coordination-network/circuits 0.0.1 → 0.0.2
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/Nargo.toml +4 -3
- package/README.md +12 -0
- package/package.json +2 -2
- package/src/eddsa.nr +175 -0
- package/src/main.nr +1 -0
package/Nargo.toml
CHANGED
|
@@ -5,8 +5,9 @@ authors = ["Coordination Network"]
|
|
|
5
5
|
|
|
6
6
|
[dependencies]
|
|
7
7
|
poseidon = { git = "https://github.com/noir-lang/poseidon", tag = "v0.3.0" }
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
8
|
+
# Baby Jubjub EdDSA verification lives in src/eddsa.nr rather than noir-lang/eddsa: the upstream
|
|
9
|
+
# verifier passes its challenge through noir-edwards v0.2.5's `ScalarField::<64>::from`, which
|
|
10
|
+
# does not constrain the decomposition to its input. Same version upstream eddsa (7e206c9) uses.
|
|
11
|
+
edwards = { git = "https://github.com/noir-lang/noir-edwards", tag = "v0.2.5" }
|
|
11
12
|
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
13
|
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
CHANGED
|
@@ -68,6 +68,18 @@ both roots against their trusted on-chain registries.
|
|
|
68
68
|
8. Prove that commitment is absent from a private LeanIMT+ witness and return
|
|
69
69
|
the resulting root publicly.
|
|
70
70
|
|
|
71
|
+
The issuer, holder and revocation signature checks rely on cofactored EdDSA
|
|
72
|
+
verification (the key is multiplied by 8).
|
|
73
|
+
|
|
74
|
+
The verifier is `src/eddsa.nr`, a copy of `noir-lang/eddsa` with one added
|
|
75
|
+
constraint. Upstream converts the Poseidon challenge with `noir-edwards`
|
|
76
|
+
v0.2.5's `ScalarField::<64>::from`, which takes its WNAF slices from an
|
|
77
|
+
unconstrained hint and only range-checks them below the field modulus, never
|
|
78
|
+
against the input. A prover could replace the challenge with one and verify
|
|
79
|
+
`S = 0, R8 = -8A` for any key. The local copy asserts that the slices
|
|
80
|
+
reconstruct the hash. A `noir-edwards` bump must re-check that conversion
|
|
81
|
+
before this copy is dropped or the dependency rebuilt.
|
|
82
|
+
|
|
71
83
|
The canonical empty revocation root is zero and requires a zeroed depth-zero
|
|
72
84
|
witness.
|
|
73
85
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coordination-network/circuits",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"compile": "nargo compile",
|
|
35
35
|
"execute": "nargo execute",
|
|
36
36
|
"fixtures": "tsx scripts/generate-fixtures.ts",
|
|
37
|
-
"test": "nargo test",
|
|
37
|
+
"test": "nargo test && tsx scripts/test-constraints.ts",
|
|
38
38
|
"test:constraints": "tsx scripts/test-constraints.ts"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
package/src/eddsa.nr
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Baby Jubjub EdDSA verification, adapted from noir-lang/eddsa at
|
|
2
|
+
// 7e206c95f850346a84faf78e3430cd2bc3f6aabf (Apache-2.0), with the challenge
|
|
3
|
+
// scalar bound to the hash it represents.
|
|
4
|
+
//
|
|
5
|
+
// noir-edwards v0.2.5's `ScalarField::<64>::from` takes its WNAF slices from an
|
|
6
|
+
// unconstrained hint and, unlike the `N < 64` branch, never checks that they
|
|
7
|
+
// reconstruct the input: it only range-checks them below the field modulus. A
|
|
8
|
+
// prover could therefore replace the challenge `h` with any scalar (e.g. one),
|
|
9
|
+
// and `S = 0, R8 = -8A` would then verify for any key and message. Upstream
|
|
10
|
+
// eddsa passes the challenge through that conversion unchanged, so this copy
|
|
11
|
+
// adds the missing equality. Drop it once noir-edwards constrains the
|
|
12
|
+
// reconstruction itself.
|
|
13
|
+
|
|
14
|
+
use std::default::Default;
|
|
15
|
+
use std::hash::Hasher;
|
|
16
|
+
|
|
17
|
+
use edwards::{bjj::BabyJubJub, Curve, CurveTrait, ScalarField};
|
|
18
|
+
|
|
19
|
+
// Baby Jubjub subgroup order
|
|
20
|
+
global SUBORDER: Field =
|
|
21
|
+
2736030358979909402780800718157159386076813972158567259200215660948447373041;
|
|
22
|
+
|
|
23
|
+
// Generator * 8
|
|
24
|
+
global BASE8_X: Field =
|
|
25
|
+
5299619240641551281634865583518297030282874472190772894086521144482721001553;
|
|
26
|
+
global BASE8_Y: Field =
|
|
27
|
+
16950150798460657717958625567821834550301663161624707787222815936182638968203;
|
|
28
|
+
|
|
29
|
+
pub fn eddsa_verify<H>(
|
|
30
|
+
pub_key_x: Field,
|
|
31
|
+
pub_key_y: Field,
|
|
32
|
+
signature_s: Field,
|
|
33
|
+
signature_r8_x: Field,
|
|
34
|
+
signature_r8_y: Field,
|
|
35
|
+
message: Field,
|
|
36
|
+
) -> bool
|
|
37
|
+
where
|
|
38
|
+
H: Hasher + Default,
|
|
39
|
+
{
|
|
40
|
+
// Verifies by testing:
|
|
41
|
+
// S * B8 = R8 + H(R8, A, m) * A8
|
|
42
|
+
|
|
43
|
+
// BabyJubJub::new asserts the point is on the curve
|
|
44
|
+
let pub_key = BabyJubJub::new(pub_key_x, pub_key_y);
|
|
45
|
+
let signature_r8 = BabyJubJub::new(signature_r8_x, signature_r8_y);
|
|
46
|
+
// Ensure S < Subgroup Order
|
|
47
|
+
assert(signature_s.lt(SUBORDER));
|
|
48
|
+
// Calculate the h = H(R, A, msg)
|
|
49
|
+
let mut hasher = H::default();
|
|
50
|
+
hasher.write(signature_r8_x);
|
|
51
|
+
hasher.write(signature_r8_y);
|
|
52
|
+
hasher.write(pub_key_x);
|
|
53
|
+
hasher.write(pub_key_y);
|
|
54
|
+
hasher.write(message);
|
|
55
|
+
let hash: Field = hasher.finish();
|
|
56
|
+
// Calculate second part of the right side: right2 = h*8*A
|
|
57
|
+
// Multiply by 8 by doubling 3 times. This also ensures that the result is in the subgroup.
|
|
58
|
+
let pub_key_mul_2 = pub_key.dbl();
|
|
59
|
+
let pub_key_mul_4 = pub_key_mul_2.dbl();
|
|
60
|
+
let pub_key_mul_8 = pub_key_mul_4.dbl();
|
|
61
|
+
// We check that A8 is not zero.
|
|
62
|
+
assert(!pub_key_mul_8.is_zero());
|
|
63
|
+
|
|
64
|
+
verify_with_challenge(
|
|
65
|
+
pub_key_mul_8,
|
|
66
|
+
signature_r8,
|
|
67
|
+
signature_s,
|
|
68
|
+
hash,
|
|
69
|
+
ScalarField::<64>::from(hash),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Split from `eddsa_verify` so tests can supply the challenge decomposition a
|
|
74
|
+
// hostile prover's hint would, which `ScalarField`'s private fields otherwise prevent.
|
|
75
|
+
fn verify_with_challenge(
|
|
76
|
+
pub_key_mul_8: BabyJubJub,
|
|
77
|
+
signature_r8: BabyJubJub,
|
|
78
|
+
signature_s: Field,
|
|
79
|
+
hash: Field,
|
|
80
|
+
hash_scalar: ScalarField<64>,
|
|
81
|
+
) -> bool {
|
|
82
|
+
// `from` range-checks the slices below the field modulus; binding their
|
|
83
|
+
// (constrained) reconstruction to `hash` makes the decomposition unique.
|
|
84
|
+
assert(
|
|
85
|
+
Into::<Field>::into(hash_scalar) == hash,
|
|
86
|
+
"eddsa challenge decomposition does not match its hash",
|
|
87
|
+
);
|
|
88
|
+
// Compute the right side: R8 + h * A8
|
|
89
|
+
let right = signature_r8 + pub_key_mul_8.mul(hash_scalar);
|
|
90
|
+
// Calculate left side of equation left = S * B8
|
|
91
|
+
let base8: BabyJubJub = Curve { x: BASE8_X, y: BASE8_Y };
|
|
92
|
+
let left = base8.mul(ScalarField::<63>::from(signature_s));
|
|
93
|
+
|
|
94
|
+
left == right
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
mod tests {
|
|
98
|
+
use edwards::{bjj::BabyJubJub, Curve, CurveTrait, ScalarField};
|
|
99
|
+
use poseidon::poseidon::PoseidonHasher;
|
|
100
|
+
use poseidon::poseidon2::Poseidon2Hasher;
|
|
101
|
+
|
|
102
|
+
use std::hash::Hasher;
|
|
103
|
+
|
|
104
|
+
use super::{BASE8_X, BASE8_Y, eddsa_verify, verify_with_challenge};
|
|
105
|
+
|
|
106
|
+
// Upstream noir-lang/eddsa's own test vectors.
|
|
107
|
+
#[test]
|
|
108
|
+
fn upstream_vectors() {
|
|
109
|
+
let msg = 789;
|
|
110
|
+
let pub_key_a = BabyJubJub::one().mul(ScalarField::<63>::from(123));
|
|
111
|
+
let pub_key_b = BabyJubJub::one().mul(ScalarField::<63>::from(456));
|
|
112
|
+
let base8: BabyJubJub = Curve { x: BASE8_X, y: BASE8_Y };
|
|
113
|
+
let r8_a = base8.mul(ScalarField::<63>::from(
|
|
114
|
+
1414770703199880747815475415092878800081323795074043628810774576767372531818,
|
|
115
|
+
));
|
|
116
|
+
let r8_b = base8.mul(ScalarField::<63>::from(
|
|
117
|
+
571799555715456644614141527517766533395606396271089506978608487688924659618,
|
|
118
|
+
));
|
|
119
|
+
let s_a = 30333430637424319196043722294837632681219980330991241982145549329256671548;
|
|
120
|
+
let s_b = 1646085314320208098241070054368798527940102577261034947654839408482102287019;
|
|
121
|
+
|
|
122
|
+
assert(eddsa_verify::<PoseidonHasher>(pub_key_a.x, pub_key_a.y, s_a, r8_a.x, r8_a.y, msg));
|
|
123
|
+
assert(eddsa_verify::<PoseidonHasher>(pub_key_b.x, pub_key_b.y, s_b, r8_b.x, r8_b.y, msg));
|
|
124
|
+
assert(!eddsa_verify::<PoseidonHasher>(pub_key_a.x, pub_key_a.y, s_b, r8_b.x, r8_b.y, msg));
|
|
125
|
+
assert(
|
|
126
|
+
!eddsa_verify::<PoseidonHasher>(pub_key_a.x, pub_key_a.y, s_a, r8_a.x, r8_a.y, msg + 1),
|
|
127
|
+
);
|
|
128
|
+
assert(
|
|
129
|
+
!eddsa_verify::<Poseidon2Hasher>(pub_key_a.x, pub_key_a.y, s_a, r8_a.x, r8_a.y, msg),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The forgery from the unconstrained-challenge finding: S = 0 and R8 = -8A
|
|
134
|
+
// need no secret key, and satisfy the curve equation once the challenge is one.
|
|
135
|
+
fn forgery() -> (BabyJubJub, BabyJubJub, Field) {
|
|
136
|
+
let pub_key = BabyJubJub::one().mul(ScalarField::<63>::from(123));
|
|
137
|
+
let pub_key_mul_8 = pub_key.dbl().dbl().dbl();
|
|
138
|
+
let signature_r8 = -pub_key_mul_8;
|
|
139
|
+
let mut hasher = PoseidonHasher::default();
|
|
140
|
+
hasher.write(signature_r8.x);
|
|
141
|
+
hasher.write(signature_r8.y);
|
|
142
|
+
hasher.write(pub_key.x);
|
|
143
|
+
hasher.write(pub_key.y);
|
|
144
|
+
hasher.write(42);
|
|
145
|
+
(pub_key_mul_8, signature_r8, hasher.finish())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Without the binding the forgery verifies, so the test below would fail if
|
|
149
|
+
// it ever accepted another decomposition.
|
|
150
|
+
#[test]
|
|
151
|
+
fn forgery_satisfies_the_equation_under_a_challenge_of_one() {
|
|
152
|
+
let (pub_key_mul_8, signature_r8, _) = forgery();
|
|
153
|
+
let base8: BabyJubJub = Curve { x: BASE8_X, y: BASE8_Y };
|
|
154
|
+
|
|
155
|
+
let left = base8.mul(ScalarField::<63>::from(0));
|
|
156
|
+
let right = signature_r8 + pub_key_mul_8.mul(ScalarField::<64>::from(1));
|
|
157
|
+
assert(left == right);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The decomposition of one is what a hostile hint returns in place of the real
|
|
161
|
+
// challenge's; the binding has to reject it before the curve equation runs.
|
|
162
|
+
#[test(should_fail_with = "eddsa challenge decomposition does not match its hash")]
|
|
163
|
+
fn rejects_the_decomposition_of_another_challenge() {
|
|
164
|
+
let (pub_key_mul_8, signature_r8, hash) = forgery();
|
|
165
|
+
assert(hash != 1);
|
|
166
|
+
|
|
167
|
+
let _ = verify_with_challenge(
|
|
168
|
+
pub_key_mul_8,
|
|
169
|
+
signature_r8,
|
|
170
|
+
0,
|
|
171
|
+
hash,
|
|
172
|
+
ScalarField::<64>::from(1),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|