crates/nornir-auth-webauthn/src/es256.rs
//! **The ES256 checks every ceremony path shares** (`pure` and `ceremony`): the
//! authenticator-data flags, the signature over `authenticatorData ||
//! SHA-256(clientDataJSON)`, and the counter rule. One copy, so the two paths
//! can never disagree about what a genuine assertion is.
use sha2::{Digest as _, Sha256};
/// User Present: the person touched the authenticator.
pub(crate) const FLAG_UP: u8 = 0x01;
/// User Verified: the authenticator checked a PIN or a biometric.
pub(crate) const FLAG_UV: u8 = 0x04;
/// Attested credential data is present (registration responses only).
pub(crate) const FLAG_AT: u8 = 0x40;
/// Extension data follows the attested credential data.
#[cfg_attr(not(feature = "ceremony"), allow(dead_code))]
pub(crate) const FLAG_ED: u8 = 0x80;
/// The SEC1 uncompressed point encoding `0x04 || X || Y` that `p256` loads.
#[cfg_attr(not(feature = "webauthn-pure"), allow(dead_code))]
pub(crate) fn sec1_uncompressed(x: &[u8], y: &[u8]) -> Vec<u8> {
let mut sec1 = Vec::with_capacity(65);
sec1.push(0x04);
sec1.extend_from_slice(x);
sec1.extend_from_slice(y);
sec1
}
/// Verify an ES256 signature (ASN.1 DER) over `authData || SHA-256(clientData)`
/// with a SEC1 public key. False for a key or a signature that does not parse.
pub(crate) fn verify_es256_sec1(sec1: &[u8], auth_data: &[u8], client_data: &[u8], der: &[u8]) -> bool {
use p256::ecdsa::{signature::Verifier as _, Signature, VerifyingKey};
let (Ok(key), Ok(signature)) = (VerifyingKey::from_sec1_bytes(sec1), Signature::from_der(der)) else {
return false;
};
let mut message = Vec::with_capacity(auth_data.len() + 32);
message.extend_from_slice(auth_data);
message.extend_from_slice(&Sha256::digest(client_data));
key.verify(&message, &signature).is_ok()
}
/// **The counter rule** (WebAuthn §7.2 step 21, A34-31). A credential stored with
/// counter 0 never counted (a synced passkey reports 0 for ever), so anything is
/// accepted. A credential that DID count must present a strictly larger number:
/// equal, smaller, or a drop to 0 is a cloned authenticator or a replay.
pub(crate) fn counter_fits(stored: u32, presented: u32) -> bool {
stored == 0 || presented > stored
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_counter_that_counted_must_grow_and_one_that_never_counted_need_not() {
assert!(counter_fits(0, 0), "a synced passkey: 0 for ever");
assert!(counter_fits(0, 7), "a first count");
assert!(counter_fits(5, 6));
assert!(!counter_fits(5, 5), "stood still");
assert!(!counter_fits(5, 4), "went back");
assert!(!counter_fits(5, 0), "A34-31: a drop to 0 after a real count");
}
}