Skip to main content

webauthn/
crypto.rs

1//! Low-level cryptographic primitives used throughout WebAuthn ceremony verification.
2//!
3//! All cryptographic operations are delegated to [`ring`], which is a carefully
4//! audited, FIPS-aligned library descended from BoringSSL. No custom crypto is
5//! implemented here.
6
7use ring::digest;
8use ring::rand::{SecureRandom, SystemRandom};
9use ring::signature::{self, UnparsedPublicKey};
10
11use crate::credential::Challenge;
12pub use crate::der::rsa_components_to_der;
13use crate::error::{Result, WebAuthnError};
14
15/// Compute SHA-256 of `data` and return the 32-byte digest.
16///
17/// Used to hash `clientDataJSON` (→ clientDataHash) and to compute the
18/// RP ID hash for comparison against authenticator data.
19pub fn sha256(data: &[u8]) -> [u8; 32] {
20    let digest = digest::digest(&digest::SHA256, data);
21    // SHA256 always produces exactly 32 bytes; the unwrap cannot fail.
22    digest
23        .as_ref()
24        .try_into()
25        .expect("SHA-256 digest is always 32 bytes")
26}
27
28/// Generate `len` cryptographically random bytes using the OS RNG.
29///
30/// # Errors
31/// Returns [`WebAuthnError::InvalidClientData`] if the system RNG fails
32/// (extremely unlikely in practice).
33pub fn random_bytes(len: usize) -> Result<Vec<u8>> {
34    let rng = SystemRandom::new();
35    let mut bytes = vec![0u8; len];
36    rng.fill(&mut bytes).map_err(|_| {
37        WebAuthnError::InvalidClientData(
38            "system random number generator failed to produce bytes".to_string(),
39        )
40    })?;
41    Ok(bytes)
42}
43
44/// Verify an ES256 (ECDSA P-256 + SHA-256) signature.
45///
46/// # Arguments
47/// * `public_key_uncompressed` — 65-byte uncompressed P-256 point:
48///   `0x04 || x (32 bytes) || y (32 bytes)`.
49/// * `message`   — The raw message that was signed (ring hashes internally via SHA-256).
50/// * `signature` — DER-encoded ASN.1 ECDSA signature, as produced by authenticators.
51///
52/// # Errors
53/// Returns [`WebAuthnError::SignatureVerificationFailed`] if the signature is
54/// invalid, the key is malformed, or the public key does not match.
55pub fn verify_es256(
56    public_key_uncompressed: &[u8],
57    message: &[u8],
58    signature: &[u8],
59) -> Result<()> {
60    let key = UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, public_key_uncompressed);
61    key.verify(message, signature)
62        .map_err(|_| WebAuthnError::SignatureVerificationFailed)
63}
64
65/// Verify an ES384 (ECDSA P-384 + SHA-384) signature.
66///
67/// # Arguments
68/// * `public_key_uncompressed` — 97-byte uncompressed P-384 point:
69///   `0x04 || x (48 bytes) || y (48 bytes)`.
70/// * `message`   — The raw message that was signed (ring hashes internally via SHA-384).
71/// * `signature` — DER-encoded ASN.1 ECDSA signature, as produced by authenticators.
72///
73/// # Errors
74/// Returns [`WebAuthnError::SignatureVerificationFailed`] if the signature is
75/// invalid, the key is malformed, or the public key does not match.
76pub fn verify_es384(
77    public_key_uncompressed: &[u8],
78    message: &[u8],
79    signature: &[u8],
80) -> Result<()> {
81    let key = UnparsedPublicKey::new(&signature::ECDSA_P384_SHA384_ASN1, public_key_uncompressed);
82    key.verify(message, signature)
83        .map_err(|_| WebAuthnError::SignatureVerificationFailed)
84}
85
86/// Verify an EdDSA (Ed25519) signature.
87///
88/// # Arguments
89/// * `public_key` — Raw 32-byte Ed25519 public key, as stored in COSE OKP key
90///   parameter `-2` (`x`). Ed25519 keys are always exactly 32 bytes.
91/// * `message`    — The raw message that was signed. Ed25519 processes the
92///   message internally (via SHA-512 internally in ring); do not pre-hash.
93/// * `signature`  — Raw 64-byte Ed25519 signature. Unlike ECDSA, Ed25519
94///   signatures are a fixed-length raw encoding, not DER.
95///
96/// # Errors
97/// Returns [`WebAuthnError::SignatureVerificationFailed`] if the signature is
98/// invalid, the key is not 32 bytes, or the signature is not 64 bytes.
99pub fn verify_eddsa(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<()> {
100    let key = UnparsedPublicKey::new(&signature::ED25519, public_key);
101    key.verify(message, signature)
102        .map_err(|_| WebAuthnError::SignatureVerificationFailed)
103}
104
105/// Verify an RS256 (RSA PKCS#1 v1.5 + SHA-256) signature.
106///
107/// # Arguments
108/// * `public_key_der` — DER-encoded RSAPublicKey (`SEQUENCE { INTEGER n, INTEGER e }`)
109///   as expected by ring. Build this from raw `(n, e)` components with
110///   [`rsa_components_to_der`].
111/// * `message`   — The raw message that was signed (ring hashes internally via SHA-256).
112/// * `signature` — Raw RSA signature bytes (same length as the modulus).
113///
114/// # Errors
115/// Returns [`WebAuthnError::SignatureVerificationFailed`] if the signature is
116/// invalid, the key is malformed, or the key does not meet ring's 2048-bit minimum.
117pub fn verify_rs256(public_key_der: &[u8], message: &[u8], signature: &[u8]) -> Result<()> {
118    let key = UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, public_key_der);
119    key.verify(message, signature)
120        .map_err(|_| WebAuthnError::SignatureVerificationFailed)
121}
122
123/// Generate a fresh 32-byte [`Challenge`] using the OS cryptographic RNG.
124///
125/// Convenience wrapper around [`Challenge::new`]; prefer that method in new code.
126pub fn generate_challenge() -> Result<Challenge> {
127    Challenge::new()
128}
129
130// ─── Tests ────────────────────────────────────────────────────────────────────
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    // ── sha256 ───────────────────────────────────────────────────────────────
137
138    #[test]
139    fn sha256_known_answer_empty_input() {
140        // RFC-specified SHA-256 of the empty string.
141        let digest = sha256(b"");
142        let expected = hex_to_bytes(
143            "e3b0c44298fc1c149afbf4c8996fb924\
144                                     27ae41e4649b934ca495991b7852b855",
145        );
146        assert_eq!(digest, expected.as_slice());
147    }
148
149    #[test]
150    fn sha256_returns_32_bytes() {
151        assert_eq!(sha256(b"anything").len(), 32);
152    }
153
154    #[test]
155    fn sha256_is_deterministic() {
156        assert_eq!(sha256(b"abc"), sha256(b"abc"));
157    }
158
159    #[test]
160    fn sha256_different_inputs_differ() {
161        assert_ne!(sha256(b"abc"), sha256(b"abd"));
162        assert_ne!(sha256(b"abc"), sha256(b""));
163    }
164
165    // ── random_bytes ─────────────────────────────────────────────────────────
166
167    #[test]
168    fn random_bytes_zero_len_returns_empty() {
169        let result = random_bytes(0).expect("test setup");
170        assert!(result.is_empty());
171    }
172
173    #[test]
174    fn random_bytes_returns_exact_len() {
175        let result = random_bytes(32).expect("test setup");
176        assert_eq!(result.len(), 32);
177    }
178
179    #[test]
180    fn random_bytes_two_calls_differ() {
181        let a = random_bytes(32).expect("test setup");
182        let b = random_bytes(32).expect("test setup");
183        assert_ne!(a, b, "two random 32-byte draws should not match");
184    }
185
186    // ── verify_es256 ─────────────────────────────────────────────────────────
187
188    #[test]
189    fn verify_es256_rejects_key_missing_prefix() {
190        // A 64-byte key (missing the 0x04 uncompressed-point prefix) is invalid.
191        let key_64 = vec![0x01u8; 64];
192        let err = verify_es256(&key_64, b"msg", b"sig").expect_err("expected error");
193        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
194    }
195
196    #[test]
197    fn verify_es256_rejects_empty_key() {
198        let err = verify_es256(&[], b"msg", b"sig").expect_err("expected error");
199        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
200    }
201
202    #[test]
203    fn verify_es256_rejects_empty_signature() {
204        let key = vec![0x04u8; 65];
205        let err = verify_es256(&key, b"hello", &[]).expect_err("expected error");
206        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
207    }
208
209    #[test]
210    fn verify_es256_rejects_wrong_signature() {
211        use ring::rand::SystemRandom;
212        use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
213
214        let rng = SystemRandom::new();
215        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng)
216            .expect("test setup");
217        let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
218            .expect("test setup");
219        let pk = kp.public_key().as_ref();
220
221        // Sign "message A" but verify against "message B".
222        let sig = kp.sign(&rng, b"message A").expect("test setup");
223        let err = verify_es256(pk, b"message B", sig.as_ref()).expect_err("expected error");
224        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
225    }
226
227    #[test]
228    fn verify_es256_rejects_garbage_der_signature() {
229        use ring::rand::SystemRandom;
230        use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_ASN1_SIGNING};
231
232        let rng = SystemRandom::new();
233        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng)
234            .expect("test setup");
235        let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
236            .expect("test setup");
237        let pk = kp.public_key().as_ref();
238
239        let err =
240            verify_es256(pk, b"hello", &[0xDE, 0xAD, 0xBE, 0xEF]).expect_err("expected error");
241        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
242    }
243
244    // ── verify_eddsa ─────────────────────────────────────────────────────────
245
246    #[test]
247    fn verify_eddsa_accepts_valid_signature() {
248        use ring::signature::{Ed25519KeyPair, KeyPair};
249
250        let rng = SystemRandom::new();
251        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).expect("test setup");
252        let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).expect("test setup");
253        let pk = kp.public_key().as_ref();
254
255        let msg = b"test message for EdDSA verification";
256        let sig = kp.sign(msg);
257        verify_eddsa(pk, msg, sig.as_ref()).expect("valid EdDSA signature should verify");
258    }
259
260    #[test]
261    fn verify_eddsa_rejects_wrong_message() {
262        use ring::signature::{Ed25519KeyPair, KeyPair};
263
264        let rng = SystemRandom::new();
265        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).expect("test setup");
266        let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).expect("test setup");
267        let pk = kp.public_key().as_ref();
268
269        let sig = kp.sign(b"message A");
270        let err = verify_eddsa(pk, b"message B", sig.as_ref()).expect_err("expected error");
271        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
272    }
273
274    #[test]
275    fn verify_eddsa_rejects_empty_key() {
276        let err = verify_eddsa(&[], b"msg", &[0u8; 64]).expect_err("expected error");
277        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
278    }
279
280    #[test]
281    fn verify_eddsa_rejects_tampered_signature() {
282        use ring::signature::{Ed25519KeyPair, KeyPair};
283
284        let rng = SystemRandom::new();
285        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).expect("test setup");
286        let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).expect("test setup");
287        let pk = kp.public_key().as_ref();
288
289        let msg = b"test message";
290        let sig = kp.sign(msg);
291        let mut sig_bytes = sig.as_ref().to_vec();
292        sig_bytes[10] ^= 0xFF;
293        let err = verify_eddsa(pk, msg, &sig_bytes).expect_err("expected error");
294        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
295    }
296
297    // ── verify_rs256 ─────────────────────────────────────────────────────────
298
299    // RSA 2048-bit test key: private key (PKCS#8 DER) + public key components.
300    // This is a test-only key — never use it outside test code.
301    const TEST_RSA_PKCS8_DER: &[u8] = &[
302        0x30, 0x82, 0x04, 0xbc, 0x02, 0x01, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
303        0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x04, 0x82, 0x04, 0xa6, 0x30, 0x82, 0x04, 0xa2,
304        0x02, 0x01, 0x00, 0x02, 0x82, 0x01, 0x01, 0x00, 0xb7, 0xe0, 0x0f, 0xc9, 0xdb, 0xfa, 0xce,
305        0x64, 0xa6, 0xe2, 0xb7, 0xfb, 0xa2, 0x1c, 0x09, 0x14, 0xfb, 0xd6, 0x26, 0xe5, 0x17, 0xcc,
306        0xf6, 0x6b, 0xf5, 0x8e, 0xbb, 0x69, 0x07, 0x50, 0xc0, 0xbb, 0x4c, 0xe7, 0x6e, 0xd8, 0xa4,
307        0x6a, 0x69, 0x29, 0xfc, 0xc9, 0x52, 0x0c, 0xdb, 0x04, 0xec, 0xa2, 0xef, 0x27, 0x7d, 0x8f,
308        0xfa, 0x9d, 0xaa, 0x10, 0x59, 0x54, 0x7b, 0x42, 0x78, 0xdb, 0xae, 0xd4, 0x24, 0x0a, 0xd4,
309        0x06, 0x69, 0xb0, 0xe2, 0xa5, 0x68, 0xca, 0x2d, 0x41, 0x34, 0xb0, 0x64, 0xaf, 0x61, 0x13,
310        0xc9, 0x32, 0xfc, 0x93, 0x56, 0x4f, 0x82, 0x7b, 0xea, 0xff, 0x20, 0xe5, 0x1c, 0x56, 0xb6,
311        0xe0, 0xf4, 0xaa, 0x6a, 0x20, 0xd2, 0x1c, 0x46, 0x71, 0xe6, 0x05, 0x9a, 0x96, 0x99, 0xad,
312        0x5a, 0x6f, 0x78, 0xfd, 0xa7, 0x06, 0xf8, 0xfd, 0x2d, 0xea, 0x91, 0xf2, 0x9e, 0xac, 0xc0,
313        0x43, 0x45, 0x2d, 0x79, 0xb0, 0xf2, 0x24, 0x5a, 0x8c, 0x91, 0xe6, 0xc6, 0xc2, 0xfe, 0x50,
314        0x8d, 0x64, 0x82, 0x06, 0x77, 0x6e, 0xef, 0x7d, 0x61, 0x6e, 0x80, 0xd1, 0x87, 0xfb, 0x25,
315        0x35, 0xc6, 0xe8, 0x3a, 0xec, 0x38, 0xce, 0x45, 0x70, 0xf8, 0x56, 0xc7, 0x6e, 0xb7, 0x20,
316        0xdb, 0x72, 0x51, 0x82, 0xd0, 0xd2, 0xd2, 0xbd, 0xc9, 0xe0, 0x3c, 0xef, 0xbb, 0x93, 0x70,
317        0xdd, 0xfb, 0xd4, 0xda, 0x6e, 0xf6, 0x73, 0xb3, 0x79, 0xf7, 0xe8, 0x49, 0x72, 0x22, 0x44,
318        0x92, 0xd8, 0xe4, 0x3e, 0x04, 0xbc, 0x83, 0xb2, 0x6c, 0x59, 0x4a, 0x79, 0x11, 0x1e, 0x33,
319        0xd6, 0x4b, 0xe6, 0x24, 0x7b, 0xdf, 0x93, 0x18, 0x1d, 0xb3, 0x27, 0x0b, 0x73, 0xbb, 0xff,
320        0xa8, 0xe2, 0x13, 0xa0, 0x8f, 0x39, 0x2c, 0x21, 0xc1, 0x5e, 0xf1, 0xa8, 0x82, 0x25, 0x28,
321        0x19, 0xae, 0xc9, 0x3f, 0x09, 0x2d, 0x8c, 0x81, 0xa5, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02,
322        0x82, 0x01, 0x00, 0x1b, 0x31, 0xd5, 0x8b, 0xf4, 0x8f, 0xbb, 0xca, 0x49, 0x9f, 0x62, 0xf8,
323        0x21, 0xb1, 0xf5, 0x4a, 0xe7, 0xf3, 0x34, 0x95, 0xf1, 0xe6, 0xf3, 0xb4, 0x24, 0x61, 0x7b,
324        0x88, 0xcd, 0x56, 0xed, 0x66, 0x56, 0x39, 0xad, 0x5c, 0x77, 0xb6, 0xb0, 0x3e, 0x90, 0x3f,
325        0x43, 0x36, 0x19, 0x07, 0x79, 0xab, 0x20, 0x65, 0x4e, 0x0e, 0x07, 0x12, 0x1d, 0xf6, 0xa4,
326        0x8b, 0x98, 0xde, 0x4c, 0x2b, 0x2b, 0x88, 0x7f, 0x1b, 0x25, 0xe0, 0x1b, 0xee, 0x18, 0x1b,
327        0x40, 0x2c, 0x14, 0xb4, 0xdd, 0xe2, 0xcf, 0xc5, 0x5b, 0x7d, 0x76, 0x66, 0xa6, 0xd1, 0xf0,
328        0xb4, 0x3a, 0x37, 0x73, 0x1a, 0x50, 0x26, 0x6a, 0x82, 0x4d, 0xb2, 0x68, 0x25, 0x33, 0x24,
329        0x8f, 0x06, 0xb5, 0x09, 0x7f, 0xec, 0x68, 0xc0, 0x68, 0xd2, 0xa9, 0x7b, 0x2e, 0xa1, 0x0f,
330        0x3c, 0xba, 0x03, 0x11, 0xf1, 0x2d, 0x2c, 0x3d, 0xb9, 0x0d, 0x87, 0x34, 0x84, 0x62, 0x65,
331        0xe4, 0xe3, 0x32, 0x0d, 0xbd, 0x90, 0xad, 0x2c, 0x57, 0x74, 0x39, 0xd8, 0x25, 0x42, 0x46,
332        0x3c, 0xc9, 0x0b, 0x26, 0xc9, 0x99, 0x75, 0xc2, 0x6e, 0x56, 0x82, 0x41, 0xfb, 0xeb, 0xd9,
333        0x80, 0x3d, 0x6e, 0x0e, 0x8b, 0x0b, 0xa3, 0xaf, 0x6c, 0x1d, 0x39, 0x79, 0xf8, 0xa5, 0xae,
334        0x6c, 0x9e, 0xdf, 0x1b, 0xd3, 0x7a, 0x16, 0x35, 0xf4, 0x14, 0x6d, 0x1e, 0x4d, 0x7b, 0x6f,
335        0xbb, 0xfb, 0xe9, 0x2c, 0x9e, 0xad, 0xf9, 0x0a, 0x53, 0x29, 0x2a, 0xd8, 0xff, 0x9d, 0xb3,
336        0xac, 0x9e, 0x2d, 0x86, 0x77, 0x67, 0x4c, 0xa8, 0x74, 0xe3, 0xb2, 0x94, 0xf8, 0xfb, 0xe8,
337        0x33, 0xf2, 0x3c, 0x5d, 0x57, 0x79, 0x89, 0x87, 0xd0, 0x9b, 0x52, 0x4b, 0xc9, 0xbc, 0x48,
338        0x68, 0xbe, 0x85, 0x1f, 0x25, 0x61, 0x44, 0x7e, 0xa6, 0x40, 0x54, 0x70, 0xaf, 0x65, 0x11,
339        0x3a, 0x58, 0xd1, 0x81, 0x02, 0x81, 0x81, 0x00, 0xfd, 0x9e, 0x1a, 0x50, 0x85, 0xd7, 0xa8,
340        0x9c, 0x36, 0x6a, 0x8f, 0x1c, 0x9f, 0x2d, 0x6d, 0xdf, 0xb4, 0xe6, 0xc4, 0xd2, 0xcf, 0x99,
341        0x6f, 0x5d, 0xc5, 0x71, 0x01, 0xe6, 0x1d, 0x5f, 0xd5, 0x6d, 0x52, 0x86, 0x7c, 0xb6, 0xc2,
342        0xc2, 0x1b, 0xcd, 0x4a, 0xd1, 0x0c, 0x79, 0x15, 0x2d, 0x4e, 0x93, 0xe2, 0xc0, 0x7d, 0xe6,
343        0xa4, 0xc4, 0x71, 0x41, 0xa3, 0x49, 0x93, 0x2c, 0xf9, 0xe7, 0xc8, 0xe6, 0x79, 0x52, 0x19,
344        0xf7, 0xe0, 0x2a, 0xd9, 0xc0, 0x0f, 0x3e, 0xad, 0xdf, 0xf4, 0x96, 0xbd, 0xb9, 0x54, 0x5a,
345        0x5a, 0xe8, 0x70, 0x2d, 0xad, 0xf0, 0x5d, 0x63, 0x80, 0xb4, 0x8f, 0x3c, 0x1b, 0xad, 0xd9,
346        0x2b, 0x63, 0x16, 0x80, 0xe1, 0x52, 0x8d, 0xd7, 0x8e, 0x2c, 0x37, 0x99, 0x6e, 0x1e, 0x8e,
347        0x64, 0xcf, 0x0e, 0x79, 0x32, 0xdd, 0x1a, 0xc1, 0x43, 0x7e, 0x4c, 0x0c, 0xab, 0x29, 0x5b,
348        0x25, 0x02, 0x81, 0x81, 0x00, 0xb9, 0x9a, 0x3e, 0x3e, 0x20, 0x74, 0xe5, 0x19, 0x82, 0x8b,
349        0x1d, 0x20, 0x2b, 0xe5, 0xa9, 0x8c, 0x71, 0xd5, 0xa8, 0xe6, 0xed, 0x7e, 0x0a, 0x91, 0x8f,
350        0x87, 0xf5, 0x80, 0xc8, 0xd4, 0x1d, 0x4e, 0x25, 0x0b, 0xfe, 0x22, 0xb6, 0xdd, 0xc3, 0xfd,
351        0x46, 0x78, 0x7f, 0x6c, 0x27, 0x9d, 0xfe, 0xbf, 0xea, 0x66, 0x27, 0x57, 0x2e, 0xc2, 0x7c,
352        0xca, 0x63, 0x1a, 0xc8, 0xb8, 0xda, 0x2f, 0xf3, 0x03, 0xfc, 0x03, 0xcb, 0xf4, 0x0c, 0x8a,
353        0x00, 0x2f, 0x5d, 0x78, 0x7a, 0xff, 0x9e, 0x84, 0xc5, 0x0b, 0xd6, 0xae, 0xf6, 0xf8, 0xc7,
354        0x6f, 0x5f, 0x77, 0x8c, 0x4f, 0xe6, 0x4f, 0x58, 0x67, 0xbf, 0xde, 0x8e, 0x39, 0xa5, 0xd8,
355        0x82, 0x59, 0x40, 0xf8, 0xd4, 0x46, 0xbe, 0x5b, 0xaa, 0x2b, 0x74, 0xf4, 0x29, 0x72, 0xfd,
356        0x2b, 0xa0, 0xa2, 0xb4, 0x1a, 0x3d, 0x4d, 0x2f, 0xfe, 0x61, 0x55, 0x04, 0x81, 0x02, 0x81,
357        0x80, 0x58, 0x92, 0xa6, 0xce, 0x08, 0x70, 0x50, 0xca, 0x7d, 0x96, 0xa9, 0x74, 0x6d, 0x83,
358        0x08, 0x24, 0x60, 0xa1, 0x57, 0x8b, 0xe8, 0x44, 0xc5, 0xc8, 0x11, 0xf4, 0x6d, 0x9d, 0x58,
359        0x14, 0xe8, 0x0c, 0xce, 0x0d, 0x79, 0xf0, 0xba, 0x03, 0xe0, 0x81, 0xc9, 0xe7, 0x48, 0x5b,
360        0xe1, 0x31, 0x79, 0x87, 0xdc, 0x61, 0x2d, 0x97, 0x27, 0x64, 0x13, 0xc9, 0xc0, 0xa5, 0x29,
361        0x69, 0x43, 0xbd, 0xd7, 0x43, 0xe6, 0x8a, 0xed, 0xd6, 0xcb, 0xcb, 0x2b, 0x51, 0x10, 0x01,
362        0xeb, 0xe7, 0x93, 0x1c, 0x32, 0x16, 0x4f, 0x87, 0x5e, 0xc8, 0x5e, 0xa5, 0x15, 0x62, 0x24,
363        0xbb, 0x63, 0x6f, 0xab, 0xb6, 0x6a, 0x54, 0x44, 0xcc, 0x0a, 0x47, 0x09, 0xab, 0xa7, 0x91,
364        0x31, 0xfe, 0xcd, 0x22, 0x7d, 0xcb, 0x1f, 0x90, 0xcb, 0x54, 0x24, 0xd1, 0xdf, 0x19, 0xa9,
365        0x06, 0x65, 0xf3, 0xed, 0xcb, 0x5e, 0xdb, 0x8a, 0xa1, 0x02, 0x81, 0x80, 0x0b, 0x53, 0x45,
366        0x1f, 0x07, 0x5d, 0xfa, 0xa8, 0xce, 0xd5, 0x6c, 0x46, 0x8d, 0x47, 0x2b, 0x4c, 0x5d, 0x99,
367        0xda, 0xff, 0x94, 0x58, 0x4f, 0x8e, 0xc8, 0x42, 0x54, 0x91, 0xb2, 0x2f, 0x77, 0x46, 0x50,
368        0x6e, 0x65, 0xe8, 0x7a, 0x5e, 0x17, 0xda, 0x79, 0x95, 0x5a, 0xb9, 0x1f, 0xc5, 0xbd, 0x48,
369        0xba, 0xa5, 0xd7, 0x1a, 0xb3, 0xc8, 0xbc, 0x52, 0xa1, 0x2f, 0x7e, 0x36, 0x01, 0x62, 0x51,
370        0xa2, 0xd9, 0x9a, 0xe5, 0xb4, 0x13, 0x9b, 0xcc, 0x1d, 0x17, 0xc8, 0x05, 0x41, 0x59, 0xcb,
371        0xe2, 0x36, 0x31, 0xb8, 0x65, 0x6b, 0x92, 0xc7, 0xd1, 0xfc, 0x7a, 0x7c, 0x59, 0xa2, 0x57,
372        0xd3, 0xa4, 0xda, 0x90, 0xb5, 0x25, 0xd0, 0x8b, 0x4b, 0xa4, 0xf2, 0x4a, 0x09, 0xb3, 0x0d,
373        0xe6, 0xd9, 0x55, 0xfe, 0x9c, 0x14, 0xdf, 0x2b, 0xed, 0x56, 0x60, 0x45, 0x05, 0x9e, 0x93,
374        0x22, 0x23, 0x90, 0x4b, 0x81, 0x02, 0x81, 0x80, 0x71, 0x49, 0x07, 0xff, 0x86, 0xc5, 0xf7,
375        0xe4, 0xbd, 0xa8, 0xbc, 0xa4, 0xbe, 0x1f, 0x8e, 0x73, 0xb0, 0xea, 0x71, 0x85, 0x61, 0xb1,
376        0x8d, 0x30, 0xe3, 0xac, 0x67, 0xfc, 0x2c, 0x5a, 0x36, 0xcc, 0x66, 0xe7, 0x2f, 0x32, 0x97,
377        0x54, 0x97, 0xe9, 0xd6, 0x5d, 0xd9, 0xe5, 0xbb, 0x1a, 0x06, 0x15, 0x95, 0x2d, 0x8e, 0xca,
378        0x27, 0x8e, 0x2e, 0x39, 0xab, 0x45, 0x2d, 0x94, 0x26, 0x93, 0xd0, 0x7b, 0xda, 0x62, 0x02,
379        0xba, 0xe6, 0xd9, 0x87, 0xad, 0xf7, 0x2a, 0x33, 0x1d, 0x5a, 0xd9, 0xa8, 0xf3, 0x38, 0x0e,
380        0x0f, 0xd1, 0x24, 0x25, 0x69, 0x2a, 0x2c, 0x99, 0xf7, 0xea, 0x0d, 0x3b, 0x51, 0x8b, 0x2a,
381        0x72, 0xb0, 0x51, 0xd3, 0x07, 0x63, 0x9e, 0x9d, 0x15, 0xe6, 0xa8, 0xf5, 0xce, 0x69, 0x74,
382        0x53, 0xd3, 0xb1, 0x26, 0x77, 0xfa, 0x0e, 0x8f, 0xdd, 0x1f, 0x0e, 0x76, 0x51, 0x56, 0x85,
383        0xb7,
384    ];
385
386    const TEST_RSA_N: &[u8] = &[
387        0xb7, 0xe0, 0x0f, 0xc9, 0xdb, 0xfa, 0xce, 0x64, 0xa6, 0xe2, 0xb7, 0xfb, 0xa2, 0x1c, 0x09,
388        0x14, 0xfb, 0xd6, 0x26, 0xe5, 0x17, 0xcc, 0xf6, 0x6b, 0xf5, 0x8e, 0xbb, 0x69, 0x07, 0x50,
389        0xc0, 0xbb, 0x4c, 0xe7, 0x6e, 0xd8, 0xa4, 0x6a, 0x69, 0x29, 0xfc, 0xc9, 0x52, 0x0c, 0xdb,
390        0x04, 0xec, 0xa2, 0xef, 0x27, 0x7d, 0x8f, 0xfa, 0x9d, 0xaa, 0x10, 0x59, 0x54, 0x7b, 0x42,
391        0x78, 0xdb, 0xae, 0xd4, 0x24, 0x0a, 0xd4, 0x06, 0x69, 0xb0, 0xe2, 0xa5, 0x68, 0xca, 0x2d,
392        0x41, 0x34, 0xb0, 0x64, 0xaf, 0x61, 0x13, 0xc9, 0x32, 0xfc, 0x93, 0x56, 0x4f, 0x82, 0x7b,
393        0xea, 0xff, 0x20, 0xe5, 0x1c, 0x56, 0xb6, 0xe0, 0xf4, 0xaa, 0x6a, 0x20, 0xd2, 0x1c, 0x46,
394        0x71, 0xe6, 0x05, 0x9a, 0x96, 0x99, 0xad, 0x5a, 0x6f, 0x78, 0xfd, 0xa7, 0x06, 0xf8, 0xfd,
395        0x2d, 0xea, 0x91, 0xf2, 0x9e, 0xac, 0xc0, 0x43, 0x45, 0x2d, 0x79, 0xb0, 0xf2, 0x24, 0x5a,
396        0x8c, 0x91, 0xe6, 0xc6, 0xc2, 0xfe, 0x50, 0x8d, 0x64, 0x82, 0x06, 0x77, 0x6e, 0xef, 0x7d,
397        0x61, 0x6e, 0x80, 0xd1, 0x87, 0xfb, 0x25, 0x35, 0xc6, 0xe8, 0x3a, 0xec, 0x38, 0xce, 0x45,
398        0x70, 0xf8, 0x56, 0xc7, 0x6e, 0xb7, 0x20, 0xdb, 0x72, 0x51, 0x82, 0xd0, 0xd2, 0xd2, 0xbd,
399        0xc9, 0xe0, 0x3c, 0xef, 0xbb, 0x93, 0x70, 0xdd, 0xfb, 0xd4, 0xda, 0x6e, 0xf6, 0x73, 0xb3,
400        0x79, 0xf7, 0xe8, 0x49, 0x72, 0x22, 0x44, 0x92, 0xd8, 0xe4, 0x3e, 0x04, 0xbc, 0x83, 0xb2,
401        0x6c, 0x59, 0x4a, 0x79, 0x11, 0x1e, 0x33, 0xd6, 0x4b, 0xe6, 0x24, 0x7b, 0xdf, 0x93, 0x18,
402        0x1d, 0xb3, 0x27, 0x0b, 0x73, 0xbb, 0xff, 0xa8, 0xe2, 0x13, 0xa0, 0x8f, 0x39, 0x2c, 0x21,
403        0xc1, 0x5e, 0xf1, 0xa8, 0x82, 0x25, 0x28, 0x19, 0xae, 0xc9, 0x3f, 0x09, 0x2d, 0x8c, 0x81,
404        0xa5,
405    ];
406
407    const TEST_RSA_E: &[u8] = &[0x01, 0x00, 0x01]; // 65537
408
409    fn rsa_sign(message: &[u8]) -> Vec<u8> {
410        use ring::rand::SystemRandom;
411        use ring::rsa::KeyPair as RsaKeyPair;
412        use ring::signature::RSA_PKCS1_SHA256;
413
414        let rng = SystemRandom::new();
415        let key_pair = RsaKeyPair::from_pkcs8(TEST_RSA_PKCS8_DER).expect("test setup");
416        let mut sig = vec![0u8; key_pair.public().modulus_len()];
417        key_pair
418            .sign(&RSA_PKCS1_SHA256, &rng, message, &mut sig)
419            .expect("test setup");
420        sig
421    }
422
423    fn rsa_public_key_der() -> Vec<u8> {
424        crate::der::rsa_components_to_der(TEST_RSA_N, TEST_RSA_E).expect("test setup")
425    }
426
427    #[test]
428    fn verify_rs256_accepts_valid_signature() {
429        let message = b"test message for RS256 verification";
430        let sig = rsa_sign(message);
431        let der = rsa_public_key_der();
432        verify_rs256(&der, message, &sig).expect("valid RS256 signature should verify");
433    }
434
435    #[test]
436    fn verify_rs256_rejects_tampered_signature() {
437        let message = b"test message";
438        let mut sig = rsa_sign(message);
439        sig[10] ^= 0xFF;
440        let der = rsa_public_key_der();
441        let err = verify_rs256(&der, message, &sig).expect_err("expected error");
442        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
443    }
444
445    #[test]
446    fn verify_rs256_rejects_wrong_message() {
447        let sig = rsa_sign(b"message A");
448        let der = rsa_public_key_der();
449        let err = verify_rs256(&der, b"message B", &sig).expect_err("expected error");
450        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
451    }
452
453    #[test]
454    fn verify_rs256_rejects_empty_public_key() {
455        let err = verify_rs256(&[], b"msg", &[0u8; 256]).expect_err("expected error");
456        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
457    }
458
459    #[test]
460    fn verify_rs256_rejects_garbage_signature() {
461        let der = rsa_public_key_der();
462        let err =
463            verify_rs256(&der, b"hello", &[0xDE, 0xAD, 0xBE, 0xEF]).expect_err("expected error");
464        assert!(matches!(err, WebAuthnError::SignatureVerificationFailed));
465    }
466
467    // ── helpers ──────────────────────────────────────────────────────────────
468
469    fn hex_to_bytes(s: &str) -> Vec<u8> {
470        (0..s.len())
471            .step_by(2)
472            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("test setup"))
473            .collect()
474    }
475}