Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_crypto/src/p256.rs

4.5 KiB, 1 run

created by r1870400018:50221, 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//! ECDSA P-256 (NIST secp256r1) signature *verification*, pure Rust and
2//! wasm-clean.
3//!
4//! # Why this exists, and why it is verify only
5//!
6//! A browser or phone signs with a key it will not export: a WebCrypto
7//! `CryptoKey` minted `extractable: false`, or a Secure Enclave / Android
8//! Keystore handle. The key never crosses into wasm, so signing and key
9//! generation stay on the host side of the boundary and are not offered here.
10//! What a downloaded wasm client *does* need is to *check* a signature on-device,
11//! rather than trust a server to have checked it, and that is the one operation
12//! this module provides.
13//!
14//! The only other P-256 in Hematite is `fe2o3_net::ecdsa`, a thin wrapper over
15//! `ring`, which is native only and cannot run in a browser. This module is its
16//! wasm-reachable peer: it rests on the RustCrypto `p256` crate, pure Rust with
17//! no `getrandom` on the verify path, and so compiles to
18//! `wasm32-unknown-unknown`. It is gated behind the crate's `p256` feature and
19//! is off by default, leaving a native build unchanged.
20//!
21//! # Accepted encodings
22//!
23//! These match exactly what WebCrypto emits, so the same bytes a browser puts on
24//! the wire verify here without reshaping:
25//!
26//! - Public key: the 65-byte uncompressed SEC1 point `0x04 || X || Y`, as
27//! `exportKey('raw')` yields for an ECDSA P-256 key.
28//! - Signature: the 64-byte fixed-length `r || s` form (IEEE P1363), as
29//! `crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' })` emits.
30//! - Message: the raw bytes as signed, NOT a digest. SHA-256 is applied within,
31//! via `fe2o3_hash`, so no second hasher is compiled onto this path.
32//!
33//! # What a false means
34//!
35//! A malformed input -- a key or signature of the wrong length, an off-curve or
36//! identity point, an `r` or `s` outside `[1, n-1]` -- is a verification
37//! *failure*, `Ok(false)`, not an error. That mirrors `fe2o3_net::ecdsa` and
38//! lets a caller treat "this does not verify" uniformly, however the bytes went
39//! wrong. Nothing here ever accepts what it cannot fully parse and check.
40//!
41//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
42//! Anthropic Claude
43
44use oxedyne_fe2o3_core::prelude::*;
45use oxedyne_fe2o3_hash::sha256;
46
47// Leading `::` so the crate `p256` is meant, not this module of the same name.
48use ::p256::ecdsa::{
49 Signature,
50 VerifyingKey,
51 signature::hazmat::PrehashVerifier,
52};
53
54/// Length of an uncompressed SEC1 P-256 point, `0x04 || X || Y`.
55pub const P256_POINT_LEN: usize = 65;
56
57/// Length of a raw `r || s` P-256 signature.
58pub const P256_SIG_LEN: usize = 64;
59
60/// Verify a P-256 / SHA-256 signature in the encodings WebCrypto emits.
61///
62/// `pubkey` is the 65-byte uncompressed SEC1 point, `sig` the 64-byte `r || s`,
63/// and `msg` the raw message rather than a digest: SHA-256 is applied within,
64/// matching WebCrypto's `hash: 'SHA-256'`. See the module header for how a
65/// malformed input is reported.
66pub fn verify_p256_sha256_fixed(pubkey: &[u8], msg: &[u8], sig: &[u8]) -> Outcome<bool> {
67 let digest = sha256::digest(msg);
68 verify_p256_prehashed(pubkey, &digest, sig)
69}
70
71/// The prehash primitive underneath [`verify_p256_sha256_fixed`]: verify a
72/// P-256 signature over a digest that has already been computed.
73///
74/// `digest` is the message digest as a big-endian byte string. It need not be 32
75/// bytes: a digest shorter or longer than the curve's scalar field is reduced
76/// per FIPS 186-4 / SEC1 (a short one is taken whole, a long one truncated to its
77/// leftmost 256 bits), which is what lets a caller present, say, a SHA-1 or
78/// SHA-512 digest and get the answer NIST's own vectors expect. The WebCrypto
79/// path hands it a 32-byte SHA-256 digest. `pubkey` and `sig` are as for
80/// [`verify_p256_sha256_fixed`].
81pub fn verify_p256_prehashed(pubkey: &[u8], digest: &[u8], sig: &[u8]) -> Outcome<bool> {
82 // A wrong width is a failure to verify, not an error; see the module header.
83 if pubkey.len() != P256_POINT_LEN {
84 return Ok(false);
85 }
86 if sig.len() != P256_SIG_LEN {
87 return Ok(false);
88 }
89 // An off-curve or identity point, or an out-of-range r or s, parses as a
90 // failure rather than propagating: the point never lands where a signature
91 // could hold.
92 let vk = match VerifyingKey::from_sec1_bytes(pubkey) {
93 Ok(vk) => vk,
94 Err(_) => return Ok(false),
95 };
96 let signature = match Signature::from_slice(sig) {
97 Ok(sig) => sig,
98 Err(_) => return Ok(false),
99 };
100 Ok(vk.verify_prehash(digest, &signature).is_ok())
101}