Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_crypto/tests/p256_verify.rs

11.3 KiB, 1 run

created by r1870400018:50227, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! P-256 verification, held to two oracles it does not itself define.
2//!
3//! - The NIST CAVP 186-4 SigVer vectors (`tests/data/ecdsa_verify_tests.txt`),
4//! the standards body's own accept/reject verdicts.
5//! - `ring`, differentially: thousands of random signatures, sound and mutated,
6//! that must earn the same verdict from this crate as from `ring`. `ring` is
7//! the backend of `fe2o3_net::ecdsa`, so this is that module's verdict without
8//! the dependency cycle its crate would create.
9
10#![cfg(feature = "p256")]
11
12use oxedyne_fe2o3_core::prelude::*;
13use oxedyne_fe2o3_crypto::p256::{
14 P256_POINT_LEN,
15 P256_SIG_LEN,
16 verify_p256_prehashed,
17 verify_p256_sha256_fixed,
18};
19
20/// Decodes a hex string from a vector file into bytes.
21fn unhex(s: &str) -> Outcome<Vec<u8>> {
22 match hex::decode(s) {
23 Ok(v) => Ok(v),
24 Err(e) => Err(err!("Cannot decode hex '{}': {:?}.", s, e; Invalid, Input)),
25 }
26}
27
28/// One P-256 case lifted from the CAVP file.
29struct Case {
30 x: Vec<u8>, // public point abscissa
31 y: Vec<u8>, // public point ordinate
32 digest: Vec<u8>, // message digest, any hash width
33 r: Vec<u8>, // signature r
34 s: Vec<u8>, // signature s
35 invalid: bool, // carries `Invalid = Y`
36}
37
38impl Case {
39 /// The 65-byte uncompressed SEC1 public key, `0x04 || X || Y`.
40 fn pubkey(&self) -> Vec<u8> {
41 let mut pk = Vec::with_capacity(P256_POINT_LEN);
42 pk.push(0x04);
43 pk.extend_from_slice(&self.x);
44 pk.extend_from_slice(&self.y);
45 pk
46 }
47
48 /// The 64-byte fixed `r || s` signature.
49 fn sig(&self) -> Vec<u8> {
50 let mut sig = Vec::with_capacity(P256_SIG_LEN);
51 sig.extend_from_slice(&self.r);
52 sig.extend_from_slice(&self.s);
53 sig
54 }
55}
56
57/// Reads the `Curve = P-256` blocks out of the CAVP file.
58///
59/// A blank line ends a block. Only P-256 blocks are returned; the file's P-384
60/// blocks are for a curve this crate does not carry.
61fn read_p256_cases() -> Outcome<Vec<Case>> {
62 let path = format!("{}/tests/data/ecdsa_verify_tests.txt", env!("CARGO_MANIFEST_DIR"));
63 let text = match std::fs::read_to_string(&path) {
64 Ok(t) => t,
65 Err(e) => return Err(err!("Cannot read the CAVP vector file '{}': {}.", path, e; IO, Read)),
66 };
67
68 let mut cases = Vec::new();
69 let mut curve: Option<String> = None;
70 let mut x: Option<Vec<u8>> = None;
71 let mut y: Option<Vec<u8>> = None;
72 let mut digest: Option<Vec<u8>> = None;
73 let mut r: Option<Vec<u8>> = None;
74 let mut s: Option<Vec<u8>> = None;
75 let mut invalid = false;
76
77 // Emits the block accumulated so far, if it is a complete P-256 one.
78 fn flush(
79 curve: &mut Option<String>,
80 x: &mut Option<Vec<u8>>,
81 y: &mut Option<Vec<u8>>,
82 digest: &mut Option<Vec<u8>>,
83 r: &mut Option<Vec<u8>>,
84 s: &mut Option<Vec<u8>>,
85 invalid: &mut bool,
86 cases: &mut Vec<Case>,
87 )
88 -> Outcome<()>
89 {
90 if curve.as_deref() == Some("P-256") {
91 match (x.take(), y.take(), digest.take(), r.take(), s.take()) {
92 (Some(x), Some(y), Some(digest), Some(r), Some(s)) => {
93 cases.push(Case { x, y, digest, r, s, invalid: *invalid });
94 },
95 _ => return Err(err!(
96 "A P-256 block in the CAVP file is missing a field."; Invalid, Input)),
97 }
98 }
99 *curve = None;
100 *x = None;
101 *y = None;
102 *digest = None;
103 *r = None;
104 *s = None;
105 *invalid = false;
106 Ok(())
107 }
108
109 for line in text.lines() {
110 let line = line.trim();
111 if line.is_empty() {
112 res!(flush(&mut curve, &mut x, &mut y, &mut digest, &mut r, &mut s,
113 &mut invalid, &mut cases));
114 continue;
115 }
116 if line.starts_with('#') {
117 continue;
118 }
119 match line.split_once('=') {
120 Some((key, val)) => {
121 let key = key.trim();
122 let val = val.trim();
123 match key {
124 "Curve" => curve = Some(val.to_string()),
125 "X" => x = Some(res!(unhex(val))),
126 "Y" => y = Some(res!(unhex(val))),
127 "Digest" => digest = Some(res!(unhex(val))),
128 "R" => r = Some(res!(unhex(val))),
129 "S" => s = Some(res!(unhex(val))),
130 "Invalid" => invalid = val == "Y",
131 _ => {}, // Other keys are not needed here.
132 }
133 },
134 None => return Err(err!("Unparsable line in the CAVP file: '{}'.", line; Invalid, Input)),
135 }
136 }
137 // The file may not end with a blank line.
138 res!(flush(&mut curve, &mut x, &mut y, &mut digest, &mut r, &mut s,
139 &mut invalid, &mut cases));
140
141 Ok(cases)
142}
143
144/// Every NIST CAVP P-256 verification vector earns the verdict NIST records:
145/// a sound signature verifies, an `Invalid = Y` one does not.
146///
147/// The digests in the file span SHA-1 through SHA-512, so this also exercises the
148/// prehash reduction across widths either side of the 32-byte scalar field. The
149/// tallies are asserted too, so a silent parsing fault that dropped or duplicated
150/// cases cannot pass unnoticed.
151#[test]
152fn cavp_p256_sigver_vectors() -> Outcome<()> {
153 let cases = res!(read_p256_cases());
154 assert_eq!(cases.len(), 85, "expected 85 P-256 CAVP blocks");
155
156 let mut valid = 0usize;
157 let mut invalid = 0usize;
158 for (i, case) in cases.iter().enumerate() {
159 let got = res!(verify_p256_prehashed(&case.pubkey(), &case.digest, &case.sig()));
160 let want = !case.invalid;
161 assert_eq!(got, want,
162 "CAVP P-256 case {} (invalid={}): verifier said {}", i, case.invalid, got);
163 if case.invalid { invalid += 1; } else { valid += 1; }
164 }
165 assert_eq!(valid, 21, "expected 21 valid P-256 vectors");
166 assert_eq!(invalid, 64, "expected 64 invalid P-256 vectors");
167 Ok(())
168}
169
170/// A wrong-width key or signature is a graceful `Ok(false)`, never a panic and
171/// never an error, matching `fe2o3_net::ecdsa`.
172#[test]
173fn malformed_inputs_fail_gracefully() -> Outcome<()> {
174 let pk = vec![0x04u8; P256_POINT_LEN];
175 let sig = vec![0x00u8; P256_SIG_LEN];
176 let msg = b"anything";
177 assert!(!res!(verify_p256_sha256_fixed(&pk[..64], msg, &sig)), "short key");
178 assert!(!res!(verify_p256_sha256_fixed(&pk, msg, &sig[..63])), "short sig");
179 assert!(!res!(verify_p256_sha256_fixed(&[], msg, &sig)), "empty key");
180 assert!(!res!(verify_p256_sha256_fixed(&pk, msg, &[])), "empty sig");
181 // A well-formed 0x04-tagged but off-curve point is a failure, not an error.
182 assert!(!res!(verify_p256_sha256_fixed(&pk, msg, &sig)), "all-zero point is off curve");
183 Ok(())
184}
185
186/// The strongest check: this crate and `ring` agree, signature by signature, over
187/// thousands of random cases -- sound ones both accept, one-bit mutations of the
188/// signature, message or key both reject.
189///
190/// Native only: it mints keys and signs with `ring`, which needs the operating
191/// system's randomness and does not exist on wasm. The verify path under test
192/// carries neither dependency.
193#[cfg(not(target_arch = "wasm32"))]
194#[test]
195fn differential_against_ring() -> Outcome<()> {
196 use rand::Rng;
197 use rand::RngCore;
198 use ring::{
199 rand::SystemRandom,
200 signature::{
201 EcdsaKeyPair,
202 KeyPair,
203 UnparsedPublicKey,
204 ECDSA_P256_SHA256_FIXED,
205 ECDSA_P256_SHA256_FIXED_SIGNING,
206 },
207 };
208
209 /// `ring`'s verdict, the oracle `fe2o3_net::ecdsa::verify_p256_sha256_fixed`
210 /// wraps.
211 fn ring_ok(pubkey: &[u8], msg: &[u8], sig: &[u8]) -> bool {
212 UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, pubkey).verify(msg, sig).is_ok()
213 }
214
215 let sysrng = SystemRandom::new();
216 let mut rng = rand::thread_rng();
217
218 const ITERS: usize = 3000;
219 let mut sound_checked = 0usize;
220 let mut mutated_checked = 0usize;
221
222 for iter in 0..ITERS {
223 // A fresh key each round, so the set spans many keys, not many signatures
224 // of one.
225 let pkcs8 = match EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &sysrng) {
226 Ok(doc) => doc,
227 Err(e) => return Err(err!("ring keygen failed at iter {}: {}.", iter, e; Test, Init)),
228 };
229 let kp = match EcdsaKeyPair::from_pkcs8(
230 &ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &sysrng) {
231 Ok(kp) => kp,
232 Err(e) => return Err(err!("ring key load failed at iter {}: {}.", iter, e; Test, Init)),
233 };
234 let pubkey = kp.public_key().as_ref().to_vec();
235
236 // A random message, sometimes empty.
237 let mlen = rng.gen_range(0..256usize);
238 let mut msg = vec![0u8; mlen];
239 rng.fill_bytes(&mut msg);
240
241 let sig = match kp.sign(&sysrng, &msg) {
242 Ok(s) => s.as_ref().to_vec(),
243 Err(e) => return Err(err!("ring sign failed at iter {}: {}.", iter, e; Test, Data)),
244 };
245
246 // Sound: both accept, and this crate says true.
247 let mine = res!(verify_p256_sha256_fixed(&pubkey, &msg, &sig));
248 let theirs = ring_ok(&pubkey, &msg, &sig);
249 assert!(theirs, "ring rejected its own signature at iter {}", iter);
250 assert_eq!(mine, theirs, "sound case disagreement at iter {}", iter);
251 assert!(mine, "this crate rejected a sound signature at iter {}", iter);
252 sound_checked += 1;
253
254 // Mutated: flip one bit in the signature, the message, or the key (past
255 // the 0x04 tag), preserving length so the crypto, not a size check, is
256 // what decides. Both verifiers must agree, and must reject.
257 let (mut pk_m, mut msg_m, mut sig_m) = (pubkey.clone(), msg.clone(), sig.clone());
258 match rng.gen_range(0..4u8) {
259 0 => {
260 // r half of the signature.
261 let bit = rng.gen_range(0..(32 * 8));
262 sig_m[bit / 8] ^= 1 << (bit % 8);
263 },
264 1 => {
265 // s half of the signature.
266 let bit = rng.gen_range(0..(32 * 8));
267 sig_m[32 + bit / 8] ^= 1 << (bit % 8);
268 },
269 2 => {
270 // A byte of the message; skip when the message is empty.
271 if msg_m.is_empty() {
272 sig_m[0] ^= 0x01;
273 } else {
274 let i = rng.gen_range(0..msg_m.len());
275 let bit = rng.gen_range(0..8u32);
276 msg_m[i] ^= 1 << bit;
277 }
278 },
279 _ => {
280 // The public point, keeping the 0x04 tag.
281 let i = rng.gen_range(1..P256_POINT_LEN);
282 let bit = rng.gen_range(0..8u32);
283 pk_m[i] ^= 1 << bit;
284 },
285 }
286 let mine_m = res!(verify_p256_sha256_fixed(&pk_m, &msg_m, &sig_m));
287 let theirs_m = ring_ok(&pk_m, &msg_m, &sig_m);
288 assert_eq!(mine_m, theirs_m,
289 "mutated case disagreement at iter {}: mine={} ring={}", iter, mine_m, theirs_m);
290 assert!(!mine_m, "a one-bit mutation still verified at iter {}", iter);
291 mutated_checked += 1;
292 }
293
294 assert_eq!(sound_checked, ITERS);
295 assert_eq!(mutated_checked, ITERS);
296 Ok(())
297}