confium_crypto_zk/
zk_key_possession.rs1use getrandom::SysRng;
23use p256::ecdsa::{SigningKey, VerifyingKey};
24use p256::elliptic_curve::PrimeField;
25use p256::elliptic_curve::rand_core::Rng;
26use p256::elliptic_curve::rand_core::UnwrapErr;
27use p256::elliptic_curve::sec1::FromSec1Point;
28use p256::elliptic_curve::sec1::ToSec1Point;
29use p256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar};
30use serde::{Deserialize, Serialize};
31use sha2::{Digest, Sha256};
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct KeyPossessionProof {
36 pub public_key_hex: String,
38 pub commitment_hex: String,
40 pub response_hex: String,
42 pub context_hex: String,
45}
46
47fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
51 loop {
52 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
53 return s;
54 }
55 let mut h = Sha256::new();
56 h.update(b"confium-scalar-reduce-v1");
57 h.update(bytes);
58 bytes = h.finalize().into();
59 }
60}
61
62fn hash_context(context: &[u8]) -> [u8; 32] {
63 let mut hasher = Sha256::new();
64 hasher.update(b"confium-key-possession-context-v1");
65 hasher.update(context);
66 hasher.finalize().into()
67}
68
69fn challenge(public_key: &[u8], context_hash: &[u8; 32], commitment: &[u8]) -> Scalar {
70 let mut hasher = Sha256::new();
71 hasher.update(b"confium-key-possession-v1");
72 hasher.update(public_key);
73 hasher.update(context_hash);
74 hasher.update(commitment);
75 let bytes: [u8; 32] = hasher.finalize().into();
76 reduce_to_scalar(bytes)
77}
78
79pub fn prove_key_possession(
82 signing_key: &SigningKey,
83 context: &[u8],
84) -> Result<KeyPossessionProof, String> {
85 let verifying = signing_key.verifying_key();
86 let pk_bytes = verifying.as_affine().to_sec1_point(true);
87 let pk_hex = hex::encode(pk_bytes.as_bytes());
88 let context_hash = hash_context(context);
89 let context_hex = hex::encode(context_hash);
90
91 let x = Option::<Scalar>::from(Scalar::from_repr(signing_key.to_bytes()))
94 .expect("signing key encodes a canonical scalar");
95
96 loop {
97 let mut nonce_bytes = [0u8; 32];
98 UnwrapErr(SysRng).fill_bytes(&mut nonce_bytes);
99 let nonce = reduce_to_scalar(nonce_bytes);
100 if nonce == Scalar::ZERO {
101 continue;
102 }
103
104 let commitment = (ProjectivePoint::GENERATOR * nonce).to_affine();
105 let commitment_bytes = commitment.to_sec1_point(true);
106 let c = challenge(
107 pk_bytes.as_bytes(),
108 &context_hash,
109 commitment_bytes.as_bytes(),
110 );
111
112 let response = nonce + c * x;
113 let response_bytes: [u8; 32] = response.to_repr().into();
114
115 return Ok(KeyPossessionProof {
116 public_key_hex: pk_hex,
117 commitment_hex: hex::encode(commitment_bytes.as_bytes()),
118 response_hex: hex::encode(response_bytes),
119 context_hex,
120 });
121 }
122}
123
124pub fn verify_key_possession(
126 proof: &KeyPossessionProof,
127 context: &[u8],
128 public_key: &VerifyingKey,
129) -> bool {
130 let context_hash = hash_context(context);
131 if hex::encode(context_hash) != proof.context_hex {
132 return false;
133 }
134
135 let pk_bytes = public_key.as_affine().to_sec1_point(true);
136 if hex::encode(pk_bytes.as_bytes()) != proof.public_key_hex {
137 return false;
138 }
139
140 let commitment_bytes = match hex::decode(&proof.commitment_hex) {
141 Ok(b) => b,
142 Err(_) => return false,
143 };
144 let encoded = match p256::elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(
145 &commitment_bytes,
146 ) {
147 Ok(e) => e,
148 Err(_) => return false,
149 };
150 let commitment = match Option::<AffinePoint>::from(AffinePoint::from_sec1_point(&encoded)) {
151 Some(p) => p,
152 None => return false,
153 };
154
155 let response_bytes = match hex::decode(&proof.response_hex) {
156 Ok(b) => b,
157 Err(_) => return false,
158 };
159 if response_bytes.len() != 32 {
160 return false;
161 }
162 let arr: [u8; 32] = match response_bytes.as_slice().try_into() {
163 Ok(a) => a,
164 Err(_) => return false,
165 };
166 let response = match Option::<Scalar>::from(Scalar::from_repr(arr.into())) {
167 Some(s) => s,
168 None => return false,
169 };
170 if response == Scalar::ZERO {
171 return false;
172 }
173
174 let c = challenge(pk_bytes.as_bytes(), &context_hash, &commitment_bytes);
175 let pk_point = ProjectivePoint::from(*public_key.as_affine());
176
177 let lhs = ProjectivePoint::GENERATOR * response;
179 let rhs = ProjectivePoint::from(commitment) + pk_point * c;
180 lhs == rhs
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use p256::elliptic_curve::Generate;
187
188 #[test]
189 fn honest_proof_round_trips() {
190 let signing = SigningKey::generate();
191 let vk = signing.verifying_key();
192 let proof = prove_key_possession(&signing, b"enroll signer 7").unwrap();
193 assert!(verify_key_possession(&proof, b"enroll signer 7", vk));
194 }
195
196 #[test]
197 fn proofs_differ_across_contexts() {
198 let signing = SigningKey::generate();
199 let p1 = prove_key_possession(&signing, b"context-a").unwrap();
200 let p2 = prove_key_possession(&signing, b"context-b").unwrap();
201 assert_ne!(p1.commitment_hex, p2.commitment_hex);
202 assert_ne!(p1.context_hex, p2.context_hex);
203 }
204
205 #[test]
206 fn proof_does_not_reveal_the_signing_key() {
207 let signing = SigningKey::generate();
208 let proof = prove_key_possession(&signing, b"context").unwrap();
209 let key_bytes = signing.to_bytes();
210 let json = serde_json::to_string(&proof).unwrap();
211 assert!(!json.contains(&hex::encode(key_bytes)));
212 }
213}
214
215#[cfg(test)]
216mod adversarial_tests {
217 use super::*;
220 use p256::elliptic_curve::Generate;
221
222 #[test]
223 fn verify_rejects_tampered_response() {
224 let signing = SigningKey::generate();
225 let vk = signing.verifying_key();
226 let mut proof = prove_key_possession(&signing, b"ctx").unwrap();
227
228 let mut resp = hex::decode(&proof.response_hex).unwrap();
229 resp[0] ^= 0x01;
230 proof.response_hex = resp.iter().map(|b| format!("{b:02x}")).collect();
231 assert!(!verify_key_possession(&proof, b"ctx", vk));
232 }
233
234 #[test]
235 fn verify_rejects_tampered_commitment() {
236 let signing = SigningKey::generate();
237 let vk = signing.verifying_key();
238 let mut proof = prove_key_possession(&signing, b"ctx").unwrap();
239
240 let other_key = SigningKey::generate();
244 let other = other_key.verifying_key();
245 proof.commitment_hex = hex::encode(other.as_affine().to_sec1_point(true).as_bytes());
246 assert!(!verify_key_possession(&proof, b"ctx", vk));
247 }
248
249 #[test]
250 fn verify_rejects_proof_for_a_different_context() {
251 let signing = SigningKey::generate();
252 let vk = signing.verifying_key();
253 let proof = prove_key_possession(&signing, b"original").unwrap();
254 assert!(!verify_key_possession(&proof, b"other", vk));
256 }
257
258 #[test]
259 fn verify_rejects_proof_under_a_different_key() {
260 let signing = SigningKey::generate();
261 let other = SigningKey::generate();
262 let proof = prove_key_possession(&signing, b"ctx").unwrap();
263 assert!(!verify_key_possession(
264 &proof,
265 b"ctx",
266 other.verifying_key()
267 ));
268 }
269
270 #[test]
271 fn verify_rejects_zero_response() {
272 let signing = SigningKey::generate();
273 let vk = signing.verifying_key();
274 let mut proof = prove_key_possession(&signing, b"ctx").unwrap();
275 proof.response_hex = hex::encode([0u8; 32]);
276 assert!(!verify_key_possession(&proof, b"ctx", vk));
277 }
278}