Skip to content

Commit e3d7684

Browse files
authored
Release v0.6.0 for web-bot-auth crates (cloudflare#68)
* Make `@authority;req` errors more prominent + fix example signature generation This change amends the `http-signature-dir` to print an error log whendirectories mistakenly sign `@authority` without the `req` parameter. It fixes a bug with the example signature agent card generation where only the host component was used to sign `@authority`, rather than the full host and port pair (i.e. the _actual_ authority component). This led to verifiers being unable to verify generated signatures. It fixes some minor comments and superfluous Github Actions changes, and does some basic refactoring to make the logic a bit more straightforward in the example. Importantly, it also adds the `alg` parameter in generated signatures - this is in line with the opinionated signing we do, whereby other elements normal to web bot auth are also enforced for arbitrary HTTP signatures. * Release v0.6.0 of web-bot-auth crates These include some pretty significant and breaking changes: 1. Dependency on `time` library is now required instead of `std::time` for all API users. As a bonus, however, we gain support on Cloudflare Workers as well as removal of a class of errors related to system clocks and `created` / `expires` parsing. 2. A number of constructs were removed: `WebBotAuthSignedMessage`, `SignedMessage::fetch_all_signature_headers` and `SignedMessage::fetch_all_signature_inputs`. The library now exposes a single method to look up components to verify. 3. `Signature-Agent` can now be parsed as a dictionary, but retains support for being parsed as a raw string. 4. It enforces use of `req` parameter in `http-message-dir`. This is in line with the specification, but can break verification of existing sites. I also removed the pin to Rust v1.87 in the Github Actions handler. This ensures we're building against the latest available Rust version.
1 parent 6c16893 commit e3d7684

9 files changed

Lines changed: 105 additions & 77 deletions

File tree

.github/workflows/pullrequest.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ jobs:
4747
target
4848
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
4949
- name: Set rust toolchain
50-
run: rustup override set 1.87 && rustup component add clippy rustfmt && rustup target add wasm32-unknown-unknown
50+
run: rustup component add clippy rustfmt && rustup target add wasm32-unknown-unknown
5151
- run: cargo fetch
52-
- run: cargo build --all --verbose --exclude plexi-cli --all-features --tests
53-
- run: cargo build --all --verbose --exclude plexi-cli --exclude http-signature-directory --all-features --tests --target wasm32-unknown-unknown
52+
- run: cargo build --all --verbose --all-features --tests
53+
- run: cargo build --all --verbose --exclude http-signature-directory --all-features --tests --target wasm32-unknown-unknown
5454
- run: cargo check --tests --examples --benches --all-features
5555
- run: cargo clippy --all-features --all-targets -- -D warnings
5656
- run: cargo fmt --all -- --check
57-
- run: cargo doc --all --exclude plexi-cli --all-features --document-private-items
58-
- run: cargo test --all --verbose --exclude plexi-cli --all-features
57+
- run: cargo doc --all --all-features --document-private-items
58+
- run: cargo test --all --verbose --all-features
5959

6060
deploy-rust:
6161
name: Deploy Rust Crates
@@ -85,8 +85,6 @@ jobs:
8585
~/.cargo/git
8686
target
8787
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
88-
- name: Set rust toolchain
89-
run: rustup override set 1.87
9088
- run: cargo publish -p web-bot-auth # will fail if we don't bump the version
9189
continue-on-error: true
9290
- run: cargo publish -p http-signature-directory # will fail if we don't bump the version

Cargo.lock

Lines changed: 4 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
resolver = "2"
88

99
[workspace.package]
10-
version = "0.5.1"
10+
version = "0.6.0"
1111
authors = [
1212
"Akshat Mahajan <akshat@cloudflare.com>",
1313
"Gauri Baraskar <gbaraskar@cloudflare.com>",
@@ -35,4 +35,4 @@ regex = "1.12.2"
3535
time = { version = "0.3.44" }
3636

3737
# workspace dependencies
38-
web-bot-auth = { version = "0.5.1", path = "./crates/web-bot-auth" }
38+
web-bot-auth = { version = "0.6.0", path = "./crates/web-bot-auth" }

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ This deployment allows to test your implementation.
3939
| [Caddy Plugin](./examples/caddy-plugin/) | Verify RFC 9421 `Signature` for every incoming request |
4040
| [Rust](./examples/rust/) | Verify a sample test request |
4141

42+
### HTTP Signature Directories
43+
44+
| Example | Description |
45+
| :----------------------------------------------------------------- | :------------------------------------------------------------- |
46+
| [Cloudflare Workers](./examples/signature-agent-card-and-registry) | Host a signature directory on Cloudflare Workers, using the [signature agent card and registry](https://datatracker.ietf.org/doc/draft-meunier-webbotauth-registry/) format |
47+
4248
## Development
4349

4450
This repository uses [npm](https://docs.npmjs.com/cli/v11/using-npm/workspaces) and [cargo](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) workspaces. There are several packages which it provides:

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

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,37 @@ struct RawKeyData {
6161
x: String,
6262
}
6363

64-
struct SignedDirectory {
65-
signature: Vec<String>,
66-
input: Vec<String>,
64+
struct SignedDirectory<'a> {
65+
headers: &'a reqwest::header::HeaderMap,
6766
authority: String,
6867
}
6968

70-
impl SignedMessage for SignedDirectory {
69+
impl SignedMessage for SignedDirectory<'_> {
7170
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
7271
match name {
7372
CoveredComponent::Derived(DerivedComponent::Authority { req: true }) => {
74-
error!(
75-
"Expected `@authority`;req in signature input components, but did not find it"
73+
debug!(
74+
"Resolved {} for derived component {:?}",
75+
self.authority, name
7676
);
77+
7778
vec![self.authority.clone()]
7879
}
80+
CoveredComponent::Derived(DerivedComponent::Authority { req: false }) => {
81+
error!(
82+
"You are signing a plain `@authority` without the `req` component parameter. Fix by signing with `req` so that Signature-Input uses `\"@authority\";req` instead",
83+
);
84+
vec![]
85+
}
7986
CoveredComponent::HTTP(HTTPField { name, .. }) => {
80-
if name == "signature" {
81-
return self.signature.clone();
82-
}
83-
if name == "signature-input" {
84-
return self.input.clone();
87+
if let Some(header) = self.headers.get(name)
88+
&& let Ok(value) = header.to_str()
89+
{
90+
debug!("Found {} for header {}", value, name);
91+
return vec![String::from(value)];
8592
}
93+
94+
debug!("No value for header {:?} found", name);
8695
vec![]
8796
}
8897
_ => vec![],
@@ -175,9 +184,10 @@ fn main() -> Result<(), String> {
175184
warnings.push("No Content Type header found".to_string());
176185
}
177186

187+
let headers = response.headers().clone();
188+
178189
// Extract signature headers
179-
let signature_headers: Vec<String> = response
180-
.headers()
190+
let signature_headers: Vec<String> = headers
181191
.get_all("Signature")
182192
.iter()
183193
.filter_map(|header| header.to_str().map(String::from).ok())
@@ -188,8 +198,7 @@ fn main() -> Result<(), String> {
188198
signature_headers
189199
);
190200

191-
let signature_inputs: Vec<String> = response
192-
.headers()
201+
let signature_inputs: Vec<String> = headers
193202
.get_all("Signature-Input")
194203
.iter()
195204
.filter_map(|header| header.to_str().map(String::from).ok())
@@ -284,8 +293,7 @@ fn main() -> Result<(), String> {
284293
None => {
285294
// Key imported successfully, now verify signature
286295
let directory = SignedDirectory {
287-
signature: signature_headers.clone(),
288-
input: signature_inputs.clone(),
296+
headers: &headers,
289297
authority: String::from(authority),
290298
};
291299

@@ -305,7 +313,9 @@ fn main() -> Result<(), String> {
305313
.and_then(|tag| tag.as_string())
306314
.is_some_and(|tag| tag.as_str() == thumbprint)
307315
&& innerlist.items.iter().any(|item| {
308-
*item == sfv::Item::new(sfv::StringRef::constant("@authority"))
316+
item.bare_item
317+
.as_string()
318+
.is_some_and(|s| (*s).as_str() == "@authority")
309319
})
310320
}) {
311321
Ok(verifier) => {

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,13 @@ impl WebBotAuthVerifier {
149149

150150
let mut signature_agent_key: Option<String> = None;
151151
'outer_loop: for (component, _) in message_verifier.parsed.base.components.iter() {
152-
if let CoveredComponent::HTTP(HTTPField { name, parameters }) = component {
153-
if name == "signature-agent" {
154-
for parameter in parameters.0.iter() {
155-
if let HTTPFieldParameters::Key(key) = parameter {
156-
signature_agent_key = Some(key.clone());
157-
break 'outer_loop;
158-
}
152+
if let CoveredComponent::HTTP(HTTPField { name, parameters }) = component
153+
&& name == "signature-agent"
154+
{
155+
for parameter in parameters.0.iter() {
156+
if let HTTPFieldParameters::Key(key) = parameter {
157+
signature_agent_key = Some(key.clone());
158+
break 'outer_loop;
159159
}
160160
}
161161
}
@@ -171,12 +171,10 @@ impl WebBotAuthVerifier {
171171
let mediatype = url.mime_type();
172172
if mediatype.type_ == "application"
173173
&& mediatype.subtype == "http-message-signatures-directory"
174+
&& let Ok((body, _)) = url.decode_to_vec()
175+
&& let Ok(jwks) = serde_json::from_slice::<JSONWebKeySet>(&body)
174176
{
175-
if let Ok((body, _)) = url.decode_to_vec() {
176-
if let Ok(jwks) = serde_json::from_slice::<JSONWebKeySet>(&body) {
177-
return Some(SignatureAgentLink::Inline(jwks));
178-
}
179-
}
177+
return Some(SignatureAgentLink::Inline(jwks));
180178
}
181179
}
182180

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ pub trait SignedMessage {
239239
/// care should be taken to ensure HTTP field names in the message are checked in a
240240
/// case-insensitive way. Only `CoveredComponent::Http` should return a vector with
241241
/// more than one element.
242+
///
243+
/// This function is also used to look up the values of `Signature-Input`, `Signature`
244+
/// and (if used for web bot auth) `Signature-Agent` as standard HTTP headers.
245+
/// Implementations should return those headers as well.
242246
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String>;
243247
}
244248

@@ -325,6 +329,19 @@ impl MessageSigner {
325329
),
326330
);
327331

332+
sfv_parameters.insert(
333+
sfv::KeyRef::constant("alg").to_owned(),
334+
sfv::BareItem::String(
335+
sfv::StringRef::from_str(&format!("{}", algorithm))
336+
.map_err(|_| {
337+
ImplementationError::ParsingError(
338+
"tag contains non-printable ASCII characters".into(),
339+
)
340+
})?
341+
.to_owned(),
342+
),
343+
);
344+
328345
let created = UtcDateTime::now();
329346
let expiry = created + expires;
330347

examples/signature-agent-card-and-registry/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ worker = { version = "0.7", features = ['http'] }
1919
worker-macros = { version = "0.7", features = ['http'] }
2020
web-bot-auth = { workspace = true }
2121

22-
# The following libraries are deliberately held back to these versions.
23-
# `ed25519-dalek` doesn't support latest `rand` in its latest stable version,
24-
# so we pin to the version of `rand` and `rand_chaca` the latest versions do support.
2522
ed25519-dalek = { version = "2.2.0", features = ['rand_core'] }
2623
rand = { version = "0.8", default-features = false, features = ["getrandom"] }
2724
getrandom = { version = "0.2", features = ["js"] }

0 commit comments

Comments
 (0)