Skip to content

Commit 6c16893

Browse files
authored
Demonstrate hosting signature agent card and registry on Cloudflare Workers (cloudflare#65)
* Support Workers with `time` crate instead of `std::time` Cloudflare Workers does not expose a system clock. This isn't caught by our Github compilation tests because `std::time` compiles in `wasm32-unknown-unknown` just fine, but throws a runtime panic in that environment. The standard advice is to use `time` crate instead per public Cloudflare documentation with `wasm-bindgen` feature enabled. The main difference is that calls to `SystemTime::now()` and `Instant::elapsed()`, both of which require a running clock, have now been replaced with `UtcTimeOffset::now()` and raw `time::Duration` calculations. As a nice side effect, we can also remove an entire category of errors thrown by `std::time` from those thrown by `web-bot-auth`. * Create an example of how to host a signature agent card and a registry on Cloudflare workers This change adds a single Worker that: 1. Generates a brand new SAC and persists it in Workers KV, accessible on visiting a message directory. 2. Generates a registry linking to that SAC. It is set up in such a way that attaching multiple custom routes will automatically expand the number of registry items - for example, every new subdomain will trigger a new SAC. This makes it useful for e.g. end-to-end tests. * Fix registry and http-signature-directory to work with spec The registry should point to `/well-known/http-message-signature` path, not just the domain of the host. This change amends that. `http-signature-directory` has been assuming responses from the directory will be signed with just the `@authority` component - however, this is wrong, both per the spec and logically. The correct component should be `"@authority";req`, indicating that the response is signing the derived component from the request. This change adds a log line to the same. It also adds a better error message for the case when signature verification fails. This is based on real challenges experienced by the community e.g. in https://community.cloudflare.com/t/web-bot-auth-signature-validation-failure/854577 during parsing. * fixup! Fix registry and http-signature-directory to work with spec
1 parent c607cdd commit 6c16893

14 files changed

Lines changed: 1893 additions & 80 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[workspace]
22
members = [
33
"crates/*",
4-
"examples/rust"
4+
"examples/rust",
5+
"examples/signature-agent-card-and-registry"
56
]
67
resolver = "2"
78

@@ -31,6 +32,7 @@ base64 = "0.22.1"
3132
serde_json = "1.0.140"
3233
data-url = "0.3.1"
3334
regex = "1.12.2"
35+
time = { version = "0.3.44" }
3436

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

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// https://opensource.org/licenses/Apache-2.0
44

55
use clap::Parser;
6-
use log::{debug, info};
6+
use log::{debug, error, info};
77
use reqwest::{
88
Url,
99
blocking::Client,
@@ -70,7 +70,10 @@ struct SignedDirectory {
7070
impl SignedMessage for SignedDirectory {
7171
fn lookup_component(&self, name: &CoveredComponent) -> Vec<String> {
7272
match name {
73-
CoveredComponent::Derived(DerivedComponent::Authority { .. }) => {
73+
CoveredComponent::Derived(DerivedComponent::Authority { req: true }) => {
74+
error!(
75+
"Expected `@authority`;req in signature input components, but did not find it"
76+
);
7477
vec![self.authority.clone()]
7578
}
7679
CoveredComponent::HTTP(HTTPField { name, .. }) => {
@@ -129,7 +132,7 @@ fn main() -> Result<(), String> {
129132

130133
let authority = url.authority();
131134
debug!(
132-
"Extracted the following @authority component: {:?}",
135+
"Assumed the following @authority component: {:?}",
133136
authority
134137
);
135138

@@ -328,7 +331,7 @@ fn main() -> Result<(), String> {
328331
}
329332
Err(err) => {
330333
key_info.error = Some(format!(
331-
"Signature verification failed: {:?}",
334+
"Generated signature was incorrect, leading to verification failure, because: {:?}",
332335
err
333336
));
334337
}

crates/web-bot-auth/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ serde = { workspace = true }
2121
serde_json = { workspace = true }
2222
sha2 = { workspace = true }
2323
base64 = { workspace = true }
24-
data-url = { workspace = true }
24+
data-url = { workspace = true }
25+
time = { workspace = true }
26+
27+
[target.'cfg(target_arch = "wasm32")'.dependencies]
28+
time = { workspace = true, features = ["wasm-bindgen"] }

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ use message_signatures::{MessageVerifier, ParsedLabel, SignatureTiming, SignedMe
3030

3131
use data_url::DataUrl;
3232
use keyring::{Algorithm, JSONWebKeySet, KeyRing};
33-
use std::time::SystemTimeError;
3433

3534
use crate::components::{HTTPField, HTTPFieldParameters};
3635

@@ -78,10 +77,6 @@ pub enum ImplementationError {
7877
/// This is considered "impossible" as invalid values should not be present in the structure
7978
/// containing those values.
8079
SignatureParamsSerialization,
81-
/// Verification of `created` or `expires` component parameter requires use of a system clock.
82-
/// This error is thrown if the system clock is configured in ways that prevent adequate time
83-
/// resolution, such as the clock believes the start of Unix time is in the future.
84-
TimeError(SystemTimeError),
8580
/// A wrapper around `WebBotAuthError`
8681
WebBotAuth(WebBotAuthError),
8782
}
@@ -319,8 +314,8 @@ mod tests {
319314
assert!(advisory.is_expired.unwrap_or(true));
320315
assert!(!advisory.nonce_is_invalid.unwrap_or(true));
321316
let timing = verifier.verify(&keyring, None).unwrap();
322-
assert!(timing.generation.as_nanos() > 0);
323-
assert!(timing.verification.as_nanos() > 0);
317+
assert!(timing.generation.whole_nanoseconds() > 0);
318+
assert!(timing.verification.whole_nanoseconds() > 0);
324319
}
325320

326321
#[test]
@@ -402,7 +397,7 @@ mod tests {
402397
signer
403398
.generate_signature_headers_content(
404399
&mut mytest,
405-
std::time::Duration::from_secs(10),
400+
time::Duration::seconds(10),
406401
Algorithm::Ed25519,
407402
&private_key.to_vec(),
408403
)
@@ -419,8 +414,8 @@ mod tests {
419414
assert!(!advisory.nonce_is_invalid.unwrap_or(true));
420415

421416
let timing = verifier.verify(&keyring, None).unwrap();
422-
assert!(timing.generation.as_nanos() > 0);
423-
assert!(timing.verification.as_nanos() > 0);
417+
assert!(timing.generation.whole_nanoseconds() > 0);
418+
assert!(timing.verification.whole_nanoseconds() > 0);
424419
}
425420

426421
#[test]

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

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use regex::bytes::Regex;
66
use sfv::SerializeValue;
77
use std::fmt::Write as _;
88
use std::sync::LazyLock;
9-
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9+
use time::{Duration, UtcDateTime};
1010
static OBSOLETE_LINE_FOLDING: LazyLock<Regex> =
1111
LazyLock::new(|| Regex::new(r"\s*\r\n\s+").unwrap());
1212

@@ -108,16 +108,12 @@ impl ParameterDetails {
108108
{
109109
SecurityAdvisory {
110110
is_expired: self.expires.map(|expires| {
111-
if expires <= 0 {
112-
return true;
111+
if let Ok(expiry) = UtcDateTime::from_unix_timestamp(expires) {
112+
let now = UtcDateTime::now();
113+
return now >= expiry;
113114
}
114115

115-
match SystemTime::now().duration_since(UNIX_EPOCH) {
116-
Ok(duration) => i64::try_from(duration.as_secs())
117-
.map(|dur| dur >= expires)
118-
.unwrap_or(true),
119-
Err(_) => true,
120-
}
116+
true
121117
}),
122118
nonce_is_invalid: self.nonce.as_ref().map(nonce_validator),
123119
}
@@ -329,31 +325,17 @@ impl MessageSigner {
329325
),
330326
);
331327

332-
let created = SystemTime::now()
333-
.duration_since(UNIX_EPOCH)
334-
.map_err(ImplementationError::TimeError)?;
328+
let created = UtcDateTime::now();
335329
let expiry = created + expires;
336330

337-
let created_as_i64 = i64::try_from(created.as_secs()).map_err(|_| {
338-
ImplementationError::ParsingError(
339-
"Clock time does not fit in i64, verfy your clock is set correctly".into(),
340-
)
341-
})?;
342-
let expires_as_i64 = i64::try_from(expiry.as_secs()).map_err(|_| {
343-
ImplementationError::ParsingError(
344-
"Clcok time + `expires` value does not fit in i64, verfy your duration is valid"
345-
.into(),
346-
)
347-
})?;
348-
349331
sfv_parameters.insert(
350332
sfv::KeyRef::constant("created").to_owned(),
351-
sfv::BareItem::Integer(sfv::Integer::constant(created_as_i64)),
333+
sfv::BareItem::Integer(sfv::Integer::constant(created.unix_timestamp())),
352334
);
353335

354336
sfv_parameters.insert(
355337
sfv::KeyRef::constant("expires").to_owned(),
356-
sfv::BareItem::Integer(sfv::Integer::constant(expires_as_i64)),
338+
sfv::BareItem::Integer(sfv::Integer::constant(expiry.unix_timestamp())),
357339
);
358340

359341
let (signature_base, signature_params_content) = SignatureBase {
@@ -529,9 +511,9 @@ impl MessageVerifier {
529511
.and_then(|key| keyring.get(key)),
530512
})
531513
.ok_or(ImplementationError::NoSuchKey)?;
532-
let generation = Instant::now();
514+
let generation = UtcDateTime::now();
533515
let (base_representation, _) = self.parsed.base.into_ascii()?;
534-
let generation = generation.elapsed();
516+
let generation = UtcDateTime::now() - generation;
535517
match &keying_material.0 {
536518
Algorithm::Ed25519 => {
537519
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
@@ -541,13 +523,13 @@ impl MessageVerifier {
541523
let sig = Signature::try_from(self.parsed.signature.as_slice())
542524
.map_err(|_| ImplementationError::InvalidSignatureLength)?;
543525

544-
let verification = Instant::now();
526+
let verification = UtcDateTime::now();
545527
verifying_key
546528
.verify(base_representation.as_bytes(), &sig)
547529
.map_err(ImplementationError::FailedToVerify)
548530
.map(|()| SignatureTiming {
549531
generation,
550-
verification: verification.elapsed(),
532+
verification: UtcDateTime::now() - verification,
551533
})
552534
}
553535
other => Err(ImplementationError::UnsupportedAlgorithm(other.clone())),
@@ -615,8 +597,8 @@ mod tests {
615597
);
616598
let verifier = MessageVerifier::parse(&test, |(_, _)| true).unwrap();
617599
let timing = verifier.verify(&keyring, None).unwrap();
618-
assert!(timing.generation.as_nanos() > 0);
619-
assert!(timing.verification.as_nanos() > 0);
600+
assert!(timing.generation.whole_nanoseconds() > 0);
601+
assert!(timing.verification.whole_nanoseconds() > 0);
620602
}
621603

622604
#[test]
@@ -669,7 +651,7 @@ mod tests {
669651
signer
670652
.generate_signature_headers_content(
671653
&mut test,
672-
Duration::from_secs(10),
654+
Duration::seconds(10),
673655
Algorithm::Ed25519,
674656
&private_key.to_vec()
675657
)

examples/rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ path = "verify.rs"
2525

2626
[dependencies]
2727
indexmap = { workspace = true }
28+
time = { workspace = true }
2829

2930
# workspace dependencies
3031
web-bot-auth = { workspace = true }

examples/rust/signing.rs

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

1515
use indexmap::IndexMap;
16-
use std::{time::Duration, vec};
16+
use time::Duration;
1717
use web_bot_auth::{
1818
components::{
1919
CoveredComponent, DerivedComponent, HTTPField, HTTPFieldParameters, HTTPFieldParametersSet,
@@ -71,7 +71,7 @@ fn main() {
7171
signer
7272
.generate_signature_headers_content(
7373
&mut headers,
74-
Duration::from_secs(10),
74+
Duration::seconds(10),
7575
Algorithm::Ed25519,
7676
&private_key,
7777
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build/
2+
.wrangler/

0 commit comments

Comments
 (0)