Validation remains blocked by rusqlite u64 conversions in cred/grants.rs, which T19 owns.
125 lines
5.3 KiB
Rust
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(®istration.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(())
|
|
}
|