[wip] YET MORE signatures cleanup and fixing
This commit is contained in:
parent
9eaad3d7bb
commit
fc4e4595c2
5 changed files with 180 additions and 230 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -485,11 +485,13 @@ dependencies = [
|
|||
"derive_more",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"pem",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"rsa",
|
||||
"serde_json",
|
||||
"sigh",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1128,6 +1130,16 @@ dependencies = [
|
|||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
|
|
|
@ -14,5 +14,7 @@ http = "*"
|
|||
chrono = "*"
|
||||
base64 = "*"
|
||||
rsa = { version = "*", features = ["sha2"] }
|
||||
spki = "*"
|
||||
http-body-util = "*"
|
||||
rand = "*"
|
||||
pem = "*"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![feature(iter_intersperse, yeet_expr, iterator_try_collect)]
|
||||
#![feature(iter_intersperse, yeet_expr, iterator_try_collect, try_blocks)]
|
||||
use chrono::Utc;
|
||||
use http_body_util::BodyExt as _;
|
||||
use reqwest::Body;
|
||||
|
@ -74,44 +74,51 @@ pub async fn deliver(key: &SigningKey, activity: Activity, inbox: &str) -> () {
|
|||
todo!()
|
||||
}
|
||||
|
||||
// Sun, 06 Nov 1994 08:49:37 GMT
|
||||
const RFC_822: &str = "%a, %d %b %Y %H:%M:%S GMT";
|
||||
pub async fn resolve(key: &SigningKey, target: &str) -> reqwest::Result<Value> {
|
||||
// TODO: make this retry with different signature options and remember what works for the
|
||||
// particular host.
|
||||
println!("[resolver]: resolving url {target} using key {}", key.id);
|
||||
|
||||
// Sun, 06 Nov 1994 08:49:37 GMT
|
||||
const RFC_822: &str = "%a, %d %b %Y %H:%M:%S GMT";
|
||||
|
||||
let uri = target.parse::<http::Uri>().unwrap();
|
||||
let host = uri.host().unwrap();
|
||||
let date = Utc::now().format(RFC_822).to_string();
|
||||
let mut req = http::Request::builder()
|
||||
.uri(target)
|
||||
.header("accept", "application/activity+json")
|
||||
.header("user-agent", "ActivityPuppy/0.0.0 (resolver)")
|
||||
.header("date", date)
|
||||
.header("host", host)
|
||||
// Empty body
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
// hs2019 works with masto, but (currently) our implementation is limited so that we cannot use those.
|
||||
key.sign(Options::LEGACY, &req)
|
||||
.expect("signing error")
|
||||
.commit(&mut req);
|
||||
println!("[resolver]: constructed request {req:#?}");
|
||||
|
||||
// hs2019 works with masto
|
||||
let sig = key.sign(Options::MODERN, &req).expect("signing error");
|
||||
|
||||
println!("[resolver]: constructed signature {sig:#?}");
|
||||
sig.commit(&mut req);
|
||||
|
||||
reqwest::Client::new()
|
||||
.execute(req.map(|_| Body::default()).try_into().unwrap())
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn forward(key: &SigningKey, target: &str) -> reqwest::Result<http::Response<String>> {
|
||||
// Sun, 06 Nov 1994 08:49:37 GMT
|
||||
const RFC_822: &str = "%a, %d %b %Y %H:%M:%S GMT";
|
||||
|
||||
let date = Utc::now().format(RFC_822).to_string();
|
||||
let uri = target.parse::<http::Uri>().unwrap();
|
||||
let host = uri.host().unwrap();
|
||||
let mut req = http::Request::get(target)
|
||||
.header("accept", "application/activity+json")
|
||||
.header("user-agent", "ActivityPuppy/0.0.0 (resolver)")
|
||||
.header("date", date)
|
||||
.header("host", host)
|
||||
// Empty body
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
//! - <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 std::path::Path;
|
||||
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use http::{HeaderValue, Method, Request};
|
||||
use http::{HeaderValue, Request};
|
||||
use rsa::{
|
||||
pkcs1v15::VerifyingKey,
|
||||
pkcs8::{
|
||||
DecodePrivateKey, DecodePublicKey, EncodePrivateKey as _, EncodePublicKey as _, LineEnding,
|
||||
},
|
||||
|
@ -37,12 +36,9 @@ use rsa::{
|
|||
RsaPrivateKey,
|
||||
};
|
||||
|
||||
use base64::prelude::*;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::signatures::new::decode;
|
||||
|
||||
use self::new::{format_target, IR};
|
||||
use self::new::{decode, encode, sha256, IR};
|
||||
|
||||
/// A key that can be used to verify a request signature.
|
||||
pub type VerificationKey = Key<Public>;
|
||||
|
@ -74,15 +70,15 @@ impl Key {
|
|||
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)
|
||||
json.get("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)?,
|
||||
id: map.get("id")?.as_str().map(str::to_owned)?,
|
||||
owner: map.get("owner")?.as_str().map(str::to_owned)?,
|
||||
inner: map.get("publicKeyPem")?.as_str().map(str::to_owned)?,
|
||||
})
|
||||
}
|
||||
/// "Upgrade" a pem-encoded public key to a key that can actually be used for requests.
|
||||
|
@ -103,12 +99,22 @@ pub struct Private(rsa::RsaPrivateKey);
|
|||
impl Private {
|
||||
/// Generate a new keypair.
|
||||
pub fn gen() -> (Private, Public) {
|
||||
println!("[!] gen");
|
||||
let mut rng = rand::thread_rng();
|
||||
let bits = 4096;
|
||||
let bits = 512;
|
||||
let private = RsaPrivateKey::new(&mut rng, bits).unwrap();
|
||||
let public = private.to_public_key();
|
||||
(Private(private), Public(public))
|
||||
}
|
||||
pub fn tee(path: impl AsRef<Path>) -> (Private, Public) {
|
||||
println!("[!] tee");
|
||||
let (a, b) = Private::gen();
|
||||
println!("[!] keygen complete");
|
||||
a.0.write_pkcs8_pem_file(path, LineEnding::default())
|
||||
.unwrap();
|
||||
println!("[!] write finished");
|
||||
(a, b)
|
||||
}
|
||||
/// Get the public counterpart to this key.
|
||||
pub fn get_public(&self) -> Public {
|
||||
Public(self.0.to_public_key())
|
||||
|
@ -149,19 +155,17 @@ impl Public {
|
|||
}
|
||||
/// 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)
|
||||
let doc = pem::parse(pkcs8_pem).unwrap();
|
||||
<rsa::RsaPublicKey as DecodePublicKey>::from_public_key_der(doc.contents())
|
||||
.map(Public)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SigningKey {
|
||||
/// Create a signature for `req` using the given algorithm, for `GET` requests.
|
||||
pub fn sign(&self, opt: Options, req: &Request<()>) -> Result<Signature, String> {
|
||||
let target = format_target(&req, opt);
|
||||
IR::partial(&req, &target, opt)?
|
||||
.signed(self)?
|
||||
.to_signature()
|
||||
/// Create a signature for `req` using the given options.
|
||||
pub fn sign<T>(&self, opt: Options, req: &Request<T>) -> Result<Signature, String> {
|
||||
IR::partial(&req, opt, |ir| ir.signed(self)?.to_signature())
|
||||
}
|
||||
/// Create a signature for `req` using the given algorithm, and calculate and attach the `digest` header to
|
||||
/// the request (if it doesn't already have one).
|
||||
|
@ -175,7 +179,12 @@ impl SigningKey {
|
|||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
todo!()
|
||||
// Calculate and insert digest if it isn't there yet, otherwise do nothing.
|
||||
let digest = encode(sha256(req.body()));
|
||||
req.headers_mut()
|
||||
.entry("digest")
|
||||
.or_insert_with(|| digest.try_into().unwrap());
|
||||
self.sign(opt, req)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,6 +236,7 @@ pub const HS2019: Algorithm = Algorithm("hs2019");
|
|||
pub const RSA_SHA256: Algorithm = Algorithm("rsa-sha256");
|
||||
|
||||
/// A signature derived from an [`http::Request`].
|
||||
#[derive(Debug)]
|
||||
pub struct Signature {
|
||||
key_id: String,
|
||||
components: Vec<(String, String)>,
|
||||
|
@ -240,9 +250,7 @@ pub struct Signature {
|
|||
impl Signature {
|
||||
/// Attempt to extract a signature from a request.
|
||||
pub fn derive<T>(req: &Request<T>) -> Result<Signature, String> {
|
||||
let target = format_target(&req, Options::MODERN);
|
||||
let ir = IR::<&str>::from_request(req, target.as_ref())?;
|
||||
ir.to_signature()
|
||||
new::with_ir(req, Options::MODERN, |ir| ir.to_signature())
|
||||
}
|
||||
/// Obtain the key id for the signature.
|
||||
pub fn key_id(&self) -> &str {
|
||||
|
@ -363,14 +371,11 @@ impl Default for Options {
|
|||
|
||||
mod new {
|
||||
use base64::prelude::*;
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use http::{HeaderName, HeaderValue, Method, Request};
|
||||
use chrono::{DateTime, Utc};
|
||||
use http::{Method, Request};
|
||||
use rsa::sha2::{Digest, Sha256};
|
||||
|
||||
use super::{
|
||||
sign_rsa_sha256, Algorithm, Options, Signature, SigningKey, EXPIRY_WINDOW, HS2019,
|
||||
RSA_SHA256,
|
||||
};
|
||||
use super::{sign_rsa_sha256, Options, Signature, SigningKey, EXPIRY_WINDOW, HS2019, RSA_SHA256};
|
||||
|
||||
/// Calculate the SHA256 hash of something.
|
||||
pub fn sha256(buf: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
|
@ -387,143 +392,6 @@ mod new {
|
|||
BASE64_STANDARD.decode(buf).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Generate signature headers for a request with a body.
|
||||
fn headers_with_digest<T>(
|
||||
req: &Request<T>,
|
||||
key: &SigningKey,
|
||||
opt: Options,
|
||||
) -> Result<Vec<(HeaderName, HeaderValue)>, String>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let digest = {
|
||||
let encoded = encode(sha256(req.body()));
|
||||
format!("sha256={encoded}")
|
||||
};
|
||||
gen_headers(req, key, opt, vec![("digest", digest)], true)
|
||||
}
|
||||
|
||||
fn gen_headers<T>(
|
||||
req: &Request<T>,
|
||||
key: &SigningKey,
|
||||
opt: Options,
|
||||
extra: Vec<(&str, String)>,
|
||||
use_digest: bool,
|
||||
) -> Result<Vec<(HeaderName, HeaderValue)>, String> {
|
||||
// We need to add a date header is one doesn't already exist.
|
||||
const RFC_822: &str = "%a, %d %b %Y %H:%M:%S GMT";
|
||||
let date = Utc::now().format(RFC_822).to_string();
|
||||
let date = (!(opt.use_created || req.headers().contains_key("date")))
|
||||
.then_some(("date", date.as_str()));
|
||||
|
||||
// QUIRK: some older mastodon versions don't sign the query params.
|
||||
let path = if opt.strip_query {
|
||||
req.uri().path()
|
||||
} else {
|
||||
req.uri()
|
||||
.path_and_query()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(req.uri().path())
|
||||
};
|
||||
|
||||
// Preprocess the headers into the format expected by the signing functions.
|
||||
// NOTE: only the first value of each key is ever used.
|
||||
let header_map: Vec<(&str, &str)> = make_header_map(&req)
|
||||
.into_iter()
|
||||
// The "uncommitted" headers need to be added as well
|
||||
.chain(extra.iter().map(|(k, v)| (*k, v.as_str())))
|
||||
// And at last, our date header too, if we're adding one.
|
||||
.chain(date)
|
||||
.collect();
|
||||
|
||||
let signature_header = make_signature_header(
|
||||
key,
|
||||
opt.algorithm,
|
||||
// We only use the expiry deadline if we're using the `(created)` and `(expires)` inputs
|
||||
// for signature generation.
|
||||
opt.use_created.then_some(opt.expires_after),
|
||||
use_digest,
|
||||
&header_map,
|
||||
req.method(),
|
||||
path,
|
||||
)?;
|
||||
|
||||
extra
|
||||
.into_iter()
|
||||
// Don't forget the signature :)
|
||||
.chain([("signature", signature_header)])
|
||||
// Turn all of the strings into header names/values.
|
||||
.map(|(name, data)| -> Result<_, http::Error> {
|
||||
let name = name.try_into()?;
|
||||
let data = data.try_into()?;
|
||||
Ok((name, data))
|
||||
})
|
||||
.try_collect()
|
||||
.map_err(|e| format!("{e:?}"))
|
||||
}
|
||||
|
||||
fn make_signature_header(
|
||||
key: &SigningKey,
|
||||
alg: Algorithm,
|
||||
expires_after: Option<TimeDelta>,
|
||||
use_digest: bool,
|
||||
headers: &[(&str, &str)],
|
||||
method: &Method,
|
||||
path: &str,
|
||||
) -> Result<String, String> {
|
||||
// List of input names that we want. Pseudo-headers are ordered before normal headers.
|
||||
let needed = ["(request-target)"]
|
||||
.into_iter()
|
||||
.chain(if expires_after.is_some() {
|
||||
vec!["(created)", "(expired)"]
|
||||
} else {
|
||||
vec!["date"]
|
||||
})
|
||||
.chain(use_digest.then_some("digest"))
|
||||
.chain(["host"]);
|
||||
|
||||
let created = Utc::now();
|
||||
let expires = created + expires_after.unwrap_or(EXPIRY_WINDOW);
|
||||
|
||||
let target_str = format!("{} {path}", method.as_str().to_ascii_lowercase());
|
||||
let created_str = created.timestamp().to_string();
|
||||
let expires_str = expires.timestamp().to_string();
|
||||
|
||||
// Lookup table for pseudo-headers.
|
||||
let pseudo = [
|
||||
("(request-target)", target_str.as_str()),
|
||||
("(created)", created_str.as_str()),
|
||||
("(expired)", expires_str.as_str()),
|
||||
];
|
||||
|
||||
let inputs = collect_inputs(needed, &headers, &pseudo)?;
|
||||
let signing_str = make_signing_string(&inputs);
|
||||
|
||||
let signature = sign_rsa_sha256(&signing_str, &key.inner)?;
|
||||
let encoded = BASE64_STANDARD.encode(signature);
|
||||
|
||||
Ok(format_header(&inputs, &key.id, alg.0, &encoded))
|
||||
}
|
||||
|
||||
fn collect_inputs<'a>(
|
||||
needed: impl IntoIterator<Item = &'a str> + 'a,
|
||||
headers: &[(&'a str, &'a str)],
|
||||
pseudo: &[(&'a str, &'a str)],
|
||||
) -> Result<Vec<(&'a str, &'a str)>, String> {
|
||||
// Quick utility function to get stuff from an association list.
|
||||
fn get<'x>(map: &[(&str, &'x str)], key: &str) -> Option<&'x str> {
|
||||
map.iter().find_map(|(k, v)| (*k == key).then_some(*v))
|
||||
}
|
||||
|
||||
let assoc = |k| {
|
||||
get(&headers, k)
|
||||
.or_else(|| get(&pseudo, k))
|
||||
.ok_or_else(|| format!("Missing (pseudo)header `{k}`"))
|
||||
.map(|v| (k, v))
|
||||
};
|
||||
needed.into_iter().map(assoc).try_collect()
|
||||
}
|
||||
|
||||
pub struct IR<'s, S = &'s str> {
|
||||
target: &'s str,
|
||||
inputs: Vec<(&'s str, &'s str)>,
|
||||
|
@ -532,6 +400,21 @@ mod new {
|
|||
sig: S,
|
||||
}
|
||||
|
||||
/// Allocates a new [`IR`] for doing signature operations with.
|
||||
pub fn with_ir<T, U>(
|
||||
req: &Request<T>,
|
||||
opt: Options,
|
||||
f: impl FnOnce(IR<'_>) -> Result<U, String>,
|
||||
) -> Result<U, String> {
|
||||
let target = &format_target(&req, opt);
|
||||
let map = make_header_map(&req);
|
||||
let Some(header) = get(&map, "signature") else {
|
||||
do yeet "Missing required `signature` header";
|
||||
};
|
||||
let (inputs, key, alg, sig) = parse_header(header, &target, &map)?;
|
||||
f(IR { target, inputs, key, alg, sig })
|
||||
}
|
||||
|
||||
impl<S> IR<'_, S> {
|
||||
/// Create an HTTP header from the IR.
|
||||
pub fn to_header(&self) -> String
|
||||
|
@ -540,22 +423,31 @@ mod new {
|
|||
{
|
||||
format_header(&self.inputs, self.key.as_ref(), self.alg, self.sig.as_ref())
|
||||
}
|
||||
/// Given a *correct* target string, extract a signature header from a request.
|
||||
pub fn from_request<'r, T>(req: &'r Request<T>, target: &'r str) -> Result<IR<'r>, String> {
|
||||
let map = make_header_map(&req);
|
||||
let Some(header) = get(&map, "signature") else {
|
||||
do yeet "Missing required `signature` header";
|
||||
};
|
||||
let (inputs, key, alg, sig) = parse_header(header, &target, &map)?;
|
||||
Ok(IR { target, inputs, key, alg, sig })
|
||||
}
|
||||
/// Validate and upgrade the IR to a structured signature.
|
||||
pub fn to_signature(&self) -> Result<Signature, String>
|
||||
where
|
||||
S: ToString,
|
||||
{
|
||||
let created = Utc::now(); // TODO
|
||||
let expires = Utc::now(); // TODO
|
||||
let times: Result<_, String> = try {
|
||||
let date = get(&self.inputs, "date").map(from_rfc822).transpose()?;
|
||||
let created = get(&self.inputs, "(created)").map(from_secs).transpose()?;
|
||||
let expires = get(&self.inputs, "(expires)").map(from_secs).transpose()?;
|
||||
(date, created, expires)
|
||||
};
|
||||
let (date, created, expires) =
|
||||
times.map_err(|e: String| format!("Failed to parse time: {e}"))?;
|
||||
|
||||
let (created, expires) = match (created, expires) {
|
||||
(Some(created), None) => (created, created + EXPIRY_WINDOW),
|
||||
(Some(created), Some(expires)) => (created, expires),
|
||||
(None, _) => {
|
||||
let Some(date) = date else {
|
||||
do yeet "Cannot determine validity window";
|
||||
};
|
||||
(date, date + EXPIRY_WINDOW)
|
||||
}
|
||||
};
|
||||
|
||||
let algorithm = match self.alg {
|
||||
"rsa-sha256" => RSA_SHA256,
|
||||
"hs2019" => HS2019,
|
||||
|
@ -599,15 +491,36 @@ mod new {
|
|||
|
||||
impl<'s> IR<'s, ()> {
|
||||
/// Create a partial, unsigned IR.
|
||||
pub fn partial<'r, T>(
|
||||
pub fn partial<'r, T, U>(
|
||||
req: &'r Request<T>,
|
||||
target: &'r str,
|
||||
opt: Options,
|
||||
) -> Result<IR<'r, ()>, String> {
|
||||
f: impl FnOnce(IR<'_, ()>) -> Result<U, String>,
|
||||
) -> Result<U, String> {
|
||||
let map = make_header_map(req);
|
||||
let digest = req.method() == Method::POST;
|
||||
let inputs = compute_inputs(&map, target, opt, digest)?;
|
||||
Ok(IR {
|
||||
|
||||
let expires_after = opt.use_created.then_some(opt.expires_after);
|
||||
let created = Utc::now();
|
||||
let expires = created + expires_after.unwrap_or(EXPIRY_WINDOW);
|
||||
|
||||
let target = &format_target(&req, opt);
|
||||
let created = created.timestamp().to_string();
|
||||
let expires = expires.timestamp().to_string();
|
||||
|
||||
// Association list mapping pseudo headers names to concrete values.
|
||||
#[rustfmt::skip]
|
||||
let pseudo = &[
|
||||
("(request-target)", target.as_str()),
|
||||
("(created)", created.as_str()),
|
||||
("(expired)", expires.as_str()),
|
||||
];
|
||||
|
||||
let inputs = match compute_inputs(&map, pseudo, opt, digest) {
|
||||
Err(error) => do yeet format!("computing inputs: {error}"),
|
||||
Ok(inputs) => inputs,
|
||||
};
|
||||
|
||||
f(IR {
|
||||
target,
|
||||
inputs,
|
||||
alg: opt.algorithm.0,
|
||||
|
@ -618,7 +531,10 @@ mod new {
|
|||
/// Sign a partially constructed IR to make it actually useful.
|
||||
pub fn signed(self, key: &'s SigningKey) -> Result<IR<'s, String>, String> {
|
||||
let sig_str = self.to_signing_str();
|
||||
let signature = sign_rsa_sha256(&sig_str, &key.inner).map(encode)?;
|
||||
let signature = match sign_rsa_sha256(&sig_str, &key.inner).map(encode) {
|
||||
Err(error) => do yeet format!("RSA error: {error}"),
|
||||
Ok(signature) => signature,
|
||||
};
|
||||
Ok(IR {
|
||||
target: self.target,
|
||||
inputs: self.inputs,
|
||||
|
@ -633,15 +549,14 @@ mod new {
|
|||
/// turned into the signing string.
|
||||
fn compute_inputs<'a>(
|
||||
headers: &[(&'a str, &'a str)],
|
||||
target: &'a str,
|
||||
pseudo: &[(&'a str, &'a str)],
|
||||
opt: Options,
|
||||
use_digest: bool,
|
||||
) -> Result<Vec<(&'a str, &'a str)>, String> {
|
||||
let expires_after = opt.use_created.then_some(opt.expires_after);
|
||||
// List of input names that we want. Pseudo-headers are ordered before normal headers.
|
||||
let needed = ["(request-target)"]
|
||||
.into_iter()
|
||||
.chain(if expires_after.is_some() {
|
||||
.chain(if opt.use_created {
|
||||
vec!["(created)", "(expired)"]
|
||||
} else {
|
||||
vec!["date"]
|
||||
|
@ -649,25 +564,18 @@ mod new {
|
|||
.chain(use_digest.then_some("digest"))
|
||||
.chain(["host"]);
|
||||
|
||||
let created = Utc::now();
|
||||
let expires = created + expires_after.unwrap_or(EXPIRY_WINDOW);
|
||||
let assoc = |k| {
|
||||
get(headers, k)
|
||||
.or_else(|| get(&pseudo, k))
|
||||
.ok_or_else(|| format!("Missing (pseudo)header `{k}`"))
|
||||
.map(|v| (k, v))
|
||||
};
|
||||
|
||||
let created_str = created.timestamp().to_string();
|
||||
let expires_str = expires.timestamp().to_string();
|
||||
|
||||
// Lookup table for pseudo-headers.
|
||||
// TODO: currently, `(created)` and `(expires)` can't be used because they require string alloc.
|
||||
let pseudo = [
|
||||
("(request-target)", target),
|
||||
// ("(created)", created_str.as_str()), FIXME
|
||||
// ("(expired)", expires_str.as_str()), FIXME
|
||||
];
|
||||
|
||||
collect_inputs(needed, &headers, &pseudo)
|
||||
needed.map(assoc).try_collect()
|
||||
}
|
||||
|
||||
/// Allocate a `(request-target)` buffer.
|
||||
pub fn format_target<T>(req: &Request<T>, opt: Options) -> String {
|
||||
fn format_target<T>(req: &Request<T>, opt: Options) -> String {
|
||||
let path = if opt.strip_query {
|
||||
req.uri().path()
|
||||
} else {
|
||||
|
@ -726,13 +634,18 @@ mod new {
|
|||
// Parse the top-level table.
|
||||
let table: Vec<(&str, &str)> = header
|
||||
// Split into entries
|
||||
.split(", ")
|
||||
.split(",")
|
||||
// Split entries into key-value pairs
|
||||
.filter_map(|pair| pair.split_once('='))
|
||||
.filter_map(|pair| {
|
||||
pair.trim_end_matches(' ') // QUIRK: akkoma does not put a space between entries
|
||||
.split_once('=')
|
||||
})
|
||||
// Undo quoting of the values
|
||||
.filter_map(|(k, v)| v.strip_prefix('"')?.strip_suffix('"').map(|v| (k, v)))
|
||||
.collect();
|
||||
|
||||
let table = dbg!(table);
|
||||
|
||||
let Some(headers) = get(&table, "headers") else {
|
||||
do yeet "Missing `headers` field";
|
||||
};
|
||||
|
@ -795,4 +708,18 @@ mod new {
|
|||
.intersperse("\n".to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn from_secs(s: &str) -> Result<DateTime<Utc>, String> {
|
||||
s.parse::<i64>().map_err(|e| e.to_string()).and_then(|t| {
|
||||
let Some(time) = DateTime::from_timestamp(t, 0) else {
|
||||
do yeet "Timestamp out of range";
|
||||
};
|
||||
Ok(time)
|
||||
})
|
||||
}
|
||||
fn from_rfc822(s: &str) -> Result<DateTime<Utc>, String> {
|
||||
DateTime::parse_from_rfc2822(s)
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|time| time.to_utc())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -215,20 +215,7 @@ pub mod auth {
|
|||
impl Verifier {
|
||||
pub async fn get_public_key(&self, uri: &str) -> Result<fetch::Key, String> {
|
||||
let json = fetch::resolve(&self.signing_key(), uri).await.unwrap();
|
||||
// TODO: make this parsing work better.
|
||||
Ok(fetch::Key {
|
||||
id: json["publicKey"]["id"].as_str().unwrap().to_string().into(),
|
||||
owner: json["publicKey"]["owner"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.into(),
|
||||
inner: json["publicKey"]["publicKeyPem"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.into(),
|
||||
})
|
||||
Ok(fetch::Key::from_json(dbg!(json)).unwrap())
|
||||
}
|
||||
|
||||
pub fn signing_key(&self) -> fetch::SigningKey {
|
||||
|
@ -257,8 +244,10 @@ pub mod auth {
|
|||
}
|
||||
|
||||
pub fn load(cfg: &Config) -> Verifier {
|
||||
println!("[*] loading private key");
|
||||
let domain = &cfg.ap_domain;
|
||||
let private = Private::load(".state/fetcher.pem");
|
||||
println!("* done loading private key");
|
||||
Verifier {
|
||||
actor_id: format!("https://{domain}/s/request-verifier"),
|
||||
key_id: format!("https://{domain}/s/request-verifier#sig-key"),
|
||||
|
@ -282,23 +271,32 @@ pub mod auth {
|
|||
|
||||
/// Check the signature for a request.
|
||||
pub async fn verify_signature(
|
||||
req: &fetch::http::Request<impl AsRef<[u8]>>,
|
||||
req: &fetch::http::Request<impl AsRef<[u8]> + std::fmt::Debug>,
|
||||
cfg: &Config,
|
||||
) -> Result<Option<Signer>, SigError> {
|
||||
println!(">>> starting signature verification for {req:#?}");
|
||||
|
||||
if req.uri().path() == "/s/request-verifier" {
|
||||
// Allow access to the request verifier actor without checking the signature.
|
||||
return Ok(None);
|
||||
}
|
||||
println!(">>> not going for the verifier!");
|
||||
|
||||
if req.headers().get("signature").is_none() {
|
||||
// Request is not signed!
|
||||
return Ok(None);
|
||||
};
|
||||
println!(">>> has signature");
|
||||
|
||||
// Parse the signature.
|
||||
let sig = match Signature::derive(&req) {
|
||||
Ok(signature) => signature,
|
||||
Err(error) => return Err(SigError::ParseSignature { error }),
|
||||
Err(error) => {
|
||||
println!(">>> signature could not be parsed: {error}");
|
||||
return Err(SigError::ParseSignature { error });
|
||||
}
|
||||
};
|
||||
println!(">>> signature is syntatically valid");
|
||||
|
||||
// Fetch the public key using the verifier private key.
|
||||
let verifier = Verifier::load(cfg);
|
||||
|
@ -307,12 +305,16 @@ pub mod auth {
|
|||
keyid: sig.key_id().to_string(),
|
||||
});
|
||||
};
|
||||
println!(">>> public key fetched");
|
||||
|
||||
// Verify the signature header on the request.
|
||||
let public_key = public_key.upgrade();
|
||||
println!(">>> upgraded");
|
||||
if let Err(error) = public_key.verify(&sig) {
|
||||
println!(">>> rejected");
|
||||
Err(SigError::VerificationFailed { error })
|
||||
} else {
|
||||
println!(">>> request verified");
|
||||
Ok(Some(Signer { ap_id: public_key.owner.into() }))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue