cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix(auth): reject malformed hmac-secret responses

Dami committed 62b8518
Return actionable fail-closed errors for empty assertion lists and invalid output lengths. Add regression coverage for empty, malformed, and valid authenticator responses.
commit
62b851816139b4469efafe74324939be63d9958e
parent
bb4cb785c453c30ae9245131ae9d154a9d0c0a34
author
Dami <dami.etoile@gmail.com>
1 file changed, +51 -5
Msrc-tauri/src/commands/auth.rs56+++++++++++++++++++++++++++---
M src-tauri/src/commands/auth.rs +51 -5
@@ -77,14 +77,27 @@ pub fn derive_secret_from_key(pin: &str) -> anyhow::Result<[u8; 32]> {
7777 }7878 7979 pub fn extract_hmac_secret(assertions: &[Assertion]) -> anyhow::Result<[u8; 32]> {80- for ext in &assertions[0].extensions {80+ let assertion = assertions.first().context(81+ "Security key returned no assertions. Reconnect the key and try the PIN and tap again.",82+ )?;83+84+ for ext in &assertion.extensions {8185 if let Gext::HmacSecret(Some(output)) = ext {82- let mut secret = [0u8; 32];83- secret.copy_from_slice(&output[..]);84- return Ok(secret);86+ return validate_hmac_secret_output(output);8587 }8688 }87- anyhow::bail!("No hmac-secret in assertion response")89+ anyhow::bail!(90+ "Security key assertion did not contain hmac-secret output. Use a compatible FIDO2 key and try again."91+ )92+}93+94+fn validate_hmac_secret_output(output: &[u8]) -> anyhow::Result<[u8; 32]> {95+ output.try_into().map_err(|_| {96+ anyhow::anyhow!(97+ "Security key returned malformed hmac-secret output: expected 32 bytes, received {}. Reconnect the key or try a compatible FIDO2 key.",98+ output.len()99+ )100+ })88101 }89102 90103 pub fn derive_iroh_secret_from_key(pin: &str) -> anyhow::Result<SecretKey> {
@@ -245,6 +258,39 @@ pub async fn key_derive_identity(pin: String) -> KeyIdentity {
245258 mod tests {246259 use super::*;247260 261+ #[test]262+ fn extract_hmac_secret_rejects_empty_assertion_list() {263+ let error = extract_hmac_secret(&[]).expect_err("empty assertions must fail closed");264+ assert_eq!(265+ error.to_string(),266+ "Security key returned no assertions. Reconnect the key and try the PIN and tap again."267+ );268+ }269+270+ #[test]271+ fn validate_hmac_secret_output_rejects_non_32_byte_values() {272+ for output in [&[0x2a; 31][..], &[0x2a; 33][..]] {273+ let error =274+ validate_hmac_secret_output(output).expect_err("invalid length must fail closed");275+ assert!(276+ error277+ .to_string()278+ .contains(&format!("expected 32 bytes, received {}", output.len()))279+ );280+ }281+ }282+283+ #[test]284+ fn extract_hmac_secret_returns_exact_output() {285+ let expected = [0x2a; 32];286+ let assertion = Assertion {287+ extensions: vec![Gext::HmacSecret(Some(expected))],288+ ..Default::default()289+ };290+291+ assert_eq!(extract_hmac_secret(&[assertion]).unwrap(), expected);292+ }293+248294 #[test]249295 fn portal_identity_derivation_is_stable_and_domain_separated() {250296 let root = [0x2a; 32];