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
12 changes: 5 additions & 7 deletions .github/workflows/pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ jobs:
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Set rust toolchain
run: rustup override set 1.87 && rustup component add clippy rustfmt && rustup target add wasm32-unknown-unknown
run: rustup component add clippy rustfmt && rustup target add wasm32-unknown-unknown
- run: cargo fetch
- run: cargo build --all --verbose --exclude plexi-cli --all-features --tests
- run: cargo build --all --verbose --exclude plexi-cli --exclude http-signature-directory --all-features --tests --target wasm32-unknown-unknown
- run: cargo build --all --verbose --all-features --tests
- run: cargo build --all --verbose --exclude http-signature-directory --all-features --tests --target wasm32-unknown-unknown
- run: cargo check --tests --examples --benches --all-features
- run: cargo clippy --all-features --all-targets -- -D warnings
- run: cargo fmt --all -- --check
- run: cargo doc --all --exclude plexi-cli --all-features --document-private-items
- run: cargo test --all --verbose --exclude plexi-cli --all-features
- run: cargo doc --all --all-features --document-private-items
- run: cargo test --all --verbose --all-features

deploy-rust:
name: Deploy Rust Crates
Expand Down Expand Up @@ -85,8 +85,6 @@ jobs:
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Set rust toolchain
run: rustup override set 1.87
- run: cargo publish -p web-bot-auth # will fail if we don't bump the version
continue-on-error: true
- run: cargo publish -p http-signature-directory # will fail if we don't bump the version
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.5.1"
version = "0.6.0"
authors = [
"Akshat Mahajan <akshat@cloudflare.com>",
"Gauri Baraskar <gbaraskar@cloudflare.com>",
Expand Down Expand Up @@ -35,4 +35,4 @@ regex = "1.12.2"
time = { version = "0.3.44" }

# workspace dependencies
web-bot-auth = { version = "0.5.1", path = "./crates/web-bot-auth" }
web-bot-auth = { version = "0.6.0", path = "./crates/web-bot-auth" }
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ This deployment allows to test your implementation.
| [Caddy Plugin](./examples/caddy-plugin/) | Verify RFC 9421 `Signature` for every incoming request |
| [Rust](./examples/rust/) | Verify a sample test request |

### HTTP Signature Directories

| Example | Description |
| :----------------------------------------------------------------- | :------------------------------------------------------------- |
| [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 |

## Development

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:
Expand Down
46 changes: 28 additions & 18 deletions crates/http-signature-directory/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,37 @@ struct RawKeyData {
x: String,
}

struct SignedDirectory {
signature: Vec<String>,
input: Vec<String>,
struct SignedDirectory<'a> {
headers: &'a reqwest::header::HeaderMap,
authority: String,
}

impl SignedMessage for SignedDirectory {
impl SignedMessage for SignedDirectory<'_> {
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
match name {
CoveredComponent::Derived(DerivedComponent::Authority { req: true }) => {
error!(
"Expected `@authority`;req in signature input components, but did not find it"
debug!(
"Resolved {} for derived component {:?}",
self.authority, name
);

vec![self.authority.clone()]
}
CoveredComponent::Derived(DerivedComponent::Authority { req: false }) => {
error!(
"You are signing a plain `@authority` without the `req` component parameter. Fix by signing with `req` so that Signature-Input uses `\"@authority\";req` instead",
);
vec![]
}
CoveredComponent::HTTP(HTTPField { name, .. }) => {
if name == "signature" {
return self.signature.clone();
}
if name == "signature-input" {
return self.input.clone();
if let Some(header) = self.headers.get(name)
&& let Ok(value) = header.to_str()
{
debug!("Found {} for header {}", value, name);
return vec![String::from(value)];
}

debug!("No value for header {:?} found", name);
vec![]
}
_ => vec![],
Expand Down Expand Up @@ -175,9 +184,10 @@ fn main() -> Result<(), String> {
warnings.push("No Content Type header found".to_string());
}

let headers = response.headers().clone();

// Extract signature headers
let signature_headers: Vec<String> = response
.headers()
let signature_headers: Vec<String> = headers
.get_all("Signature")
.iter()
.filter_map(|header| header.to_str().map(String::from).ok())
Expand All @@ -188,8 +198,7 @@ fn main() -> Result<(), String> {
signature_headers
);

let signature_inputs: Vec<String> = response
.headers()
let signature_inputs: Vec<String> = headers
.get_all("Signature-Input")
.iter()
.filter_map(|header| header.to_str().map(String::from).ok())
Expand Down Expand Up @@ -284,8 +293,7 @@ fn main() -> Result<(), String> {
None => {
// Key imported successfully, now verify signature
let directory = SignedDirectory {
signature: signature_headers.clone(),
input: signature_inputs.clone(),
headers: &headers,
authority: String::from(authority),
};

Expand All @@ -305,7 +313,9 @@ fn main() -> Result<(), String> {
.and_then(|tag| tag.as_string())
.is_some_and(|tag| tag.as_str() == thumbprint)
&& innerlist.items.iter().any(|item| {
*item == sfv::Item::new(sfv::StringRef::constant("@authority"))
item.bare_item
.as_string()
.is_some_and(|s| (*s).as_str() == "@authority")
})
}) {
Ok(verifier) => {
Expand Down
22 changes: 10 additions & 12 deletions crates/web-bot-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,13 @@ impl WebBotAuthVerifier {

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 {
if name == "signature-agent" {
for parameter in parameters.0.iter() {
if let HTTPFieldParameters::Key(key) = parameter {
signature_agent_key = Some(key.clone());
break 'outer_loop;
}
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;
}
}
}
Expand All @@ -171,12 +171,10 @@ impl WebBotAuthVerifier {
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)
{
if let Ok((body, _)) = url.decode_to_vec() {
if let Ok(jwks) = serde_json::from_slice::<JSONWebKeySet>(&body) {
return Some(SignatureAgentLink::Inline(jwks));
}
}
return Some(SignatureAgentLink::Inline(jwks));
}
}

Expand Down
17 changes: 17 additions & 0 deletions crates/web-bot-auth/src/message_signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ pub trait SignedMessage {
/// care should be taken to ensure HTTP field names in the message are checked in a
/// case-insensitive way. Only `CoveredComponent::Http` should return a vector with
/// more than one element.
///
/// This function is also used to look up the values of `Signature-Input`, `Signature`
/// and (if used for web bot auth) `Signature-Agent` as standard HTTP headers.
/// Implementations should return those headers as well.
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String>;
}

Expand Down Expand Up @@ -325,6 +329,19 @@ impl MessageSigner {
),
);

sfv_parameters.insert(
sfv::KeyRef::constant("alg").to_owned(),
sfv::BareItem::String(
sfv::StringRef::from_str(&format!("{}", algorithm))
.map_err(|_| {
ImplementationError::ParsingError(
"tag contains non-printable ASCII characters".into(),
)
})?
.to_owned(),
),
);

let created = UtcDateTime::now();
let expiry = created + expires;

Expand Down
3 changes: 0 additions & 3 deletions examples/signature-agent-card-and-registry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ worker = { version = "0.7", features = ['http'] }
worker-macros = { version = "0.7", features = ['http'] }
web-bot-auth = { workspace = true }

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