diff --git a/crates/web-bot-auth/src/lib.rs b/crates/web-bot-auth/src/lib.rs index cb7f385..44987be 100644 --- a/crates/web-bot-auth/src/lib.rs +++ b/crates/web-bot-auth/src/lib.rs @@ -32,7 +32,9 @@ use data_url::DataUrl; use components::{CoveredComponent, HTTPField, HTTPFieldParameters}; use keyring::{Algorithm, JSONWebKeySet, KeyRing}; -use message_signatures::{MessageVerifier, ParsedLabel, SignatureTiming, SignedMessage}; +use message_signatures::{ + MessageVerifier, ParsedLabel, SecurityAdvisory, SignatureTiming, SignedMessage, +}; use registry::{SignatureAgentDiscoveryType, parse_signature_agent_header}; /// Errors that may be thrown by this module. @@ -150,6 +152,18 @@ pub struct WebBotAuthVerifier { parsed_directories: Vec, } +/// The outcome of [`WebBotAuthVerifier::verify_ignore_expiry`]: cryptographic +/// verification has completed, and advisory conditions that the strict +/// [`WebBotAuthVerifier::verify`] would have enforced are reported instead. +#[derive(Clone, Debug)] +pub struct AdvisoryVerification { + /// Micro-measurements of the verification process. + pub timing: SignatureTiming, + /// Advisory observed before verification, e.g. whether the signature + /// window had already expired. + pub advisory: SecurityAdvisory, +} + /// The different types of URLs a `Signature-Agent` can have. #[derive(Eq, PartialEq, Debug, Clone)] pub enum SignatureAgentLink { @@ -315,14 +329,57 @@ impl WebBotAuthVerifier { /// If `key_id` is not supplied, a key ID to fetch the public key /// from `keyring` will be sourced from the `keyid` parameter /// within the message. + /// + /// This fails closed when `expires` is in the past (or unparsable), + /// returning [`WebBotAuthError::SignatureIsExpired`] before cryptographic + /// verification. That matches the TypeScript `http-message-sig` verifier. + /// Callers that must process expired-but-valid signatures can opt into + /// [`Self::verify_ignore_expiry`]. pub fn verify( self, keyring: &KeyRing, key_id: Option, ) -> Result { + let advisory = self + .message_verifier + .parsed + .base + .parameters + .details + .possibly_insecure(|_| false); + // Web Bot Auth parse requires `expires`; treat missing/unparsable as expired. + if advisory.is_expired.unwrap_or(true) { + return Err(ImplementationError::WebBotAuth( + WebBotAuthError::SignatureIsExpired, + )); + } self.message_verifier.verify(keyring, key_id) } + /// Verify the message like [`Self::verify`], but without failing closed on + /// the `expires` window: full cryptographic verification always runs, and + /// the pre-verification [`SecurityAdvisory`] is returned alongside the + /// timing so the caller can decide how to treat an expired signature. + /// + /// RFC 9421 leaves enforcement of application requirements such as expiry + /// to the application; this opt-in serves verifiers that need to parse and + /// judge such signatures themselves rather than reject them up front. + pub fn verify_ignore_expiry( + self, + keyring: &KeyRing, + key_id: Option, + ) -> Result { + let advisory = self + .message_verifier + .parsed + .base + .parameters + .details + .possibly_insecure(|_| false); + let timing = self.message_verifier.verify(keyring, key_id)?; + Ok(AdvisoryVerification { timing, advisory }) + } + /// Retrieve the contents of the chosen signature and signature input label for /// verification. pub fn get_parsed_label(&self) -> &ParsedLabel { @@ -386,10 +443,34 @@ mod tests { // Since the expiry date is in the past. assert!(advisory.is_expired.unwrap_or(true)); assert!(!advisory.nonce_is_invalid.unwrap_or(true)); - let timing = verifier.verify(&keyring, None).unwrap(); + // WebBotAuthVerifier::verify must fail closed on expired signatures. + let err = verifier.verify(&keyring, None).unwrap_err(); + assert!(matches!( + err, + ImplementationError::WebBotAuth(WebBotAuthError::SignatureIsExpired) + )); + } - assert!(timing.generation.as_nanos() > 0); - assert!(timing.verification.as_nanos() > 0); + #[test] + fn test_verify_ignore_expiry_on_expired_signature() { + let test = StandardTestVector {}; + let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [ + 0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58, + 0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd, + 0xce, 0x43, 0xd1, 0xbb, + ]; + let mut keyring = KeyRing::default(); + keyring.import_raw( + "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(), + Algorithm::Ed25519, + public_key.to_vec(), + ); + let verifier = WebBotAuthVerifier::parse(&test).unwrap(); + // Opt-in path: full cryptographic verification runs despite the expired + // window, and the expiry surfaces as an advisory instead of an error. + let outcome = verifier.verify_ignore_expiry(&keyring, None).unwrap(); + assert!(outcome.advisory.is_expired.unwrap_or(true)); + assert!(!outcome.advisory.nonce_is_invalid.unwrap_or(true)); } #[test] diff --git a/crates/web-bot-auth/src/message_signatures.rs b/crates/web-bot-auth/src/message_signatures.rs index 15c1d63..d15e880 100644 --- a/crates/web-bot-auth/src/message_signatures.rs +++ b/crates/web-bot-auth/src/message_signatures.rs @@ -92,6 +92,7 @@ impl From for SignatureParams { /// Advises whether or not to accept the message as valid prior to /// verification, based on a cursory examination of the message parameters. +#[derive(Clone, Debug)] pub struct SecurityAdvisory { /// If the `expires` tag was present on the message, whether or not /// the message expired in the past. diff --git a/examples/rust/verify.rs b/examples/rust/verify.rs index 2694148..3803d18 100644 --- a/examples/rust/verify.rs +++ b/examples/rust/verify.rs @@ -13,7 +13,7 @@ // limitations under the License. use web_bot_auth::{ - SignatureAgentLink, WebBotAuthVerifier, + ImplementationError, SignatureAgentLink, WebBotAuthError, WebBotAuthVerifier, components::{CoveredComponent, DerivedComponent, HTTPField}, keyring::{Algorithm, KeyRing}, message_signatures::SignedMessage, @@ -77,5 +77,16 @@ fn main() { // Since the expiry date is in the past. assert!(advisory.is_expired.unwrap_or(true)); assert!(!advisory.nonce_is_invalid.unwrap_or(true)); - assert!(verifier.verify(&keyring, None).is_ok()); + assert!(matches!( + verifier.verify(&keyring, None), + Err(ImplementationError::WebBotAuth( + WebBotAuthError::SignatureIsExpired + )) + )); + + // Opt-in advisory path: full cryptographic verification still runs, and + // the expired window is reported as a warning instead of an error. + let verifier = WebBotAuthVerifier::parse(&test).unwrap(); + let outcome = verifier.verify_ignore_expiry(&keyring, None).unwrap(); + assert!(outcome.advisory.is_expired.unwrap_or(true)); }