kv-model: passkey module (ES256, none attestation, AAGUID, KAT + independent verifier tests)
WIP: FFI surface and spec docs not yet added. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
//! Checks responses with an independent verifier: authenticatorData parsed by hand, CBOR decoded with
|
||||
//! `ciborium`, signatures verified with `p256` against the COSE key.
|
||||
|
||||
use ciborium::Value;
|
||||
use p256::{
|
||||
ecdsa::{signature::Verifier, Signature, SigningKey, VerifyingKey},
|
||||
EncodedPoint,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::*;
|
||||
|
||||
const CDH: [u8; 32] = [7; 32];
|
||||
|
||||
struct AuthData {
|
||||
rp_id_hash: [u8; 32],
|
||||
flags: u8,
|
||||
sign_count: u32,
|
||||
attested: Option<(Vec<u8>, Vec<u8>, Value)>,
|
||||
}
|
||||
|
||||
fn parse_auth_data(d: &[u8]) -> AuthData {
|
||||
assert!(d.len() >= 37);
|
||||
let flags = d[32];
|
||||
let attested = (flags & flags::AT != 0).then(|| {
|
||||
let aaguid = d[37..53].to_vec();
|
||||
let n = u16::from_be_bytes([d[53], d[54]]) as usize;
|
||||
let cred_id = d[55..55 + n].to_vec();
|
||||
let mut rest = &d[55 + n..];
|
||||
let key: Value = ciborium::from_reader(&mut rest).unwrap();
|
||||
assert!(rest.is_empty(), "no trailing data (ED is not set)");
|
||||
(aaguid, cred_id, key)
|
||||
});
|
||||
if flags & flags::AT == 0 {
|
||||
assert_eq!(d.len(), 37);
|
||||
}
|
||||
AuthData {
|
||||
rp_id_hash: d[..32].try_into().unwrap(),
|
||||
flags,
|
||||
sign_count: u32::from_be_bytes(d[33..37].try_into().unwrap()),
|
||||
attested,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_get<'a>(m: &'a Value, k: &Value) -> &'a Value {
|
||||
m.as_map().unwrap().iter().find(|(kk, _)| kk == k).map(|(_, v)| v).unwrap()
|
||||
}
|
||||
|
||||
fn verifying_key_from_cose(key: &Value) -> VerifyingKey {
|
||||
let i = |n: i64| Value::Integer(n.into());
|
||||
assert_eq!(map_get(key, &i(1)), &i(2));
|
||||
assert_eq!(map_get(key, &i(3)), &i(-7));
|
||||
assert_eq!(map_get(key, &i(-1)), &i(1));
|
||||
let x = map_get(key, &i(-2)).as_bytes().unwrap();
|
||||
let y = map_get(key, &i(-3)).as_bytes().unwrap();
|
||||
let point = EncodedPoint::from_affine_coordinates(x.as_slice().into(), y.as_slice().into(), false);
|
||||
VerifyingKey::from_encoded_point(&point).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registration_and_assertion_verify_independently() {
|
||||
let pk = generate("example.com", b"user-1", "alice", 1_700_000_000).unwrap();
|
||||
assert_eq!(pk.credential_id.len(), CREDENTIAL_ID_LEN);
|
||||
let reg = registration(&pk).unwrap();
|
||||
|
||||
let att: Value = ciborium::from_reader(reg.attestation_object.as_slice()).unwrap();
|
||||
assert_eq!(map_get(&att, &Value::Text("fmt".into())), &Value::Text("none".into()));
|
||||
assert_eq!(map_get(&att, &Value::Text("attStmt".into())), &Value::Map(vec![]));
|
||||
let auth_data = map_get(&att, &Value::Text("authData".into())).as_bytes().unwrap();
|
||||
assert_eq!(auth_data, ®.authenticator_data);
|
||||
|
||||
let ad = parse_auth_data(auth_data);
|
||||
assert_eq!(ad.rp_id_hash.as_slice(), Sha256::digest(b"example.com").as_slice());
|
||||
assert_eq!(ad.flags, 0x5d);
|
||||
assert_eq!(ad.sign_count, 0);
|
||||
let (aaguid, cred_id, key) = ad.attested.unwrap();
|
||||
assert_eq!(aaguid, KESTROVE_AAGUID.as_bytes());
|
||||
assert_eq!(cred_id, pk.credential_id);
|
||||
let vk = verifying_key_from_cose(&key);
|
||||
let cose: Value = ciborium::from_reader(reg.cose_public_key.as_slice()).unwrap();
|
||||
assert_eq!(cose, key);
|
||||
assert_eq!(vk, VerifyingKey::from_public_key_der_for_test(®.public_key_spki));
|
||||
|
||||
let a = super::assert(&pk, "example.com", &CDH).unwrap();
|
||||
let ad = parse_auth_data(&a.authenticator_data);
|
||||
assert_eq!(ad.rp_id_hash.as_slice(), Sha256::digest(b"example.com").as_slice());
|
||||
assert_eq!((ad.flags, ad.sign_count), (0x1d, 0));
|
||||
assert!(ad.attested.is_none());
|
||||
let mut msg = a.authenticator_data.clone();
|
||||
msg.extend_from_slice(&CDH);
|
||||
vk.verify(&msg, &Signature::from_der(&a.signature).unwrap()).unwrap();
|
||||
msg[0] ^= 1;
|
||||
assert!(vk.verify(&msg, &Signature::from_der(&a.signature).unwrap()).is_err());
|
||||
}
|
||||
|
||||
trait SpkiForTest {
|
||||
fn from_public_key_der_for_test(der: &[u8]) -> Self;
|
||||
}
|
||||
impl SpkiForTest for VerifyingKey {
|
||||
fn from_public_key_der_for_test(der: &[u8]) -> Self {
|
||||
use p256::pkcs8::DecodePublicKey;
|
||||
VerifyingKey::from_public_key_der(der).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rp_id_is_matched_exactly() {
|
||||
let pk = generate("example.com", b"u", "a", 0).unwrap();
|
||||
for other in ["login.example.com", "example.org", "xample.com"] {
|
||||
assert_eq!(super::assert(&pk, other, &CDH), Err(PasskeyError::RpMismatch));
|
||||
}
|
||||
for bad in ["", "Example.com", "https://example.com", "example.com:443", "example.com.", "a..b"] {
|
||||
assert_eq!(validate_rp_id(bad), Err(PasskeyError::InvalidRpId), "{bad}");
|
||||
}
|
||||
assert_eq!(super::assert(&pk, "example.com", &[0; 31]), Err(PasskeyError::InvalidClientDataHash));
|
||||
assert_eq!(generate("example.com", b"", "a", 0).unwrap_err(), PasskeyError::InvalidUserHandle);
|
||||
assert_eq!(generate("example.com", &[0; 65], "a", 0).unwrap_err(), PasskeyError::InvalidUserHandle);
|
||||
}
|
||||
|
||||
fn hex(s: &str) -> Vec<u8> {
|
||||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||||
}
|
||||
|
||||
/// RFC 6979 A.2.5 (P-256, SHA-256, message "sample") and the well-known rpIdHash of example.com.
|
||||
#[test]
|
||||
fn known_answer_vectors() {
|
||||
let key = SigningKey::from_slice(&hex("c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721")).unwrap();
|
||||
let cose = cose_key(key.verifying_key());
|
||||
let mut expected = hex("a5010203262001215820");
|
||||
expected.extend(hex("60fed4ba255a9d31c961eb74c6356d68c049b8923b61fa6ce669622e60f29fb6"));
|
||||
expected.extend(hex("225820"));
|
||||
expected.extend(hex("7903fe1008b8bc99a41ae9e95628bc64f2f1b20c2d7e9f5177a3c294d4462299"));
|
||||
assert_eq!(cose, expected);
|
||||
|
||||
let sig = Signature::from_der(&sign_der(&key, b"sample")).unwrap();
|
||||
let (r, s) = (sig.r().to_bytes().to_vec(), sig.s().to_bytes().to_vec());
|
||||
assert_eq!(r, hex("efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716"));
|
||||
assert_eq!(s, hex("f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8"));
|
||||
|
||||
let pk = from_signing_key(&key, vec![0xab; 16], "example.com", b"u", "a", 0).unwrap();
|
||||
let a = super::assert(&pk, "example.com", &CDH).unwrap();
|
||||
let mut ad = hex("a379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce1947");
|
||||
ad.extend([0x1d, 0, 0, 0, 0]);
|
||||
assert_eq!(a.authenticator_data, ad);
|
||||
assert_eq!(KESTROVE_AAGUID.to_string(), "e63b1a47-0244-453d-aef9-981639609295");
|
||||
}
|
||||
Reference in New Issue
Block a user