Split fetch::signatures into its own file

This commit is contained in:
Riley Apeldoorn 2024-04-29 00:09:17 +02:00
parent 37acb67aa5
commit c784966d20
5 changed files with 465 additions and 465 deletions

View file

@ -27,13 +27,13 @@ pub mod ap {
}; };
// Look up the actor's key in the store (which is accessible through the puppy context). // 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 actor = Actor::by_username(&cx, user)?.unwrap();
let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) = let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) =
cx.store().get_mixin_many(actor.key)?; cx.store().get_mixin_many(actor.key)?;
let Id(owner) = cx.store().get_alias(actor.key)?.unwrap(); let Id(owner) = cx.store().get_alias(actor.key)?.unwrap();
let inner = Private::decode_pem(&key_pem); let inner = Private::decode_pem(&key_pem);
SigningKey { key_id, owner, inner } SigningKey { id: key_id, owner, inner }
}) })
.unwrap(); .unwrap();

View file

@ -1,8 +1,6 @@
#![feature(iter_intersperse, yeet_expr)] #![feature(iter_intersperse, yeet_expr)]
use chrono::Utc; use chrono::Utc;
use derive_more::{Display, From, Into};
use http_body_util::BodyExt as _; use http_body_util::BodyExt as _;
use signatures::{Private, Public};
use reqwest::Body; use reqwest::Body;
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -10,11 +8,14 @@ use crate::signatures::HS2019;
pub use http; pub use http;
pub use signatures::{Key, SigningKey, VerificationKey};
pub mod signatures;
pub enum Object { pub enum Object {
Activity(Activity), Activity(Activity),
Actor(Actor), Actor(Actor),
Object { Object {
id: Id, id: String,
kind: String, kind: String,
content: Option<String>, content: Option<String>,
summary: Option<String>, summary: Option<String>,
@ -22,7 +23,7 @@ pub enum Object {
} }
impl Object { impl Object {
pub fn id(&self) -> &Id { pub fn id(&self) -> &str {
match self { match self {
Object::Activity(a) => &a.id, Object::Activity(a) => &a.id,
Object::Actor(a) => &a.id, Object::Actor(a) => &a.id,
@ -44,8 +45,8 @@ impl Object {
} }
pub struct Activity<T = String> { pub struct Activity<T = String> {
pub id: Id, pub id: String,
pub actor: Id, pub actor: String,
pub object: Box<Object>, pub object: Box<Object>,
pub kind: T, pub kind: T,
} }
@ -134,9 +135,9 @@ pub async fn forward(key: &SigningKey, target: &str) -> reqwest::Result<http::Re
/// An actor is an entity capable of producing Takes. /// An actor is an entity capable of producing Takes.
pub struct Actor { pub struct Actor {
/// The URL pointing to this object. /// The URL pointing to this object.
pub id: Id, pub id: String,
/// Where others should send activities. /// Where others should send activities.
pub inbox: Id, pub inbox: String,
/// Note: this maps to the `preferredUsername` property. /// Note: this maps to the `preferredUsername` property.
pub account_name: String, pub account_name: String,
/// Note: this maps to the `name` property. /// Note: this maps to the `name` property.
@ -160,460 +161,10 @@ impl Actor {
"name": self.display_name, "name": self.display_name,
"type": "Person", "type": "Person",
"publicKey": { "publicKey": {
"id": self.public_key.key_id, "id": self.public_key.id,
"publicKeyPem": self.public_key.inner, "publicKeyPem": self.public_key.inner,
"owner": self.id.to_string(), "owner": self.id.to_string(),
} }
}) })
} }
} }
/// A key that can be used to verify a request signature.
pub type VerificationKey = Key<Public>;
/// A key that can be used to sign a request.
pub type SigningKey = Key<Private>;
/// 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<K = String> {
/// 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<Key> {
// 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<String, Value>) -> Option<Key> {
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<Public> {
let inner = Public::decode_pem(&self.inner);
Key {
key_id: self.key_id,
owner: self.owner,
inner,
}
}
}
impl Key<Public> {
/// 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:
//!
//! - <https://swicg.github.io/activitypub-http-signature>
//! - <https://datatracker.ietf.org/doc/html/draft-cavage-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<Path>) -> 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<Signature<'_>, 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<T>(
&self,
alg: Algorithm,
req: &mut Request<T>,
) -> Result<Signature<'_>, 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::<Sha256>::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<Component>,
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<Utc> {
todo!()
}
/// If specified, get the expiry time.
pub fn expires(&self) -> Option<DateTime<Utc>> {
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<T>(req: &Request<T>) -> Result<Signature<'_>, String> {
parse(req)
}
/// Attach `self` to `req` as the `signature` header.
pub fn attach_to<T>(self, req: &mut Request<T>) {
req.headers_mut().insert("signature", render(self));
}
}
/// 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.
fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result<Vec<u8>, String> {
let Private(rsa) = key.clone();
let key = SigningKey::<sha2::Sha256>::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::<String>();
#[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::<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,
})
}
}

449
lib/fetch/src/signatures.rs Normal file
View file

@ -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:
//!
//! - <https://swicg.github.io/activitypub-http-signature>
//! - <https://datatracker.ietf.org/doc/html/draft-cavage-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<Public>;
/// A key that can be used to sign a request.
pub type SigningKey = Key<Private>;
/// 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<K = String> {
/// 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<Key> {
// 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<String, Value>) -> Option<Key> {
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<Public> {
let inner = Public::decode_pem(&self.inner);
Key {
id: self.id,
owner: self.owner,
inner,
}
}
}
impl Key<Public> {
/// 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<Path>) -> 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<Signature<'_>, 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<T>(
&self,
alg: Algorithm,
req: &mut Request<T>,
) -> Result<Signature<'_>, 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::<Sha256>::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<Component>,
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<T>(req: &Request<T>) -> Result<Signature<'_>, 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<Utc> {
todo!()
}
/// If specified, get the expiry time.
pub fn expires(&self) -> Option<DateTime<Utc>> {
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<T>(self, req: &mut Request<T>) {
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<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.
fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result<Vec<u8>, String> {
use rsa::pkcs1v15::SigningKey;
let Private(rsa) = key.clone();
let key = SigningKey::<Sha256>::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::<String>();
#[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::<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,
})
}

View file

@ -54,7 +54,7 @@ pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result<fetch::Object>
display_name, display_name,
public_key: fetch::Key { public_key: fetch::Key {
owner: obj.id.0.into(), owner: obj.id.0.into(),
key_id: key_id.into(), id: key_id.into(),
inner: key_pem, inner: key_pem,
}, },
})) }))
@ -217,7 +217,7 @@ pub mod auth {
let json = fetch::resolve(&self.signing_key(), uri).await.unwrap(); let json = fetch::resolve(&self.signing_key(), uri).await.unwrap();
// TODO: make this parsing work better. // TODO: make this parsing work better.
Ok(fetch::Key { 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"] owner: json["publicKey"]["owner"]
.as_str() .as_str()
.unwrap() .unwrap()
@ -233,7 +233,7 @@ pub mod auth {
pub fn signing_key(&self) -> fetch::SigningKey { pub fn signing_key(&self) -> fetch::SigningKey {
fetch::Key { fetch::Key {
key_id: self.key_id.clone(), id: self.key_id.clone(),
owner: self.actor_id.clone(), owner: self.actor_id.clone(),
inner: self.private.clone(), inner: self.private.clone(),
} }

View file

@ -38,7 +38,7 @@ impl Post {
}; };
let (private, public) = cx.db.get_mixin_many::<(PrivateKey, PublicKey)>(author)?; let (private, public) = cx.db.get_mixin_many::<(PrivateKey, PublicKey)>(author)?;
let key = fetch::SigningKey { let key = fetch::SigningKey {
key_id: public.key_id, id: public.key_id,
owner: author_id.0, owner: author_id.0,
inner: Private::decode_pem(&private.key_pem), inner: Private::decode_pem(&private.key_pem),
}; };