Skip to content

Commit 34a5bdb

Browse files
committed
Simplify interface: concentrate into lookup_component method
This change removes the following constructs: 1. `WebBotAuthSignedMessage` 2. `SignedMessage::fetch_all_signature_headers` 3. `SignedMessage::fetch_all_signature_inputs` and modifies the `lookup_component` method to be the primary source of information about a message. The goal here is to make it possible to treat all HTTP headers, including special ones like `Signature-Agent`, etc. on the same footing. It also removes a source of complexity for implementors, since they would need to maintain an independent method just to fetch special values.
1 parent 36b5495 commit 34a5bdb

File tree

8 files changed

+209
-191
lines changed

8 files changed

+209
-191
lines changed

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ sha2 = "0.10.9"
3030
base64 = "0.22.1"
3131
serde_json = "1.0.140"
3232
data-url = "0.3.1"
33+
regex = "1.12.2"
3334

3435
# workspace dependencies
3536
web-bot-auth = { version = "0.5.1", path = "./crates/web-bot-auth" }

crates/http-signature-directory/src/main.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use reqwest::{
1111
};
1212
use serde::{Deserialize, Serialize};
1313
use web_bot_auth::{
14-
components::{CoveredComponent, DerivedComponent},
14+
components::{CoveredComponent, DerivedComponent, HTTPField},
1515
keyring::{JSONWebKeySet, KeyRing, Thumbprintable},
1616
message_signatures::{MessageVerifier, SignedMessage},
1717
};
@@ -68,18 +68,21 @@ struct SignedDirectory {
6868
}
6969

7070
impl SignedMessage for SignedDirectory {
71-
fn fetch_all_signature_headers(&self) -> Vec<String> {
72-
self.signature.clone()
73-
}
74-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
75-
self.input.clone()
76-
}
77-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
78-
match *name {
71+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
72+
match name {
7973
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
80-
Some(self.authority.clone())
74+
vec![self.authority.clone()]
75+
}
76+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
77+
if name == "signature" {
78+
return self.signature.clone();
79+
}
80+
if name == "signature-input" {
81+
return self.input.clone();
82+
}
83+
vec![]
8184
}
82-
_ => None,
85+
_ => vec![],
8386
}
8487
}
8588
}

crates/web-bot-auth/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ categories.workspace = true
1515
[dependencies]
1616
ed25519-dalek = { workspace = true }
1717
indexmap = { workspace = true }
18+
regex = { workspace = true }
1819
sfv = { workspace = true }
1920
serde = { workspace = true }
2021
serde_json = { workspace = true }

crates/web-bot-auth/src/lib.rs

Lines changed: 97 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ use data_url::DataUrl;
3232
use keyring::{Algorithm, JSONWebKeySet, KeyRing};
3333
use std::time::SystemTimeError;
3434

35+
use crate::components::HTTPField;
36+
3537
/// Errors that may be thrown by this module.
3638
#[derive(Debug)]
3739
pub enum ImplementationError {
@@ -91,16 +93,6 @@ pub enum WebBotAuthError {
9193
/// and `creates` method.
9294
SignatureIsExpired,
9395
}
94-
/// A trait that messages wishing to be verified as a `web-bot-auth` method specifically
95-
/// must implement.
96-
pub trait WebBotAuthSignedMessage: SignedMessage {
97-
/// Obtain every `Signature-Agent` header in the message. Despite the name, you can omit
98-
/// `Signature-Agents` that are known to be invalid ahead of time. However, each `Signature-Agent`
99-
/// header must be unparsed and a be a valid sfv::Item::String value (meaning it should be encased
100-
/// in double quotes). You should separately implement looking this up in `SignedMessage::lookup_component`
101-
/// as an HTTP header with multiple values.
102-
fn fetch_all_signature_agents(&self) -> Vec<String>;
103-
}
10496

10597
/// A verifier for Web Bot Auth messages specifically.
10698
#[derive(Clone, Debug)]
@@ -127,10 +119,14 @@ impl WebBotAuthVerifier {
127119
/// # Errors
128120
///
129121
/// Returns `ImplementationErrors` relevant to verifying and parsing.
130-
pub fn parse(message: &impl WebBotAuthSignedMessage) -> Result<Self, ImplementationError> {
131-
let signature_agents = message.fetch_all_signature_agents();
132-
let web_bot_auth_verifier = Self {
133-
message_verifier: MessageVerifier::parse(message, |(_, innerlist)| {
122+
pub fn parse(message: &impl SignedMessage) -> Result<Self, ImplementationError> {
123+
let signature_agents = message.lookup_component(&CoveredComponent::HTTP(HTTPField {
124+
name: "signature-agent".to_string(),
125+
parameters: components::HTTPFieldParametersSet(vec![]),
126+
}));
127+
128+
let message_verifier =
129+
MessageVerifier::parse(message, |(_, innerlist)| {
134130
innerlist.params.contains_key("keyid")
135131
&& innerlist.params.contains_key("tag")
136132
&& innerlist.params.contains_key("expires")
@@ -140,18 +136,22 @@ impl WebBotAuthVerifier {
140136
.get("tag")
141137
.and_then(|tag| tag.as_string())
142138
.is_some_and(|tag| tag.as_str() == "web-bot-auth")
143-
&& innerlist
144-
.items
145-
.iter()
146-
.any(|item| *item == sfv::Item::new(sfv::StringRef::constant("@authority")))
139+
&& (innerlist.items.iter().any(|item| {
140+
*item == sfv::Item::new(sfv::StringRef::constant("@authority"))
141+
}) || innerlist.items.iter().any(|item| {
142+
*item == sfv::Item::new(sfv::StringRef::constant("@target-uri"))
143+
}))
147144
&& (if !signature_agents.is_empty() {
148145
innerlist.items.iter().any(|item| {
149146
*item == sfv::Item::new(sfv::StringRef::constant("signature-agent"))
150147
})
151148
} else {
152149
true
153150
})
154-
})?,
151+
})?;
152+
153+
let web_bot_auth_verifier = Self {
154+
message_verifier,
155155
parsed_directories: signature_agents
156156
.iter()
157157
.map(|header| {
@@ -230,28 +230,26 @@ mod tests {
230230
struct StandardTestVector {}
231231

232232
impl SignedMessage for StandardTestVector {
233-
fn fetch_all_signature_headers(&self) -> Vec<String> {
234-
vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()]
235-
}
236-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
237-
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()]
238-
}
239-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
240-
match *name {
233+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
234+
match name {
235+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
236+
if name == "signature" {
237+
return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
238+
}
239+
240+
if name == "signature-input" {
241+
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()];
242+
}
243+
vec![]
244+
}
241245
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
242-
Some("example.com".to_string())
246+
vec!["example.com".to_string()]
243247
}
244-
_ => None,
248+
_ => vec![],
245249
}
246250
}
247251
}
248252

249-
impl WebBotAuthSignedMessage for StandardTestVector {
250-
fn fetch_all_signature_agents(&self) -> Vec<String> {
251-
vec![]
252-
}
253-
}
254-
255253
#[test]
256254
fn test_verifying_as_web_bot_auth() {
257255
let test = StandardTestVector {};
@@ -307,28 +305,26 @@ mod tests {
307305
}
308306

309307
impl SignedMessage for MyTest {
310-
fn fetch_all_signature_headers(&self) -> Vec<String> {
311-
vec![self.signature_header.clone()]
312-
}
313-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
314-
vec![self.signature_input.clone()]
315-
}
316-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
317-
match *name {
308+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
309+
match name {
310+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
311+
if name == "signature" {
312+
return vec![self.signature_header.clone()];
313+
}
314+
315+
if name == "signature-input" {
316+
return vec![self.signature_input.clone()];
317+
}
318+
vec![]
319+
}
318320
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
319-
Some("example.com".to_string())
321+
vec!["example.com".to_string()]
320322
}
321-
_ => None,
323+
_ => vec![],
322324
}
323325
}
324326
}
325327

326-
impl WebBotAuthSignedMessage for MyTest {
327-
fn fetch_all_signature_agents(&self) -> Vec<String> {
328-
vec![]
329-
}
330-
}
331-
332328
let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
333329
0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
334330
0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,
@@ -388,30 +384,28 @@ mod tests {
388384
struct MissingParametersTestVector {}
389385

390386
impl SignedMessage for MissingParametersTestVector {
391-
fn fetch_all_signature_headers(&self) -> Vec<String> {
392-
vec![
393-
"sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()
394-
]
395-
}
396-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
397-
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()]
398-
}
399-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
400-
match *name {
387+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
388+
match name {
389+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
390+
if name == "signature" {
391+
return vec![
392+
"sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()
393+
];
394+
}
395+
396+
if name == "signature-input" {
397+
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()];
398+
}
399+
vec![]
400+
}
401401
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
402-
Some("example.com".to_string())
402+
vec!["example.com".to_string()]
403403
}
404-
_ => None,
404+
_ => vec![],
405405
}
406406
}
407407
}
408408

409-
impl WebBotAuthSignedMessage for MissingParametersTestVector {
410-
fn fetch_all_signature_agents(&self) -> Vec<String> {
411-
vec![]
412-
}
413-
}
414-
415409
let test = MissingParametersTestVector {};
416410
WebBotAuthVerifier::parse(&test).expect_err("This should not have parsed");
417411
}
@@ -421,28 +415,30 @@ mod tests {
421415
struct MissingParametersTestVector {}
422416

423417
impl SignedMessage for MissingParametersTestVector {
424-
fn fetch_all_signature_headers(&self) -> Vec<String> {
425-
vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()]
426-
}
427-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
428-
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()]
429-
}
430-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
431-
match *name {
418+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
419+
match name {
420+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
421+
if name == "signature" {
422+
return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
423+
}
424+
425+
if name == "signature-input" {
426+
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()];
427+
}
428+
429+
if name == "signature-agent" {
430+
return vec![String::from("\"https://myexample.com\"")];
431+
}
432+
vec![]
433+
}
432434
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
433-
Some("example.com".to_string())
435+
vec!["example.com".to_string()]
434436
}
435-
_ => None,
437+
_ => vec![],
436438
}
437439
}
438440
}
439441

440-
impl WebBotAuthSignedMessage for MissingParametersTestVector {
441-
fn fetch_all_signature_agents(&self) -> Vec<String> {
442-
vec![String::from("\"https://myexample.com\"")]
443-
}
444-
}
445-
446442
let test = MissingParametersTestVector {};
447443
WebBotAuthVerifier::parse(&test).expect_err("This should not have parsed");
448444
}
@@ -452,34 +448,30 @@ mod tests {
452448
struct StandardTestVector {}
453449

454450
impl SignedMessage for StandardTestVector {
455-
fn fetch_all_signature_headers(&self) -> Vec<String> {
456-
vec!["sig1=:3q7S1TtbrFhQhpcZ1gZwHPCFHTvdKXNY1xngkp6lyaqqqv3QZupwpu/wQG5a7qybnrj2vZYMeVKuWepm+rNkDw==:".to_owned()]
457-
}
458-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
459-
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()]
460-
}
461-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
451+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
462452
match name {
463-
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
464-
Some("example.com".to_string())
465-
}
466-
CoveredComponent::HTTP(components::HTTPField { name, .. }) => {
453+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
454+
if name == "signature" {
455+
return vec!["sig1=:3q7S1TtbrFhQhpcZ1gZwHPCFHTvdKXNY1xngkp6lyaqqqv3QZupwpu/wQG5a7qybnrj2vZYMeVKuWepm+rNkDw==:".to_owned()];
456+
}
457+
458+
if name == "signature-input" {
459+
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()];
460+
}
461+
467462
if name == "signature-agent" {
468-
return Some(String::from("\"https://myexample.com\""));
463+
return vec![String::from("\"https://myexample.com\"")];
469464
}
470-
None
465+
vec![]
466+
}
467+
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
468+
vec!["example.com".to_string()]
471469
}
472-
_ => None,
470+
_ => vec![],
473471
}
474472
}
475473
}
476474

477-
impl WebBotAuthSignedMessage for StandardTestVector {
478-
fn fetch_all_signature_agents(&self) -> Vec<String> {
479-
vec![String::from("\"https://myexample.com\"")]
480-
}
481-
}
482-
483475
let public_key: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = [
484476
0x26, 0xb4, 0x0b, 0x8f, 0x93, 0xff, 0xf3, 0xd8, 0x97, 0x11, 0x2f, 0x7e, 0xbc, 0x58,
485477
0x2b, 0x23, 0x2d, 0xbd, 0x72, 0x51, 0x7d, 0x08, 0x2f, 0xe8, 0x3c, 0xfb, 0x30, 0xdd,

0 commit comments

Comments
 (0)