From 9eaad3d7bb5e625eb428a3d035dd3ee717d818c9 Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Mon, 29 Apr 2024 20:17:56 +0200 Subject: [PATCH] [wip] http signatures refactor --- lib/fetch/src/lib.rs | 12 +- lib/fetch/src/signatures.rs | 629 +++++++++++++++++++----------------- lib/puppy/src/lib.rs | 2 +- 3 files changed, 337 insertions(+), 306 deletions(-) diff --git a/lib/fetch/src/lib.rs b/lib/fetch/src/lib.rs index a23d19e..bbe5a74 100644 --- a/lib/fetch/src/lib.rs +++ b/lib/fetch/src/lib.rs @@ -4,7 +4,7 @@ use http_body_util::BodyExt as _; use reqwest::Body; use serde_json::{json, Value}; -use crate::signatures::HS2019; +use crate::signatures::{Options, HS2019}; pub use http; @@ -91,10 +91,10 @@ pub async fn resolve(key: &SigningKey, target: &str) -> reqwest::Result { .body(()) .unwrap(); - // hs2019 works with masto - key.sign(HS2019, &req) + // hs2019 works with masto, but (currently) our implementation is limited so that we cannot use those. + key.sign(Options::LEGACY, &req) .expect("signing error") - .attach_to(&mut req); + .commit(&mut req); reqwest::Client::new() .execute(req.map(|_| Body::default()).try_into().unwrap()) @@ -116,9 +116,9 @@ pub async fn forward(key: &SigningKey, target: &str) -> reqwest::Result; @@ -153,11 +157,11 @@ impl Public { impl SigningKey { /// Create a signature for `req` using the given algorithm, for `GET` requests. - pub fn sign(&self, alg: Algorithm, req: &Request<()>) -> Result, String> { - let pieces = gather_pieces(&req)?; - let signing_string = make_signing_string(&pieces); - let signature = create(signing_string, alg, &self.inner, &self.id, pieces)?; - Ok(signature) + pub fn sign(&self, opt: Options, req: &Request<()>) -> Result { + let target = format_target(&req, opt); + IR::partial(&req, &target, opt)? + .signed(self)? + .to_signature() } /// Create a signature for `req` using the given algorithm, and calculate and attach the `digest` header to /// the request (if it doesn't already have one). @@ -165,9 +169,9 @@ impl SigningKey { /// This is required by most implementations when POSTing to an inbox. pub fn sign_with_digest( &self, - alg: Algorithm, + opt: Options, req: &mut Request, - ) -> Result, String> + ) -> Result where T: AsRef<[u8]>, { @@ -176,20 +180,26 @@ impl SigningKey { } impl VerificationKey { - /// Verify the request's signature. - pub fn verify(&self, sig: Signature<'_>) -> Result<(), String> { - use rsa::pkcs1v15::Signature; - // TODO: support elliptic curve keys - // TODO: nicer error reporting - let Public(key) = self.inner.clone(); - let verifying_key = VerifyingKey::::new(key); - let decoded = BASE64_STANDARD - .decode(&sig.signature_encoded) - .map_err(|e| e.to_string())?; - let signature = Signature::try_from(decoded.as_slice()).map_err(|e| e.to_string())?; - verifying_key - .verify(sig.signing_string.as_bytes(), &signature) - .map_err(|e| e.to_string()) + /// Test the signature against three requirements: + /// + /// 1. The signature must not be expired. + /// 2. The signature's `keyId` must be the same as `self`'s. + /// 3. The `signed_str` must have been signed by the private counterpart of `self`. + pub fn verify(&self, sig: &Signature) -> Result<(), String> { + if sig.is_expired_at(Utc::now()) { + do yeet format!("Signature is expired: (deadline was {})", sig.expires()); + } + if sig.key_id() != &self.id { + do yeet format! { + "Mismatched key id; signature's key id is '{}', while presented key has '{}'", + sig.key_id(), + self.id + }; + } + if !sig.was_signed_by(&self.inner)? { + do yeet "Signature was not generated by the presented key's private counterpart"; + } + Ok(()) } /// Encode a verification key so that it can be presented in a json. pub fn serialize(self) -> Key { @@ -217,20 +227,22 @@ pub const HS2019: Algorithm = Algorithm("hs2019"); pub const RSA_SHA256: Algorithm = Algorithm("rsa-sha256"); /// A signature derived from an [`http::Request`]. -pub struct Signature<'k> { - key_id: &'k str, - alg: Algorithm, - components: Vec, - created: String, - expires: String, - signing_string: Cow<'k, str>, - signature_encoded: String, +pub struct Signature { + key_id: String, + components: Vec<(String, String)>, + time_range: (DateTime, DateTime), + target_str: String, + signed_str: String, + signature: String, + algorithm: Algorithm, } -impl Signature<'_> { +impl Signature { /// Attempt to extract a signature from a request. - pub fn derive(req: &Request) -> Result, String> { - parse(req) + pub fn derive(req: &Request) -> Result { + let target = format_target(&req, Options::MODERN); + let ir = IR::<&str>::from_request(req, target.as_ref())?; + ir.to_signature() } /// Obtain the key id for the signature. pub fn key_id(&self) -> &str { @@ -239,99 +251,50 @@ impl Signature<'_> { /// Get the time the signature was created. This information is extracted from the `(created)` /// pseudo-header if it is defined, and the `date` http header otherwise. pub fn created(&self) -> DateTime { - todo!() + self.time_range.0 } - /// If specified, get the expiry time. - pub fn expires(&self) -> Option> { - todo!() + /// If specified, get the `(expires)` header, otherwise get the creation time + the configured grace window. + pub fn expires(&self) -> DateTime { + self.time_range.1 } /// Retrieve the algorithm used for the signature. pub fn algorithm(&self) -> Algorithm { - self.alg + self.algorithm } /// Attach `self` to `req` as the `signature` header. - pub fn attach_to(self, req: &mut Request) { + pub fn commit(self, req: &mut Request) { req.headers_mut().insert("signature", self.make_header()); } + /// Determine whether `self` was signed by the private counterpart of `key`. + pub fn was_signed_by(&self, key: &Public) -> Result { + use rsa::pkcs1v15::{VerifyingKey, Signature}; + + let Public(inner) = key.clone(); + let key = VerifyingKey::::new(inner); + + let raw_buf = decode(&self.signature)?; + let Ok(sig) = Signature::try_from(raw_buf.as_slice()) else { + do yeet "Failed to construct signature from decoded signature"; + }; + + key.verify(self.signed_str.as_bytes(), &sig) + .map_err(|s| format!("{s:?}"))?; + + Ok(true) + } + /// Check whether the given `time` falls within the window for valid signatures. + pub fn is_expired_at(&self, time: DateTime) -> bool { + !(self.created()..self.expires()).contains(&time) + } /// Turn the signature into an HTTP header value. fn make_header(self) -> HeaderValue { - render(self) + IR::<&str>::from_signature(&self) + .to_header() + .try_into() + .unwrap() } } -/// Gather all the bits from a `Request`. -fn gather_pieces(req: &Request) -> Result, &'static str> { - let target = { - let method = req.method().as_str().to_lowercase(); - let path = req.uri().path(); - format!("{method} {path}") - }; - - let created = Utc::now(); - let expires = created + TimeDelta::minutes(5); - - let mut components = vec![ - ("(request-target)", target), - ("(created)", created.timestamp().to_string()), - ("(expires)", expires.timestamp().to_string()), - ("host", req.uri().host().unwrap().to_owned()), - ]; - - if let Method::POST | Method::PUT | Method::PATCH = *req.method() { - let digest = req - .headers() - .get("digest") - .map(|v| v.to_str().unwrap().to_string()) - .ok_or("digest header is required for POST, PUT and PATCH requests")?; - components.push(("digest", digest)); - } - - Ok(components) -} - -fn make_signing_string(pieces: &[Component]) -> String { - pieces - .iter() - .map(|(k, v)| format!("{k}: {v}")) - .intersperse("\n".to_string()) - .collect() -} - -type Component = (&'static str, String); - -/// Sign the `signing_string`. -fn create<'s>( - signing_string: String, - alg: Algorithm, - key: &Private, - key_url: &'s str, - components: Vec, -) -> Result, String> { - let created = components - .iter() - .find_map(|(k, v)| (*k == "(created)").then_some(v)) - .cloned() - .unwrap(); - let expires = components - .iter() - .find_map(|(k, v)| (*k == "(expires)").then_some(v)) - .cloned() - .unwrap(); - // Regardless of the algorithm, we produce RSA-SHA256 signatures, because this is broadly compatible - // with everything. - let signature = sign_rsa_sha256(&signing_string, key)?; - let encoded = BASE64_STANDARD.encode(signature); - Ok(Signature { - signature_encoded: encoded, - signing_string: signing_string.into(), - key_id: key_url, - components, - created, - expires, - alg, - }) -} - /// `rsa-sha256` is created using an rsa key and a sha256 hash. fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result, String> { use rsa::pkcs1v15::SigningKey; @@ -341,108 +304,61 @@ fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result, Strin Ok(buf) } -/// Format the signature. -fn render(sig: Signature<'_>) -> HeaderValue { - let headers = sig - .components - .iter() - // We only need the header names. - .map(|(name, _)| name.as_ref()) - // Names need to be space-separated. - .intersperse(" ") - // Equivalent to `fold ["a", "b"]` in haskell - .collect::(); +/// Default for when +const EXPIRY_WINDOW: TimeDelta = TimeDelta::minutes(5); - #[rustfmt::skip] - let data = [ - ("keyId", sig.key_id), - ("created", &sig.created), - ("expires", &sig.expires), - ("algorithm", sig.alg.0), - ("headers", &headers), - ("signature", &sig.signature_encoded), - ]; - - // Ok, now let's put it all together. - data.into_iter() - // Step 1: all the values need to be surrounded by quotes - .map(|(k, v)| (k, format!(r#""{v}""#))) - // Step 2. join each pair together - .map(|(k, v)| format!("{k}={v}")) - // Step 3. comma separate everything - .intersperse(", ".to_string()) - // Step 4. fold the entire thing into one - .collect::() - // Then, it needs to become a header value - .try_into() - .expect("signature formatting should give a correct header value") +/// Configuration for the behavior of the signing and verification routines. +/// +/// This struct is non-exhaustive. +#[derive(Clone, Copy)] +pub struct Options { + /// Whether to use the `(created)` and `(expires)`. If `false`, the `date` header is used instead. + /// + /// Defaults to `true`. + pub use_created: bool, + /// Quirk for older mastodon versions, which don't incorporate the query string into the signing + /// string during verification. + /// + /// Defaults to `false`. + pub strip_query: bool, + /// For how long the signature is valid. + /// + /// For signing, this only has an effect if `use_created` is also set. For verification, this is only + /// used if the `(expires)` pseudo-header is not present. + /// + /// Defaults to 5 minutes. + pub expires_after: TimeDelta, + /// Which signature algorithm to use. + /// + /// Defaults to [`"hs2019"`][super::HS2019]. + pub algorithm: Algorithm, } -// TODO: clean this mess up -fn parse<'s, T>(req: &'s Request) -> Result, String> { - let Some(sig_header) = req.headers().get("signature").and_then(|v| v.to_str().ok()) else { - do yeet "No signature header"; +impl Options { + /// Use hs2019 with the `(created)` pseudo-header. + pub const MODERN: Options = Options { + use_created: true, + strip_query: false, + expires_after: EXPIRY_WINDOW, + algorithm: HS2019, }; - - let stuff: HashMap<&str, &str> = sig_header - .split(", ") - .filter_map(|piece| piece.split_once('=')) - // TODO: technically more liberal than the spec - .map(|(k, v)| (k, v.trim_matches('"'))) - .collect(); - - let inputs: Vec<&str> = stuff["headers"].split(' ').collect(); - - let (created, expires) = if inputs.contains(&"(created)") && inputs.contains(&"(expires)") { - (stuff["created"].to_string(), stuff["expires"].to_string()) - } else { - // TODO: support "date" header instead of created/expires - do yeet "Only (created) + (expires) is currently supported"; + /// Use rsa-sha256 with the `date` header. + pub const LEGACY: Options = Options { + use_created: false, + algorithm: RSA_SHA256, + ..Options::MODERN }; +} - let alg = match stuff.get("algorithm") { - Some(&"hs2019") => HS2019, - Some(&"rsa-sha256") => RSA_SHA256, - Some(alg) => do yeet format!("unsupported alg: {alg}"), - None => do yeet "Missing `algorithm`", - }; - - let components = { - let target = { - let method = req.method().as_str().to_lowercase(); - let path = req.uri().path(); - format!("{method} {path}") - }; - - let Some(host) = req.headers().get("host").and_then(|v| v.to_str().ok()) else { - do yeet "host header is required" - }; - let mut components = vec![ - ("(request-target)", target), - ("(created)", created.clone()), - ("(expires)", expires.clone()), - ("host", req.uri().host().unwrap_or(host).to_owned()), - ]; - - if let Method::POST | Method::PUT | Method::PATCH = *req.method() { - let Some(digest) = req.headers().get("digest").and_then(|v| v.to_str().ok()) else { - do yeet "Digest header is required for POST/PATCH/PUT" - }; - components.push(("digest", digest.to_string())); +impl Default for Options { + fn default() -> Self { + Options { + use_created: true, + strip_query: false, + expires_after: EXPIRY_WINDOW, + algorithm: HS2019, } - components - }; - let signing_string = make_signing_string(&components).into(); - - Ok(Signature { - key_id: stuff["keyId"], - signature_encoded: stuff["signature"].to_string(), - alg, - created, - expires, - components, - signing_string, - }) + } } mod new { @@ -451,76 +367,28 @@ mod new { use http::{HeaderName, HeaderValue, Method, Request}; use rsa::sha2::{Digest, Sha256}; - use super::{sign_rsa_sha256, Algorithm, SigningKey, HS2019, RSA_SHA256}; + use super::{ + sign_rsa_sha256, Algorithm, Options, Signature, SigningKey, EXPIRY_WINDOW, HS2019, + RSA_SHA256, + }; - /// Default for when - const EXPIRY_WINDOW: TimeDelta = TimeDelta::minutes(5); - - /// Configuration for the behavior of the signing and verification routines. - /// - /// This struct is non-exhaustive. - #[derive(Clone, Copy)] - pub struct Options { - /// Whether to use the `(created)` and `(expires)`. If `false`, the `date` header is used instead. - /// - /// Defaults to `true`. - pub use_created: bool, - /// Quirk for older mastodon versions, which don't incorporate the query string into the signing - /// string during verification. - /// - /// Defaults to `false`. - pub strip_query: bool, - /// For how long the signature is valid. - /// - /// For signing, this only has an effect if `use_created` is also set. For verification, this is only - /// used if the `(expires)` pseudo-header is not present. - /// - /// Defaults to 5 minutes. - pub expires_after: TimeDelta, - /// Which signature algorithm to use. - /// - /// Defaults to [`"hs2019"`][super::HS2019]. - pub algorithm: Algorithm, + /// Calculate the SHA256 hash of something. + pub fn sha256(buf: impl AsRef<[u8]>) -> Vec { + ::digest(buf.as_ref()).to_vec() } - impl Options { - /// Use hs2019 with the `(created)` pseudo-header. - pub const MODERN: Options = Options { - use_created: true, - strip_query: false, - expires_after: EXPIRY_WINDOW, - algorithm: HS2019, - }; - /// Use rsa-sha256 with the `date` header. - pub const LEGACY: Options = Options { - use_created: false, - algorithm: RSA_SHA256, - ..Options::MODERN - }; + /// Base64-encode something. + pub fn encode(buf: impl AsRef<[u8]>) -> String { + BASE64_STANDARD.encode(buf.as_ref()) } - impl Default for Options { - fn default() -> Self { - Options { - use_created: true, - strip_query: false, - expires_after: EXPIRY_WINDOW, - algorithm: HS2019, - } - } - } - - /// Get the headers that need to be added to a request in order for the signature to be complete. - pub fn headers( - req: &Request<()>, - key: &SigningKey, - opt: Options, - ) -> Result, String> { - gen_headers(req, key, opt, Vec::new(), false) + /// Base64-decode something. + pub fn decode(buf: &str) -> Result, String> { + BASE64_STANDARD.decode(buf).map_err(|e| e.to_string()) } /// Generate signature headers for a request with a body. - pub fn headers_with_digest( + fn headers_with_digest( req: &Request, key: &SigningKey, opt: Options, @@ -529,8 +397,7 @@ mod new { T: AsRef<[u8]>, { let digest = { - let hash = ::digest(req.body().as_ref()); - let encoded = BASE64_STANDARD.encode(hash); + let encoded = encode(sha256(req.body())); format!("sha256={encoded}") }; gen_headers(req, key, opt, vec![("digest", digest)], true) @@ -561,11 +428,8 @@ mod new { // Preprocess the headers into the format expected by the signing functions. // NOTE: only the first value of each key is ever used. - let header_map: Vec<(&str, &str)> = req - .headers() - .iter() - // Our headers need to be turned into an association list. - .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str(), v))) + let header_map: Vec<(&str, &str)> = make_header_map(&req) + .into_iter() // The "uncommitted" headers need to be added as well .chain(extra.iter().map(|(k, v)| (*k, v.as_str()))) // And at last, our date header too, if we're adding one. @@ -643,8 +507,8 @@ mod new { fn collect_inputs<'a>( needed: impl IntoIterator + 'a, - headers: &'a [(&'a str, &'a str)], - pseudo: &'a [(&'a str, &'a str)], + headers: &[(&'a str, &'a str)], + pseudo: &[(&'a str, &'a str)], ) -> Result, String> { // Quick utility function to get stuff from an association list. fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> { @@ -654,18 +518,169 @@ mod new { let assoc = |k| { get(&headers, k) .or_else(|| get(&pseudo, k)) - .ok_or_else(|| format!("missing (pseudo)header `{k}`")) + .ok_or_else(|| format!("Missing (pseudo)header `{k}`")) .map(|v| (k, v)) }; needed.into_iter().map(assoc).try_collect() } - fn format_header(inputs: &[(&str, &str)], key: &str, alg: &str, sig: &str) -> String { - // Quick utility function to get stuff from an association list. - fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> { - map.iter().find_map(|(k, v)| (*k == key).then_some(*v)) - } + pub struct IR<'s, S = &'s str> { + target: &'s str, + inputs: Vec<(&'s str, &'s str)>, + alg: &'s str, + key: S, + sig: S, + } + impl IR<'_, S> { + /// Create an HTTP header from the IR. + pub fn to_header(&self) -> String + where + S: AsRef, + { + format_header(&self.inputs, self.key.as_ref(), self.alg, self.sig.as_ref()) + } + /// Given a *correct* target string, extract a signature header from a request. + pub fn from_request<'r, T>(req: &'r Request, target: &'r str) -> Result, String> { + let map = make_header_map(&req); + let Some(header) = get(&map, "signature") else { + do yeet "Missing required `signature` header"; + }; + let (inputs, key, alg, sig) = parse_header(header, &target, &map)?; + Ok(IR { target, inputs, key, alg, sig }) + } + /// Validate and upgrade the IR to a structured signature. + pub fn to_signature(&self) -> Result + where + S: ToString, + { + let created = Utc::now(); // TODO + let expires = Utc::now(); // TODO + let algorithm = match self.alg { + "rsa-sha256" => RSA_SHA256, + "hs2019" => HS2019, + a => do yeet format!("Unsupported algorithm {a}"), + }; + let signed_str = make_signing_string(&self.inputs); + let components = self + .inputs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + Ok(Signature { + key_id: self.key.to_string(), + signature: self.sig.to_string(), + time_range: (created, expires), + target_str: self.target.to_string(), + components, + signed_str, + algorithm, + }) + } + /// Create an IR from a signature. + pub fn from_signature<'s>(sig: &'s Signature) -> IR<'s> { + IR { + target: &sig.target_str, + inputs: sig + .components + .iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + .collect(), + key: &sig.key_id, + alg: sig.algorithm.0, + sig: &sig.signature, + } + } + /// Create a signing string. + pub fn to_signing_str(&self) -> String { + make_signing_string(&self.inputs) + } + } + + impl<'s> IR<'s, ()> { + /// Create a partial, unsigned IR. + pub fn partial<'r, T>( + req: &'r Request, + target: &'r str, + opt: Options, + ) -> Result, String> { + let map = make_header_map(req); + let digest = req.method() == Method::POST; + let inputs = compute_inputs(&map, target, opt, digest)?; + Ok(IR { + target, + inputs, + alg: opt.algorithm.0, + key: (), + sig: (), + }) + } + /// Sign a partially constructed IR to make it actually useful. + pub fn signed(self, key: &'s SigningKey) -> Result, String> { + let sig_str = self.to_signing_str(); + let signature = sign_rsa_sha256(&sig_str, &key.inner).map(encode)?; + Ok(IR { + target: self.target, + inputs: self.inputs, + alg: self.alg, + key: key.id.to_string(), + sig: signature, + }) + } + } + + /// With the given options and headers, compute a set of headers and pseudo-headers that (in order) are to be + /// turned into the signing string. + fn compute_inputs<'a>( + headers: &[(&'a str, &'a str)], + target: &'a str, + opt: Options, + use_digest: bool, + ) -> Result, String> { + let expires_after = opt.use_created.then_some(opt.expires_after); + // List of input names that we want. Pseudo-headers are ordered before normal headers. + let needed = ["(request-target)"] + .into_iter() + .chain(if expires_after.is_some() { + vec!["(created)", "(expired)"] + } else { + vec!["date"] + }) + .chain(use_digest.then_some("digest")) + .chain(["host"]); + + let created = Utc::now(); + let expires = created + expires_after.unwrap_or(EXPIRY_WINDOW); + + let created_str = created.timestamp().to_string(); + let expires_str = expires.timestamp().to_string(); + + // Lookup table for pseudo-headers. + // TODO: currently, `(created)` and `(expires)` can't be used because they require string alloc. + let pseudo = [ + ("(request-target)", target), + // ("(created)", created_str.as_str()), FIXME + // ("(expired)", expires_str.as_str()), FIXME + ]; + + collect_inputs(needed, &headers, &pseudo) + } + + /// Allocate a `(request-target)` buffer. + pub fn format_target(req: &Request, opt: Options) -> String { + let path = if opt.strip_query { + req.uri().path() + } else { + req.uri() + .path_and_query() + .map(|r| r.as_str()) + .unwrap_or_else(|| req.uri().path()) + }; + let method = req.method().as_str().to_ascii_lowercase(); + format!("{method} {path}") + } + + fn format_header(inputs: &[(&str, &str)], key: &str, alg: &str, sig: &str) -> String { // Format all the headers in the order that we used them in the signing string. let headers: String = inputs .iter() @@ -703,14 +718,11 @@ mod new { .collect::() } - /// Partial inverse of `format_header_` (sort of, we don't have enough information to construct the inputs map from - /// within the function). - fn parse_header(header: &str) -> Result<(Vec<(&str, Option<&str>)>, &str, &str, &str), String> { - // Quick utility function to get stuff from an association list. - fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> { - map.iter().find_map(|(k, v)| (*k == key).then_some(*v)) - } - + fn parse_header<'s>( + header: &'s str, + target: &'s str, + extra: &[(&'s str, &'s str)], + ) -> Result<(Vec<(&'s str, &'s str)>, &'s str, &'s str, &'s str), String> { // Parse the top-level table. let table: Vec<(&str, &str)> = header // Split into entries @@ -734,28 +746,47 @@ mod new { do yeet "Missing `signature` field" }; - // Construct a partial multi-map of all the inputs that are used to generate the signing string. - let inputs: Vec<(&str, Option<&str>)> = headers + let inputs: Vec<(&str, &str)> = headers // Headers and pseudo-headers are separated by spaces in the order in which they appear. .split(' ') // Map created and expires pseudo-headers to the ones specified in the inputs table. .map(|k| match k { + "(request-target)" => Ok(("(request-target)", target)), // If these exist, the table must have them, but other than that they're optional. "(created)" => get(&table, "created") - .ok_or("`(created)` pseudo-header is listed, but does not exist") - .map(|v| ("created", Some(v))), + .ok_or("`(created)` pseudo-header is listed, but does not exist".to_string()) + .map(|v| ("(created)", v)), "(expires)" => get(&table, "expires") - .ok_or("`(expires)` pseudo-header is listed, but does not exist") - .map(|v| ("expires", Some(v))), + .ok_or("`(expires)` pseudo-header is listed, but does not exist".to_string()) + .map(|v| ("(expires)", v)), // For anything else, we don't have the required information, and we'll need access // to the entire request in order to fill in the blanks. - k => Ok((k, None)), + k => get(&extra, k) + .ok_or(format!("header '{k}' is missing")) + .map(|v| (k, v)), }) .try_collect()?; Ok((inputs, key, algorithm, signature)) } + /// Make an association list associating header names to header values. + /// + /// Allocates a new vector, but not any strings. + fn make_header_map<'r, T>(req: &'r Request) -> Vec<(&'r str, &'r str)> { + req.headers() + .iter() + // Acquire string slices of every name-value pair. + .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.as_str(), v))) + .collect() + } + + /// Quick utility function to get stuff from an association list. + fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> { + map.iter() + .find_map(|(k, v)| k.eq_ignore_ascii_case(key).then_some(*v)) + } + fn make_signing_string(data: &[(&str, &str)]) -> String { data.iter() // Each pair is separated by a colon and a space diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index beb820e..d941b2a 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -310,7 +310,7 @@ pub mod auth { // Verify the signature header on the request. let public_key = public_key.upgrade(); - if let Err(error) = public_key.verify(sig) { + if let Err(error) = public_key.verify(&sig) { Err(SigError::VerificationFailed { error }) } else { Ok(Some(Signer { ap_id: public_key.owner.into() }))