Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 85 additions & 4 deletions crates/web-bot-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -150,6 +152,18 @@ pub struct WebBotAuthVerifier {
parsed_directories: Vec<SignatureAgentLink>,
}

/// 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 {
Expand Down Expand Up @@ -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<String>,
) -> Result<SignatureTiming, ImplementationError> {
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<String>,
) -> Result<AdvisoryVerification, ImplementationError> {
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 {
Expand Down Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions crates/web-bot-auth/src/message_signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ impl From<sfv::Parameters> 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.
Expand Down
15 changes: 13 additions & 2 deletions examples/rust/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}