[wip] http signatures refactor

This commit is contained in:
Riley Apeldoorn 2024-04-29 20:17:56 +02:00
parent 9845603846
commit 9eaad3d7bb
3 changed files with 337 additions and 306 deletions

View file

@ -4,7 +4,7 @@ use http_body_util::BodyExt as _;
use reqwest::Body; use reqwest::Body;
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::signatures::HS2019; use crate::signatures::{Options, HS2019};
pub use http; pub use http;
@ -91,10 +91,10 @@ pub async fn resolve(key: &SigningKey, target: &str) -> reqwest::Result<Value> {
.body(()) .body(())
.unwrap(); .unwrap();
// hs2019 works with masto // hs2019 works with masto, but (currently) our implementation is limited so that we cannot use those.
key.sign(HS2019, &req) key.sign(Options::LEGACY, &req)
.expect("signing error") .expect("signing error")
.attach_to(&mut req); .commit(&mut req);
reqwest::Client::new() reqwest::Client::new()
.execute(req.map(|_| Body::default()).try_into().unwrap()) .execute(req.map(|_| Body::default()).try_into().unwrap())
@ -116,9 +116,9 @@ pub async fn forward(key: &SigningKey, target: &str) -> reqwest::Result<http::Re
.body(()) .body(())
.unwrap(); .unwrap();
// hs2019 works with masto // hs2019 works with masto
key.sign(HS2019, &req) key.sign(Options::LEGACY, &req)
.expect("signing error") .expect("signing error")
.attach_to(&mut req); .commit(&mut req);
let resp = reqwest::Client::new() let resp = reqwest::Client::new()
.execute(req.map(|_| Body::default()).try_into().unwrap()) .execute(req.map(|_| Body::default()).try_into().unwrap())

View file

@ -40,6 +40,10 @@ use rsa::{
use base64::prelude::*; use base64::prelude::*;
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::signatures::new::decode;
use self::new::{format_target, IR};
/// A key that can be used to verify a request signature. /// A key that can be used to verify a request signature.
pub type VerificationKey = Key<Public>; pub type VerificationKey = Key<Public>;
@ -153,11 +157,11 @@ impl Public {
impl SigningKey { impl SigningKey {
/// Create a signature for `req` using the given algorithm, for `GET` requests. /// Create a signature for `req` using the given algorithm, for `GET` requests.
pub fn sign(&self, alg: Algorithm, req: &Request<()>) -> Result<Signature<'_>, String> { pub fn sign(&self, opt: Options, req: &Request<()>) -> Result<Signature, String> {
let pieces = gather_pieces(&req)?; let target = format_target(&req, opt);
let signing_string = make_signing_string(&pieces); IR::partial(&req, &target, opt)?
let signature = create(signing_string, alg, &self.inner, &self.id, pieces)?; .signed(self)?
Ok(signature) .to_signature()
} }
/// Create a signature for `req` using the given algorithm, and calculate and attach the `digest` header to /// 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). /// 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. /// This is required by most implementations when POSTing to an inbox.
pub fn sign_with_digest<T>( pub fn sign_with_digest<T>(
&self, &self,
alg: Algorithm, opt: Options,
req: &mut Request<T>, req: &mut Request<T>,
) -> Result<Signature<'_>, String> ) -> Result<Signature, String>
where where
T: AsRef<[u8]>, T: AsRef<[u8]>,
{ {
@ -176,20 +180,26 @@ impl SigningKey {
} }
impl VerificationKey { impl VerificationKey {
/// Verify the request's signature. /// Test the signature against three requirements:
pub fn verify(&self, sig: Signature<'_>) -> Result<(), String> { ///
use rsa::pkcs1v15::Signature; /// 1. The signature must not be expired.
// TODO: support elliptic curve keys /// 2. The signature's `keyId` must be the same as `self`'s.
// TODO: nicer error reporting /// 3. The `signed_str` must have been signed by the private counterpart of `self`.
let Public(key) = self.inner.clone(); pub fn verify(&self, sig: &Signature) -> Result<(), String> {
let verifying_key = VerifyingKey::<Sha256>::new(key); if sig.is_expired_at(Utc::now()) {
let decoded = BASE64_STANDARD do yeet format!("Signature is expired: (deadline was {})", sig.expires());
.decode(&sig.signature_encoded) }
.map_err(|e| e.to_string())?; if sig.key_id() != &self.id {
let signature = Signature::try_from(decoded.as_slice()).map_err(|e| e.to_string())?; do yeet format! {
verifying_key "Mismatched key id; signature's key id is '{}', while presented key has '{}'",
.verify(sig.signing_string.as_bytes(), &signature) sig.key_id(),
.map_err(|e| e.to_string()) 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. /// Encode a verification key so that it can be presented in a json.
pub fn serialize(self) -> Key { pub fn serialize(self) -> Key {
@ -217,20 +227,22 @@ pub const HS2019: Algorithm = Algorithm("hs2019");
pub const RSA_SHA256: Algorithm = Algorithm("rsa-sha256"); pub const RSA_SHA256: Algorithm = Algorithm("rsa-sha256");
/// A signature derived from an [`http::Request`]. /// A signature derived from an [`http::Request`].
pub struct Signature<'k> { pub struct Signature {
key_id: &'k str, key_id: String,
alg: Algorithm, components: Vec<(String, String)>,
components: Vec<Component>, time_range: (DateTime<Utc>, DateTime<Utc>),
created: String, target_str: String,
expires: String, signed_str: String,
signing_string: Cow<'k, str>, signature: String,
signature_encoded: String, algorithm: Algorithm,
} }
impl Signature<'_> { impl Signature {
/// Attempt to extract a signature from a request. /// Attempt to extract a signature from a request.
pub fn derive<T>(req: &Request<T>) -> Result<Signature<'_>, String> { pub fn derive<T>(req: &Request<T>) -> Result<Signature, String> {
parse(req) 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. /// Obtain the key id for the signature.
pub fn key_id(&self) -> &str { 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)` /// 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. /// pseudo-header if it is defined, and the `date` http header otherwise.
pub fn created(&self) -> DateTime<Utc> { pub fn created(&self) -> DateTime<Utc> {
todo!() self.time_range.0
} }
/// If specified, get the expiry time. /// If specified, get the `(expires)` header, otherwise get the creation time + the configured grace window.
pub fn expires(&self) -> Option<DateTime<Utc>> { pub fn expires(&self) -> DateTime<Utc> {
todo!() self.time_range.1
} }
/// Retrieve the algorithm used for the signature. /// Retrieve the algorithm used for the signature.
pub fn algorithm(&self) -> Algorithm { pub fn algorithm(&self) -> Algorithm {
self.alg self.algorithm
} }
/// Attach `self` to `req` as the `signature` header. /// Attach `self` to `req` as the `signature` header.
pub fn attach_to<T>(self, req: &mut Request<T>) { pub fn commit<T>(self, req: &mut Request<T>) {
req.headers_mut().insert("signature", self.make_header()); 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<bool, String> {
use rsa::pkcs1v15::{VerifyingKey, Signature};
let Public(inner) = key.clone();
let key = VerifyingKey::<Sha256>::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<Utc>) -> bool {
!(self.created()..self.expires()).contains(&time)
}
/// Turn the signature into an HTTP header value. /// Turn the signature into an HTTP header value.
fn make_header(self) -> HeaderValue { 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<T>(req: &Request<T>) -> Result<Vec<Component>, &'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<Component>,
) -> Result<Signature<'s>, 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. /// `rsa-sha256` is created using an rsa key and a sha256 hash.
fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result<Vec<u8>, String> { fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result<Vec<u8>, String> {
use rsa::pkcs1v15::SigningKey; use rsa::pkcs1v15::SigningKey;
@ -341,126 +304,14 @@ fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result<Vec<u8>, Strin
Ok(buf) Ok(buf)
} }
/// Format the signature. /// Default for when
fn render(sig: Signature<'_>) -> HeaderValue { const EXPIRY_WINDOW: TimeDelta = TimeDelta::minutes(5);
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::<String>();
#[rustfmt::skip] /// Configuration for the behavior of the signing and verification routines.
let data = [ ///
("keyId", sig.key_id), /// This struct is non-exhaustive.
("created", &sig.created), #[derive(Clone, Copy)]
("expires", &sig.expires), pub struct Options {
("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::<String>()
// Then, it needs to become a header value
.try_into()
.expect("signature formatting should give a correct header value")
}
// TODO: clean this mess up
fn parse<'s, T>(req: &'s Request<T>) -> Result<Signature<'s>, String> {
let Some(sig_header) = req.headers().get("signature").and_then(|v| v.to_str().ok()) else {
do yeet "No signature header";
};
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";
};
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()));
}
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 {
use base64::prelude::*;
use chrono::{TimeDelta, Utc};
use http::{HeaderName, HeaderValue, Method, Request};
use rsa::sha2::{Digest, Sha256};
use super::{sign_rsa_sha256, Algorithm, SigningKey, 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. /// Whether to use the `(created)` and `(expires)`. If `false`, the `date` header is used instead.
/// ///
/// Defaults to `true`. /// Defaults to `true`.
@ -481,9 +332,9 @@ mod new {
/// ///
/// Defaults to [`"hs2019"`][super::HS2019]. /// Defaults to [`"hs2019"`][super::HS2019].
pub algorithm: Algorithm, pub algorithm: Algorithm,
} }
impl Options { impl Options {
/// Use hs2019 with the `(created)` pseudo-header. /// Use hs2019 with the `(created)` pseudo-header.
pub const MODERN: Options = Options { pub const MODERN: Options = Options {
use_created: true, use_created: true,
@ -497,9 +348,9 @@ mod new {
algorithm: RSA_SHA256, algorithm: RSA_SHA256,
..Options::MODERN ..Options::MODERN
}; };
} }
impl Default for Options { impl Default for Options {
fn default() -> Self { fn default() -> Self {
Options { Options {
use_created: true, use_created: true,
@ -508,19 +359,36 @@ mod new {
algorithm: HS2019, algorithm: HS2019,
} }
} }
}
mod new {
use base64::prelude::*;
use chrono::{TimeDelta, Utc};
use http::{HeaderName, HeaderValue, Method, Request};
use rsa::sha2::{Digest, Sha256};
use super::{
sign_rsa_sha256, Algorithm, Options, Signature, SigningKey, EXPIRY_WINDOW, HS2019,
RSA_SHA256,
};
/// Calculate the SHA256 hash of something.
pub fn sha256(buf: impl AsRef<[u8]>) -> Vec<u8> {
<Sha256 as Digest>::digest(buf.as_ref()).to_vec()
} }
/// Get the headers that need to be added to a request in order for the signature to be complete. /// Base64-encode something.
pub fn headers( pub fn encode(buf: impl AsRef<[u8]>) -> String {
req: &Request<()>, BASE64_STANDARD.encode(buf.as_ref())
key: &SigningKey, }
opt: Options,
) -> Result<Vec<(HeaderName, HeaderValue)>, String> { /// Base64-decode something.
gen_headers(req, key, opt, Vec::new(), false) pub fn decode(buf: &str) -> Result<Vec<u8>, String> {
BASE64_STANDARD.decode(buf).map_err(|e| e.to_string())
} }
/// Generate signature headers for a request with a body. /// Generate signature headers for a request with a body.
pub fn headers_with_digest<T>( fn headers_with_digest<T>(
req: &Request<T>, req: &Request<T>,
key: &SigningKey, key: &SigningKey,
opt: Options, opt: Options,
@ -529,8 +397,7 @@ mod new {
T: AsRef<[u8]>, T: AsRef<[u8]>,
{ {
let digest = { let digest = {
let hash = <Sha256 as Digest>::digest(req.body().as_ref()); let encoded = encode(sha256(req.body()));
let encoded = BASE64_STANDARD.encode(hash);
format!("sha256={encoded}") format!("sha256={encoded}")
}; };
gen_headers(req, key, opt, vec![("digest", digest)], true) 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. // Preprocess the headers into the format expected by the signing functions.
// NOTE: only the first value of each key is ever used. // NOTE: only the first value of each key is ever used.
let header_map: Vec<(&str, &str)> = req let header_map: Vec<(&str, &str)> = make_header_map(&req)
.headers() .into_iter()
.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)))
// The "uncommitted" headers need to be added as well // The "uncommitted" headers need to be added as well
.chain(extra.iter().map(|(k, v)| (*k, v.as_str()))) .chain(extra.iter().map(|(k, v)| (*k, v.as_str())))
// And at last, our date header too, if we're adding one. // And at last, our date header too, if we're adding one.
@ -643,8 +507,8 @@ mod new {
fn collect_inputs<'a>( fn collect_inputs<'a>(
needed: impl IntoIterator<Item = &'a str> + 'a, needed: impl IntoIterator<Item = &'a str> + 'a,
headers: &'a [(&'a str, &'a str)], headers: &[(&'a str, &'a str)],
pseudo: &'a [(&'a str, &'a str)], pseudo: &[(&'a str, &'a str)],
) -> Result<Vec<(&'a str, &'a str)>, String> { ) -> Result<Vec<(&'a str, &'a str)>, String> {
// Quick utility function to get stuff from an association list. // Quick utility function to get stuff from an association list.
fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> { fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> {
@ -654,18 +518,169 @@ mod new {
let assoc = |k| { let assoc = |k| {
get(&headers, k) get(&headers, k)
.or_else(|| get(&pseudo, 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)) .map(|v| (k, v))
}; };
needed.into_iter().map(assoc).try_collect() needed.into_iter().map(assoc).try_collect()
} }
fn format_header(inputs: &[(&str, &str)], key: &str, alg: &str, sig: &str) -> String { pub struct IR<'s, S = &'s str> {
// Quick utility function to get stuff from an association list. target: &'s str,
fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> { inputs: Vec<(&'s str, &'s str)>,
map.iter().find_map(|(k, v)| (*k == key).then_some(*v)) alg: &'s str,
key: S,
sig: S,
} }
impl<S> IR<'_, S> {
/// Create an HTTP header from the IR.
pub fn to_header(&self) -> String
where
S: AsRef<str>,
{
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<T>, target: &'r str) -> Result<IR<'r>, 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<Signature, String>
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<T>,
target: &'r str,
opt: Options,
) -> Result<IR<'r, ()>, 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<IR<'s, String>, 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<Vec<(&'a str, &'a str)>, 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<T>(req: &Request<T>, 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. // Format all the headers in the order that we used them in the signing string.
let headers: String = inputs let headers: String = inputs
.iter() .iter()
@ -703,14 +718,11 @@ mod new {
.collect::<String>() .collect::<String>()
} }
/// Partial inverse of `format_header_` (sort of, we don't have enough information to construct the inputs map from fn parse_header<'s>(
/// within the function). header: &'s str,
fn parse_header(header: &str) -> Result<(Vec<(&str, Option<&str>)>, &str, &str, &str), String> { target: &'s str,
// Quick utility function to get stuff from an association list. extra: &[(&'s str, &'s str)],
fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> { ) -> Result<(Vec<(&'s str, &'s str)>, &'s str, &'s str, &'s str), String> {
map.iter().find_map(|(k, v)| (*k == key).then_some(*v))
}
// Parse the top-level table. // Parse the top-level table.
let table: Vec<(&str, &str)> = header let table: Vec<(&str, &str)> = header
// Split into entries // Split into entries
@ -734,28 +746,47 @@ mod new {
do yeet "Missing `signature` field" 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, &str)> = headers
let inputs: Vec<(&str, Option<&str>)> = headers
// Headers and pseudo-headers are separated by spaces in the order in which they appear. // Headers and pseudo-headers are separated by spaces in the order in which they appear.
.split(' ') .split(' ')
// Map created and expires pseudo-headers to the ones specified in the inputs table. // Map created and expires pseudo-headers to the ones specified in the inputs table.
.map(|k| match k { .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. // If these exist, the table must have them, but other than that they're optional.
"(created)" => get(&table, "created") "(created)" => get(&table, "created")
.ok_or("`(created)` pseudo-header is listed, but does not exist") .ok_or("`(created)` pseudo-header is listed, but does not exist".to_string())
.map(|v| ("created", Some(v))), .map(|v| ("(created)", v)),
"(expires)" => get(&table, "expires") "(expires)" => get(&table, "expires")
.ok_or("`(expires)` pseudo-header is listed, but does not exist") .ok_or("`(expires)` pseudo-header is listed, but does not exist".to_string())
.map(|v| ("expires", Some(v))), .map(|v| ("(expires)", v)),
// For anything else, we don't have the required information, and we'll need access // 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. // 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()?; .try_collect()?;
Ok((inputs, key, algorithm, signature)) 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<T>) -> 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 { fn make_signing_string(data: &[(&str, &str)]) -> String {
data.iter() data.iter()
// Each pair is separated by a colon and a space // Each pair is separated by a colon and a space

View file

@ -310,7 +310,7 @@ pub mod auth {
// Verify the signature header on the request. // Verify the signature header on the request.
let public_key = public_key.upgrade(); 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 }) Err(SigError::VerificationFailed { error })
} else { } else {
Ok(Some(Signer { ap_id: public_key.owner.into() })) Ok(Some(Signer { ap_id: public_key.owner.into() }))