use super::b64;
use super::key::AgentKey;
pub enum SigKey<'a> {
Jwt(&'a str),
Hwk,
}
impl SigKey<'_> {
fn header_value(&self, key: &AgentKey) -> String {
match self {
SigKey::Jwt(tok) => format!("sig=jwt;jwt=\"{tok}\""),
SigKey::Hwk => {
format!(
"sig=hwk;kty=\"OKP\";crv=\"Ed25519\";x=\"{}\"",
b64::url_nopad(key.public_bytes())
)
}
}
}
}
pub fn content_digest(body: &[u8]) -> String {
let d = ring::digest::digest(&ring::digest::SHA256, body);
format!("sha-256=:{}:", b64::std_pad(d.as_ref()))
}
pub fn sign_request(
key: &AgentKey,
method: &str,
authority: &str,
path: &str,
sigkey: SigKey<'_>,
created: u64,
digest: Option<&str>,
) -> Vec<(String, String)> {
let key_hdr = sigkey.header_value(key);
let covered = if digest.is_some() {
r#"("@method" "@authority" "@path" "content-digest" "signature-key")"#
} else {
r#"("@method" "@authority" "@path" "signature-key")"#
};
let sig_params = format!("{covered};created={created}");
let digest_line = digest
.map(|d| format!("\"content-digest\": {d}\n"))
.unwrap_or_default();
let base = format!(
"\"@method\": {method}\n\
\"@authority\": {authority}\n\
\"@path\": {path}\n\
{digest_line}\
\"signature-key\": {key_hdr}\n\
\"@signature-params\": {sig_params}"
);
let signature = key.sign(base.as_bytes());
let mut headers = vec![
("Signature-Input".into(), format!("sig={sig_params}")),
(
"Signature".into(),
format!("sig=:{}:", b64::std_pad(&signature)),
),
("Signature-Key".into(), key_hdr),
];
if let Some(d) = digest {
headers.push(("Content-Digest".into(), d.to_string()));
}
headers
}
pub fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signature_base_is_deterministic_and_verifies() {
let key = AgentKey::from_seed(&[3u8; 32]).unwrap();
let hdrs = sign_request(
&key,
"POST",
"mcp.example",
"/mcp",
SigKey::Jwt("TOK"),
1_700_000_000,
None,
);
let map: std::collections::HashMap<_, _> = hdrs.iter().cloned().collect();
assert_eq!(
map["Signature-Input"],
"sig=(\"@method\" \"@authority\" \"@path\" \"signature-key\");created=1700000000"
);
assert_eq!(map["Signature-Key"], "sig=jwt;jwt=\"TOK\"");
let sig_hdr = &map["Signature"];
assert!(sig_hdr.starts_with("sig=:") && sig_hdr.ends_with(':'));
let base = "\"@method\": POST\n\
\"@authority\": mcp.example\n\
\"@path\": /mcp\n\
\"signature-key\": sig=jwt;jwt=\"TOK\"\n\
\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"signature-key\");created=1700000000";
let raw = sig_hdr.trim_start_matches("sig=:").trim_end_matches(':');
let sig = b64::url_decode(raw).unwrap(); let vk =
ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, key.public_bytes());
vk.verify(base.as_bytes(), &sig)
.expect("signature verifies");
}
#[test]
fn content_digest_is_added_and_covered() {
let key = AgentKey::from_seed(&[5u8; 32]).unwrap();
let body = br#"{"jsonrpc":"2.0","method":"tools/call"}"#;
let digest = content_digest(body);
assert!(digest.starts_with("sha-256=:") && digest.ends_with(':'));
let hdrs = sign_request(
&key,
"POST",
"mcp.example",
"/mcp",
SigKey::Jwt("TOK"),
1,
Some(&digest),
);
let map: std::collections::HashMap<_, _> = hdrs.iter().cloned().collect();
assert_eq!(map["Content-Digest"], digest);
assert!(map["Signature-Input"].contains("\"content-digest\""));
let base = format!(
"\"@method\": POST\n\"@authority\": mcp.example\n\"@path\": /mcp\n\
\"content-digest\": {digest}\n\"signature-key\": sig=jwt;jwt=\"TOK\"\n\
\"@signature-params\": (\"@method\" \"@authority\" \"@path\" \"content-digest\" \"signature-key\");created=1"
);
let raw = map["Signature"]
.trim_start_matches("sig=:")
.trim_end_matches(':');
let sig = b64::url_decode(raw).unwrap();
let vk =
ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, key.public_bytes());
vk.verify(base.as_bytes(), &sig)
.expect("digest-covering signature verifies");
}
#[test]
fn hwk_scheme_presents_inline_okp_params() {
let key = AgentKey::from_seed(&[9u8; 32]).unwrap();
let hdrs = sign_request(&key, "POST", "apd.example", "/enroll", SigKey::Hwk, 1, None);
let map: std::collections::HashMap<_, _> = hdrs.iter().cloned().collect();
let sk = &map["Signature-Key"];
assert!(sk.starts_with("sig=hwk;"), "got: {sk}");
assert!(sk.contains(r#"kty="OKP""#), "got: {sk}");
assert!(sk.contains(r#"crv="Ed25519""#), "got: {sk}");
assert!(
!sk.contains("jwk="),
"must not carry a jwk blob param: {sk}"
);
assert!(!sk.contains("alg="), "hwk must not carry alg: {sk}");
let x = sk
.split(r#"x=""#)
.nth(1)
.and_then(|s| s.split('"').next())
.expect("x param present");
assert_eq!(x, b64::url_nopad(key.public_bytes()));
}
}