//! Software-authenticator coverage for enrollment gates and bound assertions. use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use sausage::cred::webauthn::{Binding, Decision, Gate, Passkeys}; use webauthn_authenticator_rs::{WebauthnAuthenticator, softpasskey::SoftPasskey}; use webauthn_rs::prelude::*; fn binding() -> Binding { Binding::Decision { request_id: "request-1".into(), decision: Decision::Approve, overrides: sausage::cred::requests::Overrides::default(), payload_sha256: "synthetic-payload".into(), } } #[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)?.binding(), &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(()) }