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 | |
| 12 | use oxedyne_fe2o3_core::prelude::*; |
| 13 | use 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. |
| 21 | fn 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. |
| 29 | struct 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 | |
| 38 | impl 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. |
| 61 | fn 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] |
| 152 | fn 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] |
| 173 | fn 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] |
| 195 | fn 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 | } |