#![deny(missing_docs)]
pub mod components;
pub mod keyring;
pub mod message_signatures;
use components::CoveredComponent;
use message_signatures::{MessageVerifier, ParsedLabel, SignatureTiming, SignedMessage};
use data_url::DataUrl;
use keyring::{Algorithm, JSONWebKeySet, KeyRing};
use crate::components::{HTTPField, HTTPFieldParameters};
#[derive(Debug)]
pub enum ImplementationError {
ImpossibleSfvError(sfv::Error),
ParsingError(String),
LookupError(CoveredComponent),
UnsupportedAlgorithm(Algorithm),
NoSuchKey,
InvalidKeyLength,
InvalidSignatureLength,
FailedToVerify(ed25519_dalek::SignatureError),
NonAsciiContentFound,
SignatureParamsSerialization,
WebBotAuth(WebBotAuthError),
}
impl core::fmt::Display for ImplementationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImplementationError::ImpossibleSfvError(e) => {
write!(f, "impossible structured field value error: {e}")
}
ImplementationError::ParsingError(s) => write!(f, "parsing error: {s}"),
ImplementationError::LookupError(component) => {
write!(f, "lookup error: component not found: {component:?}")
}
ImplementationError::UnsupportedAlgorithm(alg) => {
write!(f, "unsupported algorithm: {alg:?}")
}
ImplementationError::NoSuchKey => write!(f, "no such key"),
ImplementationError::InvalidKeyLength => write!(f, "invalid key length"),
ImplementationError::InvalidSignatureLength => write!(f, "invalid signature length"),
ImplementationError::FailedToVerify(e) => {
write!(f, "signature verification failed: {e}")
}
ImplementationError::NonAsciiContentFound => {
write!(f, "non-ASCII content found in signature base")
}
ImplementationError::SignatureParamsSerialization => {
write!(f, "failed to serialize signature params")
}
ImplementationError::WebBotAuth(e) => write!(f, "web bot auth error: {e}"),
}
}
}
impl core::error::Error for ImplementationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ImplementationError::ImpossibleSfvError(e) => Some(e),
ImplementationError::FailedToVerify(e) => Some(e),
ImplementationError::WebBotAuth(e) => Some(e),
_ => None,
}
}
}
#[derive(Debug)]
pub enum WebBotAuthError {
SignatureIsExpired,
}
impl core::fmt::Display for WebBotAuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WebBotAuthError::SignatureIsExpired => write!(f, "signature is expired"),
}
}
}
impl core::error::Error for WebBotAuthError {}
#[derive(Clone, Debug)]
pub struct WebBotAuthVerifier {
message_verifier: MessageVerifier,
parsed_directories: Vec<SignatureAgentLink>,
}
#[derive(Eq, PartialEq, Debug, Clone)]
pub enum SignatureAgentLink {
Inline(JSONWebKeySet),
External(String),
}
impl WebBotAuthVerifier {
pub fn parse(message: &impl SignedMessage) -> Result<Self, ImplementationError> {
let signature_agents = message.lookup_component(&CoveredComponent::HTTP(HTTPField {
name: "signature-agent".to_string(),
parameters: components::HTTPFieldParametersSet(vec![]),
}));
let message_verifier =
MessageVerifier::parse(message, |(_, innerlist)| {
innerlist.params.contains_key("keyid")
&& innerlist.params.contains_key("tag")
&& innerlist.params.contains_key("expires")
&& innerlist.params.contains_key("created")
&& innerlist
.params
.get("tag")
.and_then(|tag| tag.as_string())
.is_some_and(|tag| tag.as_str() == "web-bot-auth")
&& (innerlist.items.iter().any(|item| {
*item == sfv::Item::new(sfv::StringRef::constant("@authority"))
}) || innerlist.items.iter().any(|item| {
*item == sfv::Item::new(sfv::StringRef::constant("@target-uri"))
}))
&& (if !signature_agents.is_empty() {
innerlist.items.iter().any(|item| {
item.bare_item
.as_string()
.is_some_and(|i| i == sfv::StringRef::constant("signature-agent"))
})
} else {
true
})
})?;
let mut signature_agent_key: Option<String> = None;
'outer_loop: for (component, _) in message_verifier.parsed.base.components.iter() {
if let CoveredComponent::HTTP(HTTPField { name, parameters }) = component
&& name == "signature-agent"
{
for parameter in parameters.0.iter() {
if let HTTPFieldParameters::Key(key) = parameter {
signature_agent_key = Some(key.clone());
break 'outer_loop;
}
}
}
}
let parse_link = |link: &sfv::StringRef| {
let link_str = link.as_str();
if link_str.starts_with("https://") || link_str.starts_with("http://") {
return Some(SignatureAgentLink::External(String::from(link_str)));
}
if let Ok(url) = DataUrl::process(link_str) {
let mediatype = url.mime_type();
if mediatype.type_ == "application"
&& mediatype.subtype == "http-message-signatures-directory"
&& let Ok((body, _)) = url.decode_to_vec()
&& let Ok(jwks) = serde_json::from_slice::<JSONWebKeySet>(&body)
{
return Some(SignatureAgentLink::Inline(jwks));
}
}
None
};
let parsed_directories = match signature_agent_key {
Some(key) => signature_agents
.iter()
.filter_map(|header| sfv::Parser::new(header).parse_dictionary().ok())
.reduce(|mut acc, sig_agent| {
acc.extend(sig_agent);
acc
})
.ok_or(ImplementationError::ParsingError(
"Failed to parse `Signature-Agent` into valid sfv::Dictionary".to_string(),
))?
.into_iter()
.filter_map(|(label, listentry)| match listentry {
sfv::ListEntry::Item(item) => Some((label, item)),
sfv::ListEntry::InnerList(_) => None,
})
.filter_map(|(label, item)| {
if label.as_str() != key {
return None;
}
let as_string = item.bare_item.as_string();
as_string.and_then(parse_link)
})
.collect(),
None => signature_agents
.iter()
.map(|header| {
sfv::Parser::new(header).parse_item().map_err(|e| {
ImplementationError::ParsingError(format!(
"Failed to parse `Signature-Agent` into valid sfv::Item: {e}"
))
})
})
.collect::<Result<Vec<_>, _>>()?
.iter()
.flat_map(|item| {
let as_string = item.bare_item.as_string();
as_string.and_then(parse_link)
})
.collect(),
};
let web_bot_auth_verifier = Self {
message_verifier,
parsed_directories,
};
Ok(web_bot_auth_verifier)
}
pub fn get_signature_agents(&self) -> &Vec<SignatureAgentLink> {
&self.parsed_directories
}
pub fn verify(
self,
keyring: &KeyRing,
key_id: Option<String>,
) -> Result<SignatureTiming, ImplementationError> {
self.message_verifier.verify(keyring, key_id)
}
pub fn get_parsed_label(&self) -> &ParsedLabel {
&self.message_verifier.parsed
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use components::DerivedComponent;
use indexmap::IndexMap;
use super::*;
struct StandardTestVector {}
impl SignedMessage for StandardTestVector {
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
match name {
CoveredComponent::HTTP(HTTPField { name, .. }) => {
if name == "signature" {
return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
}
if name == "signature-input" {
return vec![r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="web-bot-auth""#.to_owned()];
}
vec![]
}
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
vec!["example.com".to_string()]
}
_ => vec![],
}
}
}
#[test]
fn test_verifying_as_web_bot_auth() {
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();
let advisory = verifier
.get_parsed_label()
.base
.parameters
.details
.possibly_insecure(|_| false);
assert!(advisory.is_expired.unwrap_or(true));
assert!(!advisory.nonce_is_invalid.unwrap_or(true));
let timing = verifier.verify(&keyring, None).unwrap();
assert!(timing.generation.as_nanos() > 0);
assert!(timing.verification.as_nanos() > 0);
}
#[test]
fn test_signing_then_verifying() {
struct MyTest {
signature_input: String,
signature_header: String,
}
impl message_signatures::UnsignedMessage for MyTest {
fn fetch_components_to_cover(&self) -> IndexMap<CoveredComponent, String> {
IndexMap::from_iter([(
CoveredComponent::Derived(DerivedComponent::Authority { req: false }),
"example.com".to_string(),
)])
}
fn register_header_contents(
&mut self,
signature_input: String,
signature_header: String,
) {
self.signature_input = format!("sig1={signature_input}");
self.signature_header = format!("sig1={signature_header}");
}
}
impl SignedMessage for MyTest {
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
match name {
CoveredComponent::HTTP(HTTPField { name, .. }) => {
if name == "signature" {
return vec![self.signature_header.clone()];
}
if name == "signature-input" {
return vec![self.signature_input.clone()];
}
vec![]
}
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
vec!["example.com".to_string()]
}
_ => vec![],
}
}
}
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 private_key: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [
0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c,
0x0e, 0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f,
0x6a, 0x7d, 0x29, 0xc5,
];
let mut keyring = KeyRing::default();
keyring.import_raw(
"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
Algorithm::Ed25519,
public_key.to_vec(),
);
let signer = message_signatures::MessageSigner {
keyid: "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".into(),
nonce: "end-to-end-test".into(),
tag: "web-bot-auth".into(),
};
let mut mytest = MyTest {
signature_input: String::new(),
signature_header: String::new(),
};
signer
.generate_signature_headers_content(
&mut mytest,
Duration::from_secs(10),
Algorithm::Ed25519,
&private_key,
)
.unwrap();
let verifier = WebBotAuthVerifier::parse(&mytest).unwrap();
let advisory = verifier
.get_parsed_label()
.base
.parameters
.details
.possibly_insecure(|_| false);
assert!(!advisory.is_expired.unwrap_or(true));
assert!(!advisory.nonce_is_invalid.unwrap_or(true));
let timing = verifier.verify(&keyring, None).unwrap();
assert!(timing.generation.as_nanos() > 0);
assert!(timing.verification.as_nanos() > 0);
}
#[test]
fn test_missing_tags_break_web_bot_auth() {
struct MissingParametersTestVector {}
impl SignedMessage for MissingParametersTestVector {
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
match name {
CoveredComponent::HTTP(HTTPField { name, .. }) => {
if name == "signature" {
return vec![
"sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()
];
}
if name == "signature-input" {
return vec![r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="not-web-bot-auth""#.to_owned()];
}
vec![]
}
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
vec!["example.com".to_string()]
}
_ => vec![],
}
}
}
let test = MissingParametersTestVector {};
WebBotAuthVerifier::parse(&test).expect_err("This should not have parsed");
}
#[test]
fn test_signature_agents_are_required_in_signature_input() {
struct MissingParametersTestVector {}
impl SignedMessage for MissingParametersTestVector {
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
match name {
CoveredComponent::HTTP(HTTPField { name, .. }) => {
if name == "signature" {
return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
}
if name == "signature-input" {
return vec![r#"sig1=("@authority");created=1735689600;keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";alg="ed25519";expires=1735693200;nonce="gubxywVx7hzbYKatLgzuKDllDAIXAkz41PydU7aOY7vT+Mb3GJNxW0qD4zJ+IOQ1NVtg+BNbTCRUMt1Ojr5BgA==";tag="web-bot-auth""#.to_owned()];
}
if name == "signature-agent" {
return vec![String::from("\"https://myexample.com\"")];
}
vec![]
}
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
vec!["example.com".to_string()]
}
_ => vec![],
}
}
}
let test = MissingParametersTestVector {};
WebBotAuthVerifier::parse(&test).expect_err("This should not have parsed");
}
#[test]
fn test_signature_agents_are_parsed_with_fallback() {
struct StandardTestVector {}
impl SignedMessage for StandardTestVector {
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
match name {
CoveredComponent::HTTP(HTTPField { name, .. }) => {
if name == "signature" {
return vec!["sig1=:3q7S1TtbrFhQhpcZ1gZwHPCFHTvdKXNY1xngkp6lyaqqqv3QZupwpu/wQG5a7qybnrj2vZYMeVKuWepm+rNkDw==:".to_owned()];
}
if name == "signature-input" {
return vec![r#"sig1=("@authority" "signature-agent");alg="ed25519";keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";nonce="ZO3/XMEZjrvSnLtAP9M7jK0WGQf3J+pbmQRUpKDhF9/jsNCWqUh2sq+TH4WTX3/GpNoSZUa8eNWMKqxWp2/c2g==";tag="web-bot-auth";created=1749331474;expires=1749331484"#.to_owned()];
}
if name == "signature-agent" {
return vec![
String::from("\"https://myexample.com\""),
String::from("\"https://myexample2.com\""),
];
}
vec![]
}
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
vec!["example.com".to_string()]
}
_ => vec![],
}
}
}
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 test = StandardTestVector {};
let verifier = WebBotAuthVerifier::parse(&test).unwrap();
assert_eq!(verifier.get_signature_agents().len(), 2);
assert_eq!(
verifier.get_signature_agents()[0],
SignatureAgentLink::External("https://myexample.com".to_string())
);
}
#[test]
fn test_signature_agents_are_parsed_correctly() {
struct StandardTestVector {}
impl SignedMessage for StandardTestVector {
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
match name {
CoveredComponent::HTTP(HTTPField { name, .. }) => {
if name == "signature" {
return vec!["sig1=:3q7S1TtbrFhQhpcZ1gZwHPCFHTvdKXNY1xngkp6lyaqqqv3QZupwpu/wQG5a7qybnrj2vZYMeVKuWepm+rNkDw==:".to_owned()];
}
if name == "signature-input" {
return vec![r#"sig1=("@authority" "signature-agent";key="agent1");alg="ed25519";keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";nonce="ZO3/XMEZjrvSnLtAP9M7jK0WGQf3J+pbmQRUpKDhF9/jsNCWqUh2sq+TH4WTX3/GpNoSZUa8eNWMKqxWp2/c2g==";tag="web-bot-auth";created=1749331474;expires=1749331484"#.to_owned()];
}
if name == "signature-agent" {
return vec![
r#"agent1="https://myexample.com", agent2="https://example2.com""#
.to_owned(),
];
}
vec![]
}
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
vec!["example.com".to_string()]
}
_ => vec![],
}
}
}
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 test = StandardTestVector {};
let verifier = WebBotAuthVerifier::parse(&test).unwrap();
assert_eq!(verifier.get_signature_agents().len(), 1);
assert_eq!(
verifier.get_signature_agents()[0],
SignatureAgentLink::External("https://myexample.com".to_string())
);
}
}