Skip to content

Commit 8ba0c4a

Browse files
authored
Add support for parsing Signature-Agent as sfv::Dictionary + simplify library (#61)
* 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. * Add support for parsing Signature-Agent as an sfv::Dictionary This change supports handling Signature-Agent as either an sfv::Dictionary or an sfv::Item. To decide which way to process, it relies on whether or not the `Signature-Agent` header listed in the `Signature-Input` field has an associated `key` parameter. If present, it attempts to parse as a dictionary. I've added tests to ensure both types can be handled perfectly well.
1 parent 36b5495 commit 8ba0c4a

File tree

9 files changed

+638
-472
lines changed

9 files changed

+638
-472
lines changed

Cargo.lock

Lines changed: 295 additions & 256 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: 225 additions & 130 deletions
Large diffs are not rendered by default.

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

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
use crate::components::CoveredComponent;
1+
use super::ImplementationError;
2+
use crate::components::{self, CoveredComponent, HTTPField};
23
use crate::keyring::{Algorithm, KeyRing};
34
use indexmap::IndexMap;
5+
use regex::bytes::Regex;
46
use sfv::SerializeValue;
57
use std::fmt::Write as _;
8+
use std::sync::LazyLock;
69
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7-
8-
use super::ImplementationError;
10+
static OBSOLETE_LINE_FOLDING: LazyLock<Regex> =
11+
LazyLock::new(|| Regex::new(r"\s*\r\n\s+").unwrap());
912

1013
/// The component parameters associated with the signature in `Signature-Input`
1114
#[derive(Clone, Debug)]
@@ -153,8 +156,26 @@ impl SignatureBaseBuilder {
153156
self.components
154157
.into_iter()
155158
.map(|component| match message.lookup_component(&component) {
156-
Some(serialized_value) => Ok((component, serialized_value)),
157-
None => Err(ImplementationError::LookupError(component)),
159+
v if v.len() == 1 => Ok((component, v[0].to_owned())),
160+
v if v.len() > 1 && matches!(component, CoveredComponent::HTTP(_)) => {
161+
let mut register: Vec<String> = vec![];
162+
163+
for header_value in v.into_iter() {
164+
register.push(
165+
// replace leading / trailing whitespace and obsolete line folding,
166+
// per HTTP message signature spec
167+
String::from_utf8(
168+
OBSOLETE_LINE_FOLDING
169+
.replace_all(header_value.as_bytes().trim_ascii(), b" ")
170+
.into_owned(),
171+
)
172+
.map_err(|_| ImplementationError::NonAsciiContentFound)?,
173+
);
174+
}
175+
176+
Ok((component, register.join(", ")))
177+
}
178+
_ => Err(ImplementationError::LookupError(component)),
158179
})
159180
.collect::<Result<Vec<(CoveredComponent, String)>, ImplementationError>>()?,
160181
),
@@ -215,31 +236,14 @@ impl SignatureBase {
215236
/// Trait that messages seeking verification should implement to facilitate looking up
216237
/// raw values from the underlying message.
217238
pub trait SignedMessage {
218-
/// Obtain every `Signature` header in the message. Despite the name, you can omit
219-
/// `Signature` that are known to be invalid ahead of time. However, each `Signature-`
220-
/// header should be unparsed and be a valid sfv::Item::Dictionary value. You should
221-
/// separately implement looking this up in `lookup_component` as an HTTP header with
222-
/// multiple values, although including these as signature components when signing is
223-
/// NOT recommended. During verification, invalid values (those that cannot be
224-
/// parsed as an sfv::Dictionary) will be skipped without raising an error.
225-
fn fetch_all_signature_headers(&self) -> Vec<String>;
226-
/// Obtain every `Signature-Input` header in the message. Despite the name, you
227-
/// can omit `Signature-Input` that are known to be invalid ahead of time. However,
228-
/// each `Signature-Input` header should be unparsed and be a valid sfv::Item::Dictionary
229-
/// value (meaning it should be encased in double quotes). You should separately implement
230-
/// looking this up in `lookup_component` as an HTTP header with multiple values, although
231-
/// including these as signature components when signing is NOT recommended. During
232-
/// verification, invalid values (those that cannot be parsed as an sfv::Dictionary) will
233-
/// be skipped will be skipped without raising an error.
234-
fn fetch_all_signature_inputs(&self) -> Vec<String>;
235-
/// Obtain the serialized value of a covered component. Implementations should
239+
/// Retrieve the raw value(s) of a covered component. Implementations should
236240
/// respect any parameter values set on the covered component per the message
237-
/// signature spec. Component values that cannot be found must return None.
241+
/// signature spec. Component values that cannot be found must return an empty vector.
238242
/// `CoveredComponent::HTTP` fields are guaranteed to have lowercase ASCII names, so
239243
/// care should be taken to ensure HTTP field names in the message are checked in a
240-
/// case-insensitive way. HTTP fields with multiple values should be combined into a
241-
/// single string in the manner described in <https://www.rfc-editor.org/rfc/rfc9421#name-http-fields>.
242-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String>;
244+
/// case-insensitive way. Only `CoveredComponent::Http` should return a vector with
245+
/// more than one element.
246+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String>;
243247
}
244248

245249
/// Trait that messages seeking signing should implement to generate `Signature-Input`
@@ -386,6 +390,8 @@ impl MessageSigner {
386390
/// of the chosen labl and its components.
387391
#[derive(Clone, Debug)]
388392
pub struct ParsedLabel {
393+
/// The label that was chosen.
394+
pub label: sfv::Key,
389395
/// The signature obtained from the message that verifiers will verify
390396
pub signature: Vec<u8>,
391397
/// The signature base obtained from the message, containining both the chosen
@@ -424,27 +430,33 @@ impl MessageVerifier {
424430
P: Fn(&(sfv::Key, sfv::InnerList)) -> bool,
425431
{
426432
let signature_input = message
427-
.fetch_all_signature_inputs()
433+
.lookup_component(&CoveredComponent::HTTP(HTTPField {
434+
name: "signature-input".to_string(),
435+
parameters: components::HTTPFieldParametersSet(vec![]),
436+
}))
428437
.into_iter()
429438
.filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok())
430439
.reduce(|mut acc, sig_input| {
431440
acc.extend(sig_input);
432441
acc
433442
})
434443
.ok_or(ImplementationError::ParsingError(
435-
"No `Signature-Input` headers found".to_string(),
444+
"No validly-formatted `Signature-Input` headers found".to_string(),
436445
))?;
437446

438447
let mut signature_header = message
439-
.fetch_all_signature_headers()
448+
.lookup_component(&CoveredComponent::HTTP(HTTPField {
449+
name: "signature".to_string(),
450+
parameters: components::HTTPFieldParametersSet(vec![]),
451+
}))
440452
.into_iter()
441453
.filter_map(|sig_input| sfv::Parser::new(&sig_input).parse_dictionary().ok())
442454
.reduce(|mut acc, sig_input| {
443455
acc.extend(sig_input);
444456
acc
445457
})
446458
.ok_or(ImplementationError::ParsingError(
447-
"No `Signature` headers found".to_string(),
459+
"No validly-formatted `Signature` headers found".to_string(),
448460
))?;
449461

450462
let (label, innerlist) = signature_input
@@ -483,7 +495,11 @@ impl MessageVerifier {
483495
let base = builder.into_signature_base(message)?;
484496

485497
Ok(MessageVerifier {
486-
parsed: ParsedLabel { signature, base },
498+
parsed: ParsedLabel {
499+
label,
500+
signature,
501+
base,
502+
},
487503
})
488504
}
489505

@@ -550,18 +566,22 @@ mod tests {
550566
struct StandardTestVector {}
551567

552568
impl SignedMessage for StandardTestVector {
553-
fn fetch_all_signature_headers(&self) -> Vec<String> {
554-
vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()]
555-
}
556-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
557-
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()]
558-
}
559-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
560-
match *name {
569+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
570+
match name {
571+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
572+
if name == "signature" {
573+
return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
574+
}
575+
576+
if name == "signature-input" {
577+
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()];
578+
}
579+
vec![]
580+
}
561581
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
562-
Some("example.com".to_string())
582+
vec!["example.com".to_string()]
563583
}
564-
_ => None,
584+
_ => vec![],
565585
}
566586
}
567587
}

examples/rust/signing.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
use indexmap::IndexMap;
1616
use std::{time::Duration, vec};
1717
use web_bot_auth::{
18-
components::{CoveredComponent, DerivedComponent, HTTPField, HTTPFieldParametersSet},
18+
components::{
19+
CoveredComponent, DerivedComponent, HTTPField, HTTPFieldParameters, HTTPFieldParametersSet,
20+
},
1921
keyring::Algorithm,
2022
message_signatures::{MessageSigner, UnsignedMessage},
2123
};
@@ -36,9 +38,11 @@ impl UnsignedMessage for MyThing {
3638
(
3739
CoveredComponent::HTTP(HTTPField {
3840
name: "signature-agent".to_string(),
39-
parameters: HTTPFieldParametersSet(vec![]),
41+
parameters: HTTPFieldParametersSet(vec![HTTPFieldParameters::Key(
42+
"agent1".to_string(),
43+
)]),
4044
}),
41-
"\"https://myexample.com\"".to_string(),
45+
r#"agent1="https://myexample.com""#.to_string(),
4246
),
4347
])
4448
}
@@ -50,12 +54,14 @@ impl UnsignedMessage for MyThing {
5054
}
5155

5256
fn main() {
53-
// Signing a message
57+
// Signing a message - private key pulled from https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/
58+
// and only for example purposes.
5459
let private_key = vec![
5560
0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95, 0x4e, 0x6e, 0x74, 0x0c, 0x5b, 0x4c, 0x0e,
5661
0x84, 0x22, 0x91, 0x39, 0xa2, 0x0a, 0xa8, 0xab, 0x56, 0xff, 0x66, 0x58, 0x6f, 0x6a, 0x7d,
5762
0x29, 0xc5,
5863
];
64+
// sample keyid pulled from https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/
5965
let signer = MessageSigner {
6066
keyid: "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".into(),
6167
nonce: "ZO3/XMEZjrvSnLtAP9M7jK0WGQf3J+pbmQRUpKDhF9/jsNCWqUh2sq+TH4WTX3/GpNoSZUa8eNWMKqxWp2/c2g==".into(),

examples/rust/verify.rs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
use web_bot_auth::{
16-
SignatureAgentLink, WebBotAuthSignedMessage, WebBotAuthVerifier,
16+
SignatureAgentLink, WebBotAuthVerifier,
1717
components::{CoveredComponent, DerivedComponent, HTTPField},
1818
keyring::{Algorithm, KeyRing},
1919
message_signatures::SignedMessage,
@@ -22,34 +22,30 @@ use web_bot_auth::{
2222
struct MySignedMsg;
2323

2424
impl SignedMessage for MySignedMsg {
25-
fn fetch_all_signature_headers(&self) -> Vec<String> {
26-
vec!["sig1=:GXzHSRZ9Sf6WwLOZjxAhfE6WEUPfDMrVBJITsL2sbG8gtcZgqKe2Yn7uavk0iNQrfcPzgGq8h8Pk5osNGqdtCw==:".to_owned()]
27-
}
28-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
29-
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=1749332605;expires=1749332615"#.to_owned()]
30-
}
31-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
25+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
3226
match name {
3327
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
34-
Some("example.com".to_string())
28+
vec!["example.com".to_string()]
3529
}
3630
CoveredComponent::HTTP(HTTPField { name, .. }) => {
3731
if name == "signature-agent" {
38-
return Some(String::from("\"https://myexample.com\""));
32+
return vec![r#"agent1="https://myexample.com""#.to_string()];
3933
}
40-
None
34+
35+
if name == "signature" {
36+
return vec![r#"sig1=:EZZ8VJcVQ9WgiUytQWAfEvRWLLu2O+UkJ15aVI//dfLTCLnr1Vg2CDXXlrW4D+OjBB6zu/UkFtxpKzbXh2ESBg==:"#.to_string()];
37+
}
38+
39+
if name == "signature-input" {
40+
return vec![r#"sig1=("@authority" "signature-agent";key="agent1");keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";nonce="ZO3/XMEZjrvSnLtAP9M7jK0WGQf3J+pbmQRUpKDhF9/jsNCWqUh2sq+TH4WTX3/GpNoSZUa8eNWMKqxWp2/c2g==";tag="web-bot-auth";created=1761143856;expires=1761143866"#.to_string()];
41+
}
42+
vec![]
4143
}
42-
_ => None,
44+
_ => vec![],
4345
}
4446
}
4547
}
4648

47-
impl WebBotAuthSignedMessage for MySignedMsg {
48-
fn fetch_all_signature_agents(&self) -> Vec<String> {
49-
vec!["\"https://myexample.com\"".into()]
50-
}
51-
}
52-
5349
fn main() {
5450
// Verifying a Web Bot Auth message
5551
let public_key = [
@@ -58,6 +54,7 @@ fn main() {
5854
0xd1, 0xbb,
5955
];
6056
let mut keyring = KeyRing::default();
57+
// sample keyid pulled from https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/
6158
keyring.import_raw(
6259
"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U".to_string(),
6360
Algorithm::Ed25519,

examples/rust/verify_arbitrary.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,30 @@
1313
// limitations under the License.
1414

1515
use web_bot_auth::{
16-
components::{CoveredComponent, DerivedComponent},
16+
components::{CoveredComponent, DerivedComponent, HTTPField},
1717
keyring::{Algorithm, KeyRing},
1818
message_signatures::{MessageVerifier, SignedMessage},
1919
};
2020

2121
struct MySignedMsg;
2222

2323
impl SignedMessage for MySignedMsg {
24-
fn fetch_all_signature_headers(&self) -> Vec<String> {
25-
vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()]
26-
}
27-
fn fetch_all_signature_inputs(&self) -> Vec<String> {
28-
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()]
29-
}
30-
fn lookup_component(&self, name: &CoveredComponent) -> Option<String> {
31-
match *name {
24+
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
25+
match name {
3226
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
33-
Some("example.com".to_string())
27+
vec!["example.com".to_string()]
28+
}
29+
CoveredComponent::HTTP(HTTPField { name, .. }) => {
30+
if name == "signature" {
31+
return vec!["sig1=:uz2SAv+VIemw+Oo890bhYh6Xf5qZdLUgv6/PbiQfCFXcX/vt1A8Pf7OcgL2yUDUYXFtffNpkEr5W6dldqFrkDg==:".to_owned()];
32+
}
33+
34+
if name == "signature-input" {
35+
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()];
36+
}
37+
vec![]
3438
}
35-
_ => None,
39+
_ => vec![],
3640
}
3741
}
3842
}

0 commit comments

Comments
 (0)