From c784966d20b797bf89f5d66f4f98826b111e4147 Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Mon, 29 Apr 2024 00:09:17 +0200 Subject: [PATCH] Split `fetch::signatures` into its own file --- bin/server/src/api.rs | 4 +- lib/fetch/src/lib.rs | 469 +----------------------------------- lib/fetch/src/signatures.rs | 449 ++++++++++++++++++++++++++++++++++ lib/puppy/src/lib.rs | 6 +- lib/puppy/src/post.rs | 2 +- 5 files changed, 465 insertions(+), 465 deletions(-) create mode 100644 lib/fetch/src/signatures.rs diff --git a/bin/server/src/api.rs b/bin/server/src/api.rs index b01366e..c2219bb 100644 --- a/bin/server/src/api.rs +++ b/bin/server/src/api.rs @@ -27,13 +27,13 @@ pub mod ap { }; // Look up the actor's key in the store (which is accessible through the puppy context). - let (signing_key) = puppy::context::<_, puppy::Error>(|cx| try { + let signing_key = puppy::context::<_, puppy::Error>(|cx| try { let actor = Actor::by_username(&cx, user)?.unwrap(); let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) = cx.store().get_mixin_many(actor.key)?; let Id(owner) = cx.store().get_alias(actor.key)?.unwrap(); let inner = Private::decode_pem(&key_pem); - SigningKey { key_id, owner, inner } + SigningKey { id: key_id, owner, inner } }) .unwrap(); diff --git a/lib/fetch/src/lib.rs b/lib/fetch/src/lib.rs index b58662c..ebccfc8 100644 --- a/lib/fetch/src/lib.rs +++ b/lib/fetch/src/lib.rs @@ -1,8 +1,6 @@ #![feature(iter_intersperse, yeet_expr)] use chrono::Utc; -use derive_more::{Display, From, Into}; use http_body_util::BodyExt as _; -use signatures::{Private, Public}; use reqwest::Body; use serde_json::{json, Value}; @@ -10,11 +8,14 @@ use crate::signatures::HS2019; pub use http; +pub use signatures::{Key, SigningKey, VerificationKey}; +pub mod signatures; + pub enum Object { Activity(Activity), Actor(Actor), Object { - id: Id, + id: String, kind: String, content: Option, summary: Option, @@ -22,7 +23,7 @@ pub enum Object { } impl Object { - pub fn id(&self) -> &Id { + pub fn id(&self) -> &str { match self { Object::Activity(a) => &a.id, Object::Actor(a) => &a.id, @@ -44,8 +45,8 @@ impl Object { } pub struct Activity { - pub id: Id, - pub actor: Id, + pub id: String, + pub actor: String, pub object: Box, pub kind: T, } @@ -134,9 +135,9 @@ pub async fn forward(key: &SigningKey, target: &str) -> reqwest::Result; - -/// A key that can be used to sign a request. -pub type SigningKey = Key; - -/// A key used for authorized fetch. -/// -/// It comes in several flavors: -/// -/// - `Key` (`K` = [`String`]): PEM-encoded, can be turned into a JSON object. -/// - [`VerificationKey`] (`K` = [`Public`]): used as an input in the request signature validation process. -/// - [`SigningKey`] (`K` = [`Private`]): used as an input in the generation of a signed request. -pub struct Key { - /// The `"id"` property of the public key. - pub key_id: String, - /// The `"owner"` property. - pub owner: String, - /// Maps to the `"publicKeyPem"` property of an actor's `"publicKey"` when (de)serializing, and when the - /// key is used for doing [signatures]. - pub inner: K, -} - -impl Key { - /// Tries to find the PEM-encoded public key from the result of fetching a key id. - pub fn from_json(json: Value) -> Option { - // First, we try the object itself. - json.as_object().and_then(Key::from_map).or_else(|| { - // Because of how mastodon deals with pubkey resolution, most implementations will serve the whole actor - // object instead of just the key, so we try that first, because it is the de facto standard. - json["publicKey"].as_object().and_then(Key::from_map) - }) - } - /// Construct - fn from_map(map: &serde_json::Map) -> Option { - Some(Key { - key_id: map["id"].as_str().map(str::to_owned)?, - owner: map["owner"].as_str().map(str::to_owned)?, - inner: map["publicKeyPem"].as_str().map(str::to_owned)?, - }) - } - /// "Upgrade" a pem-encoded public key to a key that can actually be used for requests. - pub fn upgrade(self) -> Key { - let inner = Public::decode_pem(&self.inner); - Key { - key_id: self.key_id, - owner: self.owner, - inner, - } - } -} - -impl Key { - /// Encode a verification key so that it can be presented in a json. - pub fn serialize(self) -> Key { - let public_key_pem = self.inner.encode_pem(); - Key { - key_id: self.key_id, - owner: self.owner, - inner: public_key_pem, - } - } -} - -#[derive(Display, From, Into, Debug, Clone)] -pub struct Id(String); - -pub mod signatures { - //! Containment zone for the funny math that doesn't make much sense to puppy. - //! - //! This module provides ActivityPuppy's HTTP signatures implementation. The state of HTTP signatures implementations - //! is, to put it mildly, *een fucking kutzooi*. For historical reasons, no one implements it *exactly* right (much - //! like URI parsers). This implementation aims to be as broadly compatible as possible. - //! - //! The only non-deprecated [`Algorithm`] is [`"hs2019"`][HS2019], but not everyone implements it, because the initial - //! round of implementations of the spec were based on a draft, and [`"rsa-sha256"`][RSA_SHA256] is kinda the de facto - //! standard. - //! - //! # Behavior - //! - //! By default, puppy will sign with `algorithm="hs2019"` (using `(created)` and `(expires)` pseudo-headers), and retry - //! in legacy mode (using `algorithm="rsa-sha256"` with `date` header) if the signature gets rejected. - //! - //! Currently, `"hs2019"` is treated as equivalent to `"rsa-sha256"` for verification purposes. Support for elliptic - //! curve keys is planned, but not a priority. - //! - //! # Links - //! - //! More information about http signatures: - //! - //! - - //! - - - use std::{borrow::Cow, collections::HashMap, path::Path}; - - use chrono::{DateTime, TimeDelta, Utc}; - use http::{HeaderValue, Method, Request}; - use rsa::{ - signature::{Verifier as _, Signer as _, SignatureEncoding as _}, - pkcs1v15::{SigningKey, VerifyingKey}, - pkcs8::{ - DecodePrivateKey, DecodePublicKey, EncodePublicKey as _, EncodePrivateKey as _, - LineEnding, - }, - sha2::{self, Sha256}, - RsaPrivateKey, - }; - - use base64::prelude::*; - - /// A key that can be used to generate signatures. - #[derive(Clone)] - pub struct Private(rsa::RsaPrivateKey); - - impl Private { - /// Generate a new keypair. - pub fn gen() -> (Private, Public) { - let mut rng = rand::thread_rng(); - let bits = 4096; - let private = RsaPrivateKey::new(&mut rng, bits).unwrap(); - let public = private.to_public_key(); - (Private(private), Public(public)) - } - /// Get the public counterpart to this key. - pub fn get_public(&self) -> Public { - Public(self.0.to_public_key()) - } - /// Load a private key from a file on disk. - pub fn load(path: impl AsRef) -> Private { - use rsa::pkcs8::DecodePrivateKey; - DecodePrivateKey::read_pkcs8_pem_file(path) - .map(Private) - .unwrap() - } - /// PEM-encode the key PKCS#8 style. - pub fn encode_pem(&self) -> String { - self.0 - .to_pkcs8_pem(LineEnding::default()) - .unwrap() - .to_string() - } - /// Decode the key from a PKCS#8 PEM-encoded string. - pub fn decode_pem(pkcs8_pem: &str) -> Private { - DecodePrivateKey::from_pkcs8_pem(&pkcs8_pem) - .map(Private) - .unwrap() - } - } - - /// A key that can be used to verify signatures. - #[derive(Clone)] - pub struct Public(rsa::RsaPublicKey); - - impl Public { - /// PEM-encode the public key in accordance with PKCS#8. - pub fn encode_pem(&self) -> String { - self.0 - .to_public_key_pem(LineEnding::default()) - .unwrap() - .to_string() - } - /// Decode a PKCS#8 PEM-encoded public key from a string. - pub fn decode_pem(pkcs8_pem: &str) -> Public { - DecodePublicKey::from_public_key_pem(&pkcs8_pem) - .map(Public) - .unwrap() - } - } - - impl crate::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.key_id, pieces)?; - Ok(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). - /// - /// This is required by most implementations when POSTing to an inbox. - pub fn sign_with_digest( - &self, - alg: Algorithm, - req: &mut Request, - ) -> Result, String> - where - T: AsRef<[u8]>, - { - todo!() - } - } - - impl crate::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()) - } - } - - /// The algorithm to sign with. - /// - /// Your two options are: - /// - /// - [`hs2019`][HS2019], the *correct* option - /// - [`"rsa-sha256"`][RSA_SHA256], the most compatible option - #[derive(PartialEq, Debug, Clone, Copy)] - pub struct Algorithm(&'static str); - - /// `hs2019`, the only non-deprecated HTTP signatures algorithm. - pub const HS2019: Algorithm = Algorithm("hs2019"); - /// The HTTP signatures algorithm everyone uses, `rsa-sha256`. - 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, - } - - impl Signature<'_> { - /// Obtain the key id for the signature. - pub fn key_id(&self) -> &str { - &self.key_id - } - /// 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!() - } - /// If specified, get the expiry time. - pub fn expires(&self) -> Option> { - todo!() - } - /// Retrieve the algorithm used for the signature. - pub fn algorithm(&self) -> Algorithm { - self.alg - } - /// Attempt to extract a signature from a request. - pub fn derive(req: &Request) -> Result, String> { - parse(req) - } - /// Attach `self` to `req` as the `signature` header. - pub fn attach_to(self, req: &mut Request) { - req.headers_mut().insert("signature", render(self)); - } - } - - /// 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> { - let Private(rsa) = key.clone(); - let key = SigningKey::::new(rsa); - let buf = key.sign(signing_string.as_bytes()).to_vec(); - 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::(); - - #[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") - } - - // 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"; - }; - - 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, - }) - } -} diff --git a/lib/fetch/src/signatures.rs b/lib/fetch/src/signatures.rs new file mode 100644 index 0000000..d36ee86 --- /dev/null +++ b/lib/fetch/src/signatures.rs @@ -0,0 +1,449 @@ +//! Containment zone for the funny math that doesn't make much sense to puppy. +//! +//! This module provides ActivityPuppy's HTTP signatures implementation. The state of HTTP signatures implementations +//! is, to put it mildly, *een fucking kutzooi*. For historical reasons, no one implements it *exactly* right (much +//! like URI parsers). This implementation aims to be as broadly compatible as possible. +//! +//! The only non-deprecated [`Algorithm`] is [`"hs2019"`][HS2019], but not everyone implements it, because the initial +//! round of implementations of the spec were based on a draft, and [`"rsa-sha256"`][RSA_SHA256] is kinda the de facto +//! standard. +//! +//! # Behavior +//! +//! By default, puppy will sign with `algorithm="hs2019"` (using `(created)` and `(expires)` pseudo-headers), and retry +//! in legacy mode (using `algorithm="rsa-sha256"` with `date` header) if the signature gets rejected. +//! +//! Currently, `"hs2019"` is treated as equivalent to `"rsa-sha256"` for verification purposes. Support for elliptic +//! curve keys is planned, but not a priority. +//! +//! # Links +//! +//! More information about http signatures: +//! +//! - +//! - + +use std::{borrow::Cow, collections::HashMap, path::Path}; + +use chrono::{DateTime, TimeDelta, Utc}; +use http::{HeaderValue, Method, Request}; +use rsa::{ + pkcs1v15::VerifyingKey, + pkcs8::{ + DecodePrivateKey, DecodePublicKey, EncodePrivateKey as _, EncodePublicKey as _, LineEnding, + }, + sha2::Sha256, + signature::{SignatureEncoding as _, Signer as _, Verifier as _}, + RsaPrivateKey, +}; + +use base64::prelude::*; +use serde_json::{Map, Value}; + +/// A key that can be used to verify a request signature. +pub type VerificationKey = Key; + +/// A key that can be used to sign a request. +pub type SigningKey = Key; + +/// A key used for authorized fetch. +/// +/// It comes in several flavors: +/// +/// - `Key` (`K` = [`String`]): PEM-encoded, can be turned into a JSON object. +/// - [`VerificationKey`] (`K` = [`Public`]): used as an input in the request signature validation process. +/// - [`SigningKey`] (`K` = [`Private`]): used as an input in the generation of a signed request. +pub struct Key { + /// The `"id"` property of the public key, which should equal the `keyId` part of a signature. + pub id: String, + /// The `"owner"` property. + pub owner: String, + /// Maps to the `"publicKeyPem"` property of an actor's `"publicKey"` when (de)serializing, and when the + /// key is used for doing signatures. + pub inner: K, +} + +impl Key { + /// Tries to find the PEM-encoded public key from the result of fetching a key id. + pub fn from_json(json: Value) -> Option { + // First, we try the object itself. + json.as_object().and_then(Key::from_map).or_else(|| { + // Because of how mastodon deals with pubkey resolution, most implementations will serve the whole actor + // object instead of just the key, so we try that first, because it is the de facto standard. + json["publicKey"].as_object().and_then(Key::from_map) + }) + } + /// Construct + fn from_map(map: &Map) -> Option { + Some(Key { + id: map["id"].as_str().map(str::to_owned)?, + owner: map["owner"].as_str().map(str::to_owned)?, + inner: map["publicKeyPem"].as_str().map(str::to_owned)?, + }) + } + /// "Upgrade" a pem-encoded public key to a key that can actually be used for requests. + pub fn upgrade(self) -> Key { + let inner = Public::decode_pem(&self.inner); + Key { + id: self.id, + owner: self.owner, + inner, + } + } +} + +impl Key { + /// Encode a verification key so that it can be presented in a json. + pub fn serialize(self) -> Key { + let public_key_pem = self.inner.encode_pem(); + Key { + id: self.id, + owner: self.owner, + inner: public_key_pem, + } + } +} + +/// A key that can be used to generate signatures. +#[derive(Clone)] +pub struct Private(rsa::RsaPrivateKey); + +impl Private { + /// Generate a new keypair. + pub fn gen() -> (Private, Public) { + let mut rng = rand::thread_rng(); + let bits = 4096; + let private = RsaPrivateKey::new(&mut rng, bits).unwrap(); + let public = private.to_public_key(); + (Private(private), Public(public)) + } + /// Get the public counterpart to this key. + pub fn get_public(&self) -> Public { + Public(self.0.to_public_key()) + } + /// Load a private key from a file on disk. + pub fn load(path: impl AsRef) -> Private { + use rsa::pkcs8::DecodePrivateKey; + DecodePrivateKey::read_pkcs8_pem_file(path) + .map(Private) + .unwrap() + } + /// PEM-encode the key PKCS#8 style. + pub fn encode_pem(&self) -> String { + self.0 + .to_pkcs8_pem(LineEnding::default()) + .unwrap() + .to_string() + } + /// Decode the key from a PKCS#8 PEM-encoded string. + pub fn decode_pem(pkcs8_pem: &str) -> Private { + DecodePrivateKey::from_pkcs8_pem(&pkcs8_pem) + .map(Private) + .unwrap() + } +} + +/// A key that can be used to verify signatures. +#[derive(Clone)] +pub struct Public(rsa::RsaPublicKey); + +impl Public { + /// PEM-encode the public key in accordance with PKCS#8. + pub fn encode_pem(&self) -> String { + self.0 + .to_public_key_pem(LineEnding::default()) + .unwrap() + .to_string() + } + /// Decode a PKCS#8 PEM-encoded public key from a string. + pub fn decode_pem(pkcs8_pem: &str) -> Public { + DecodePublicKey::from_public_key_pem(&pkcs8_pem) + .map(Public) + .unwrap() + } +} + +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) + } + /// 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). + /// + /// This is required by most implementations when POSTing to an inbox. + pub fn sign_with_digest( + &self, + alg: Algorithm, + req: &mut Request, + ) -> Result, String> + where + T: AsRef<[u8]>, + { + todo!() + } +} + +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()) + } +} + +/// The algorithm to sign with. +/// +/// Your two options are: +/// +/// - [`hs2019`][HS2019], the *correct* option +/// - [`"rsa-sha256"`][RSA_SHA256], the most compatible option +#[derive(PartialEq, Debug, Clone, Copy)] +pub struct Algorithm(&'static str); + +/// `hs2019`, the only non-deprecated HTTP signatures algorithm. +pub const HS2019: Algorithm = Algorithm("hs2019"); +/// The HTTP signatures algorithm everyone uses, `rsa-sha256`. +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, +} + +impl Signature<'_> { + /// Attempt to extract a signature from a request. + pub fn derive(req: &Request) -> Result, String> { + parse(req) + } + /// Obtain the key id for the signature. + pub fn key_id(&self) -> &str { + &self.key_id + } + /// 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!() + } + /// If specified, get the expiry time. + pub fn expires(&self) -> Option> { + todo!() + } + /// Retrieve the algorithm used for the signature. + pub fn algorithm(&self) -> Algorithm { + self.alg + } + /// Attach `self` to `req` as the `signature` header. + pub fn attach_to(self, req: &mut Request) { + req.headers_mut().insert("signature", self.make_header()); + } + /// Turn the signature into an HTTP header value. + fn make_header(self) -> HeaderValue { + render(self) + } +} + +/// 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; + let Private(rsa) = key.clone(); + let key = SigningKey::::new(rsa); + let buf = key.sign(signing_string.as_bytes()).to_vec(); + 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::(); + + #[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") +} + +// 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"; + }; + + 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, + }) +} diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index 6657795..beb820e 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -54,7 +54,7 @@ pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result display_name, public_key: fetch::Key { owner: obj.id.0.into(), - key_id: key_id.into(), + id: key_id.into(), inner: key_pem, }, })) @@ -217,7 +217,7 @@ pub mod auth { let json = fetch::resolve(&self.signing_key(), uri).await.unwrap(); // TODO: make this parsing work better. Ok(fetch::Key { - key_id: json["publicKey"]["id"].as_str().unwrap().to_string().into(), + id: json["publicKey"]["id"].as_str().unwrap().to_string().into(), owner: json["publicKey"]["owner"] .as_str() .unwrap() @@ -233,7 +233,7 @@ pub mod auth { pub fn signing_key(&self) -> fetch::SigningKey { fetch::Key { - key_id: self.key_id.clone(), + id: self.key_id.clone(), owner: self.actor_id.clone(), inner: self.private.clone(), } diff --git a/lib/puppy/src/post.rs b/lib/puppy/src/post.rs index e44a820..b932d5e 100644 --- a/lib/puppy/src/post.rs +++ b/lib/puppy/src/post.rs @@ -38,7 +38,7 @@ impl Post { }; let (private, public) = cx.db.get_mixin_many::<(PrivateKey, PublicKey)>(author)?; let key = fetch::SigningKey { - key_id: public.key_id, + id: public.key_id, owner: author_id.0, inner: Private::decode_pem(&private.key_pem), };