Files
sausage/host/tests/cred_webauthn.rs
T
surma 1dc09bc418 cred: add passkey challenges and encrypted secret entry
Validation remains blocked by rusqlite u64 conversions in cred/grants.rs, which T19 owns.
2026-11-06 19:04:42 +00:00

125 lines
5.3 KiB
Rust

//! Software-authenticator coverage for enrollment gates and bound assertions.
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use sausage::cred::webauthn::{Binding, Gate, Passkeys};
use webauthn_authenticator_rs::{WebauthnAuthenticator, softpasskey::SoftPasskey};
use webauthn_rs::prelude::*;
fn binding() -> Binding {
Binding::Decision {
request_id: "request-1".into(),
decision: "approve".into(),
overrides: serde_json::json!({}),
}
}
#[test]
fn enrollment_and_assertions() -> anyhow::Result<()> {
let db = rusqlite::Connection::open_in_memory()?;
let origin = Url::parse("http://localhost:8000")?;
let mut host = Passkeys::new(&db, "localhost", origin.as_str())?;
let mut auth = WebauthnAuthenticator::new(SoftPasskey::new(true));
assert!(host.is_unlocked());
let start = host.start_registration("first".into(), None)?;
let racing = host.start_registration("racing".into(), None)?;
let response = auth.do_registration(origin.clone(), start.options)?;
host.finish_registration(&start.id, response)?;
assert!(!host.is_unlocked());
assert!(host.start_registration("second".into(), None).is_err());
let response = auth.do_registration(origin.clone(), racing.options)?;
assert!(host.finish_registration(&racing.id, response).is_err());
let gate = host.start_assertion(Binding::Enrollment {
label: "second".into(),
})?;
let response = auth.do_authentication(origin.clone(), gate.options)?;
let start = host.start_registration(
"second".into(),
Some(Gate {
id: gate.id,
response,
}),
)?;
let response = auth.do_registration(origin.clone(), start.options)?;
host.finish_registration(&start.id, response)?;
let start = host.start_assertion(binding())?;
let response = auth.do_authentication(origin.clone(), start.options)?;
let replay = serde_json::to_value(&response)?;
assert_eq!(host.finish_assertion(&start.id, response)?.1, binding());
assert!(
host.finish_assertion(&start.id, serde_json::from_value(replay)?)
.is_err()
);
let gate = host.start_assertion(binding())?;
let response = auth.do_authentication(origin.clone(), gate.options)?;
assert!(
host.start_registration(
"unauthorized".into(),
Some(Gate {
id: gate.id,
response
})
)
.is_err()
);
drop(host);
let mut host = Passkeys::new(&db, "localhost", origin.as_str())?;
let older = host.start_assertion(binding())?;
let newer = host.start_assertion(binding())?;
let old_response = auth.do_authentication(origin.clone(), older.options)?;
let new_response = auth.do_authentication(origin.clone(), newer.options)?;
host.finish_assertion(&newer.id, new_response)?;
assert!(host.finish_assertion(&older.id, old_response).is_err());
// Sign a different, valid random challenge with the same credential.
let a = host.start_assertion(binding())?;
let b = host.start_assertion(binding())?;
let response = auth.do_authentication(origin.clone(), b.options)?;
assert!(host.finish_assertion(&a.id, response).is_err());
let a = host.start_assertion(binding())?;
let response = auth.do_authentication(Url::parse("http://localhost:8001")?, a.options)?;
assert!(host.finish_assertion(&a.id, response).is_err());
let mut a = host.start_assertion(binding())?;
a.options.public_key.user_verification =
serde_json::from_value(serde_json::json!("discouraged"))?;
let response = auth.do_authentication(origin.clone(), a.options)?;
assert!(host.finish_assertion(&a.id, response).is_err());
for field in ["signature", "authenticatorData"] {
let a = host.start_assertion(binding())?;
let response = auth.do_authentication(origin.clone(), a.options)?;
let mut json = serde_json::to_value(response)?;
let mut bytes = URL_SAFE_NO_PAD.decode(json["response"][field].as_str().unwrap())?;
let index = if field == "signature" {
bytes.len() - 1
} else {
0
};
bytes[index] ^= 1; // Preserve signature encoding, or change rpIdHash.
json["response"][field] = URL_SAFE_NO_PAD.encode(bytes).into();
assert!(
host.finish_assertion(&a.id, serde_json::from_value(json)?)
.is_err()
);
}
let foreign_db = rusqlite::Connection::open_in_memory()?;
let mut foreign = Passkeys::new(&foreign_db, "localhost", origin.as_str())?;
let mut foreign_auth = WebauthnAuthenticator::new(SoftPasskey::new(true));
let registration = foreign.start_registration("foreign".into(), None)?;
let response = foreign_auth.do_registration(origin.clone(), registration.options)?;
let credential = foreign.finish_registration(&registration.id, response)?;
let mut a = host.start_assertion(binding())?;
a.options.public_key.allow_credentials[0].id = credential.cred_id().to_vec().into();
let response = foreign_auth.do_authentication(origin.clone(), a.options)?;
assert!(host.finish_assertion(&a.id, response).is_err());
let a = host.start_assertion(binding())?;
let response = auth.do_authentication(origin, a.options)?;
db.execute("DELETE FROM passkey_credentials", [])?;
assert!(host.finish_assertion(&a.id, response).is_err());
Ok(())
}