use base64::{Engine as _, engine::general_purpose};
use ed25519_dalek::SigningKey;
use indexmap::map::IndexMap;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::time::Duration;
use web_bot_auth::{
components::{CoveredComponent, DerivedComponent, HTTPField, HTTPFieldParametersSet},
keyring::{Algorithm, JSONWebKeySet, Thumbprintable},
message_signatures::{MessageSigner, UnsignedMessage},
};
use worker::*;
const README: &str = r#"
Example Signature Agent Card and Registry on Cloudflare Workers
This deploys a registry and a signature agent card
on the same authority: a Cloudflare worker.
Instructions
- Navigate to
/signature-agent-card to view a generated Signature Agent card.
- Navigate to
/.well-known/http-message-signatures-directory to view generated key material.
- Navigate to
/registry.txt to view a generated registry linking to that Signature Agent card.
Customize
You can add a worker binding and multiple custom routes, and visit /.well-known/http-message-signatures-directory on each custom route.
This will automatically populate your registry with multiple, unique entries.
"#;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct SignatureAgentCard {
client_id: String,
client_name: String,
contacts: Vec,
jwks_uri: String,
web_bot_auth: WebBotAuthMetadata,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct WebBotAuthMetadata {
trigger: String,
purpose: String,
}
struct SignatureHeaderGenerator<'a> {
authority: String,
digest_header: String,
response_headers: &'a mut worker::Headers,
}
impl UnsignedMessage for SignatureHeaderGenerator<'_> {
fn fetch_components_to_cover(
&self,
) -> IndexMap {
IndexMap::from_iter([
(
CoveredComponent::Derived(DerivedComponent::Authority { req: true }),
self.authority.clone(),
),
(
CoveredComponent::HTTP(HTTPField {
name: "content-digest".to_string(),
parameters: HTTPFieldParametersSet(vec![]),
}),
self.digest_header.clone(),
),
])
}
fn register_header_contents(&mut self, signature_input: String, signature_header: String) {
let _ = self
.response_headers
.set("signature-input", &(format!("sig1={}", signature_input)));
let _ = self
.response_headers
.set("signature", &(format!("sig1={}", signature_header)));
}
}
#[event(fetch)]
async fn fetch(req: HttpRequest, env: Env, _ctx: Context) -> Result {
let kv = env.kv("signed-agent-registry-hostnames")?;
let authority = format!(
"{}",
req.uri()
.authority()
.ok_or(worker::Error::RouteNoDataError)?
);
match req.uri().path() {
"/registry.txt" => {
let scheme = req
.uri()
.scheme_str()
.ok_or(worker::Error::RouteNoDataError)?
.to_string();
let list = kv.list().limit(1000).execute().await?;
Response::ok(
list.keys
.into_iter()
.map(|key| {
format!(
"{}://{}/signature-agent-card",
scheme.clone(),
key.name.clone()
)
})
.collect::>()
.join("\n"),
)
}
"/signature-agent-card" => {
let scheme = req
.uri()
.scheme_str()
.ok_or(worker::Error::RouteNoDataError)?
.to_string();
let origin = format!("{}://{}", scheme, authority);
let card = SignatureAgentCard {
client_id: format!("{origin}/signature-agent-card"),
client_name: authority.to_string(),
contacts: vec!["mailto:test@example.com".to_string()],
jwks_uri: format!("{origin}/.well-known/http-message-signatures-directory"),
web_bot_auth: WebBotAuthMetadata {
trigger: "fetcher".to_string(),
purpose: "example".to_string(),
},
};
let body = serde_json::to_string(&card)
.map_err(|e| worker::Error::RustError(e.to_string()))?;
let mut response = Response::from_body(ResponseBody::Body(body.into_bytes()))?;
response
.headers_mut()
.set("content-type", "application/json")?;
Ok(response)
}
"/.well-known/http-message-signatures-directory" => {
let vectorized_keypair: Vec = match kv.get(&authority).bytes().await? {
Some(pair) => pair,
None => {
let mut secret_key = [0_u8; ed25519_dalek::SECRET_KEY_LENGTH];
getrandom::getrandom(&mut secret_key)
.map_err(|e| worker::Error::RustError(e.to_string()))?;
let signing_key = SigningKey::from_bytes(&secret_key);
let keypair = signing_key.to_keypair_bytes().to_vec();
kv.put_bytes(&authority, &keypair)?.execute().await?;
keypair
}
};
let keypair_bytes: [u8; 64_usize] = vectorized_keypair
.try_into()
.expect("Vec length must match array length");
let signing_key = SigningKey::from_keypair_bytes(&keypair_bytes)
.map_err(|e| worker::Error::RustError(e.to_string()))?;
let verifying_key = signing_key.verifying_key();
let thumbprintable = Thumbprintable::OKP {
crv: "Ed25519".to_string(),
x: general_purpose::URL_SAFE_NO_PAD.encode(verifying_key.to_bytes()),
};
let thumbprint = thumbprintable.b64_thumbprint();
let jwks = JSONWebKeySet {
keys: vec![thumbprintable],
};
let body = serde_json::to_string(&jwks)
.map_err(|e| worker::Error::RustError(e.to_string()))?;
let mut hasher = Sha256::new();
hasher.update(&body);
let digest_header = format!(
"sha-256=:{}:",
general_purpose::STANDARD.encode(hasher.finalize())
);
let mut nonce: [u8; 64] = [0; 64];
getrandom::getrandom(&mut nonce)
.map_err(|e| worker::Error::RustError(e.to_string()))?;
let mut response = Response::from_body(ResponseBody::Body(body.into_bytes()))?;
let headers = response.headers_mut();
headers.set("content-digest", &digest_header)?;
headers.set(
"content-type",
"application/http-message-signatures-directory+json",
)?;
let mut generator = SignatureHeaderGenerator {
authority,
digest_header: digest_header.clone(),
response_headers: headers,
};
let signer = MessageSigner {
keyid: thumbprint,
nonce: general_purpose::STANDARD.encode(nonce),
tag: "http-message-signatures-directory".to_string(),
};
signer
.generate_signature_headers_content(
&mut generator,
Duration::from_secs(10),
Algorithm::Ed25519,
signing_key.as_bytes(),
)
.unwrap();
Ok(response)
}
_ => Response::from_html(README),
}
}