Split fetch::signatures
into its own file
This commit is contained in:
parent
37acb67aa5
commit
c784966d20
5 changed files with 465 additions and 465 deletions
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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
449
lib/fetch/src/signatures.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue