Implemented outbound request signing

This commit is contained in:
Natty 2024-04-09 16:49:00 +02:00
parent aefef079a7
commit ce46782318
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
8 changed files with 785 additions and 14 deletions

1
Cargo.lock generated
View File

@ -1726,6 +1726,7 @@ dependencies = [
"futures-util",
"headers",
"http",
"httpdate",
"hyper",
"indexmap",
"magnetar_common",

View File

@ -42,6 +42,7 @@ futures-core = "0.3"
futures-util = "0.3"
headers = "0.4"
http = "1.0"
httpdate = "1"
hyper = "1.1"
idna = "0.5"
indexmap = "2.2"

View File

@ -27,6 +27,7 @@ base64 = { workspace = true }
url = { workspace = true, features = ["serde"] }
chrono = { workspace = true, features = ["serde"] }
httpdate = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
@ -39,7 +40,12 @@ hyper = { workspace = true, features = ["full"] }
percent-encoding = { workspace = true }
reqwest = { workspace = true, features = ["stream"] }
ed25519-dalek = { workspace = true, features = ["pem", "pkcs8", "signature"] }
ed25519-dalek = { workspace = true, features = [
"pem",
"pkcs8",
"signature",
"digest",
] }
rsa = { workspace = true, features = ["sha2"] }
sha2 = { workspace = true }

View File

@ -0,0 +1,450 @@
use std::{fmt::Display, sync::Arc};
use chrono::Utc;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use indexmap::IndexSet;
use magnetar_core::web_model::content_type::ContentActivityStreams;
use serde_json::Value;
use sha2::Digest;
use thiserror::Error;
use url::Url;
use crate::{
client::federation_client::{FederationClient, FederationClientError},
crypto::{ApSigningError, ApSigningKey, SigningAlgorithm},
ApClientService, ApSignature, ApSigningField, ApSigningHeaders, SigningInput, SigningParts,
};
pub struct ApClientServiceDefaultProvider {
client: Arc<FederationClient>,
}
impl Display for ApSignature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "keyId=\"{}\"", self.key_id)?;
if let Some(ref algorithm) = self.algorithm {
write!(f, ",algorithm=\"{}\"", algorithm)?;
}
if let Some(ref created) = self.created {
write!(f, ",created={}", created.timestamp())?;
}
if let Some(ref expires) = self.expires {
write!(f, ",expires={}", expires.timestamp())?;
}
if let Some(ref headers) = self.headers {
write!(f, ",headers=\"{}\"", headers)?;
}
write!(f, ",signature=\"{}\"", self.signature)?;
Ok(())
}
}
impl Display for ApSigningHeaders {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.0
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>()
.join(" ")
)
}
}
#[derive(Debug, Error)]
pub enum ApClientError {
#[error("Federation client error: {0}")]
FederationClientError(#[from] FederationClientError),
#[error("Signing error: {0}")]
SigningError(#[from] ApSigningError),
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Failed to serialize JSON: {0}")]
SerializerError(#[from] serde_json::Error),
#[error("Invalid header value: {0}")]
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
}
trait CreateField {
fn create_field(&self) -> Option<(ApSigningField, String)>;
}
#[derive(Debug)]
struct RequestTarget<'a> {
url: &'a Url,
method: Method,
}
impl CreateField for RequestTarget<'_> {
fn create_field(&self) -> Option<(ApSigningField, String)> {
Some((
ApSigningField::PseudoRequestTarget,
format!(
"{} {}",
self.method.as_str().to_lowercase(),
self.url.path()
),
))
}
}
#[derive(Debug)]
struct HostPseudoHeader<'a>(&'a Url);
impl CreateField for HostPseudoHeader<'_> {
fn create_field(&self) -> Option<(ApSigningField, String)> {
Some((ApSigningField::Host, self.0.host()?.to_string()))
}
}
#[derive(Debug)]
struct DateHeader(chrono::DateTime<Utc>);
impl CreateField for DateHeader {
fn create_field(&self) -> Option<(ApSigningField, String)> {
Some((ApSigningField::Date, httpdate::fmt_http_date(self.0.into())))
}
}
#[derive(Debug)]
struct CreatedPseudoHeader(chrono::DateTime<Utc>);
impl CreateField for CreatedPseudoHeader {
fn create_field(&self) -> Option<(ApSigningField, String)> {
Some((
ApSigningField::PseudoCreated,
self.0.timestamp().to_string(),
))
}
}
#[derive(Debug)]
struct ExpiresPseudoHeader(chrono::DateTime<Utc>);
impl CreateField for ExpiresPseudoHeader {
fn create_field(&self) -> Option<(ApSigningField, String)> {
Some((
ApSigningField::PseudoExpires,
self.0.timestamp().to_string(),
))
}
}
#[derive(Debug)]
struct DigestHeader<'a>(&'a str);
impl CreateField for DigestHeader<'_> {
fn create_field(&self) -> Option<(ApSigningField, String)> {
Some((ApSigningField::Digest, self.0.to_owned()))
}
}
impl<T: CreateField> CreateField for Option<T> {
fn create_field(&self) -> Option<(ApSigningField, String)> {
self.as_ref().and_then(T::create_field)
}
}
macro_rules! signing_input {
($name:ident, $($field_name:ident => $field_type:path),+ ) => {
#[derive(Debug)]
struct $name<'a> {
$($field_name: $field_type),+
}
impl SigningInput for $name<'_> {
fn create_signing_input(&self) -> Vec<(ApSigningField, String)> {
[$(self.$field_name.create_field()),+].into_iter().flatten().collect::<Vec<_>>()
}
}
}
}
signing_input!(SigningInputGetRsaSha256,
request_target => RequestTarget<'a>,
host => HostPseudoHeader<'a>,
date => DateHeader,
expires => Option<ExpiresPseudoHeader>);
impl SigningParts for SigningInputGetRsaSha256<'_> {
fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
None
}
fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
}
}
signing_input!(SigningInputGetHs2019,
request_target => RequestTarget<'a>,
host => HostPseudoHeader<'a>,
created => CreatedPseudoHeader,
expires => Option<ExpiresPseudoHeader>);
impl SigningParts for SigningInputGetHs2019<'_> {
fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
Some(&self.created.0)
}
fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
}
}
signing_input!(SigningInputPostRsaSha256,
request_target => RequestTarget<'a>,
host => HostPseudoHeader<'a>,
date => DateHeader,
digest => DigestHeader<'a>,
expires => Option<ExpiresPseudoHeader>);
impl SigningParts for SigningInputPostRsaSha256<'_> {
fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
None
}
fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
}
}
signing_input!(SigningInputPostHs2019,
request_target => RequestTarget<'a>,
host => HostPseudoHeader<'a>,
created => CreatedPseudoHeader,
digest => DigestHeader<'a>,
expires => Option<ExpiresPseudoHeader>);
impl SigningParts for SigningInputPostHs2019<'_> {
fn get_created(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
Some(&self.created.0)
}
fn get_expires(&self) -> Option<&chrono::prelude::DateTime<Utc>> {
self.expires.as_ref().map(|ExpiresPseudoHeader(v)| v)
}
}
#[async_trait::async_trait]
impl ApClientService for ApClientServiceDefaultProvider {
type Error = ApClientError;
fn sign_request(
&self,
signing_key: ApSigningKey<'_>,
signing_algorithm: SigningAlgorithm,
request: impl SigningInput,
) -> Result<ApSignature, Self::Error> {
let components = request.create_signing_input();
let message = components
.iter()
.map(|(k, v)| format!("{}: {}", k.as_ref(), v))
.collect::<Vec<_>>()
.join("\n");
let signature = signing_key
.key
.sign_base64(signing_algorithm, &message.into_bytes())?;
Ok(ApSignature {
key_id: signing_key.key_id.clone().into_owned(),
algorithm: Some(signing_algorithm),
created: request.get_created().cloned(),
expires: request.get_expires().cloned(),
headers: Some(ApSigningHeaders(IndexSet::from_iter(
components.iter().map(|(k, _)| k).copied(),
))),
signature,
})
}
async fn signed_get(
&self,
signing_key: ApSigningKey<'_>,
signing_algorithm: SigningAlgorithm,
expires: Option<chrono::DateTime<Utc>>,
url: &str,
) -> Result<Value, Self::Error> {
let url = url.parse()?;
let time_created = Utc::now();
let signed = match signing_algorithm {
SigningAlgorithm::RsaSha256 => self.sign_request(
signing_key,
signing_algorithm,
SigningInputGetRsaSha256 {
request_target: RequestTarget {
url: &url,
method: Method::GET,
},
host: HostPseudoHeader(&url),
date: DateHeader(time_created),
expires: expires.map(ExpiresPseudoHeader),
},
)?,
SigningAlgorithm::Hs2019 => self.sign_request(
signing_key,
signing_algorithm,
SigningInputGetHs2019 {
request_target: RequestTarget {
url: &url,
method: Method::GET,
},
host: HostPseudoHeader(&url),
created: CreatedPseudoHeader(time_created),
expires: expires.map(ExpiresPseudoHeader),
},
)?,
};
let mut headers = HeaderMap::new();
if !matches!(signing_algorithm, SigningAlgorithm::Hs2019) {
headers.insert(
http::header::DATE,
HeaderValue::try_from(httpdate::fmt_http_date(time_created.into()))
.expect("date should always be a valid header value"),
);
}
headers.insert(
HeaderName::from_lowercase(b"signature").unwrap(),
HeaderValue::try_from(signed.to_string())?,
);
Ok(self
.client
.get(url)
.accept(ContentActivityStreams)
.headers(headers)
.json()
.await?)
}
async fn signed_post(
&self,
signing_key: ApSigningKey<'_>,
signing_algorithm: SigningAlgorithm,
expires: Option<chrono::DateTime<Utc>>,
url: &str,
body: &Value,
) -> Result<Value, Self::Error> {
let url = url.parse()?;
let body_bytes = serde_json::to_vec(body)?;
let mut sha = sha2::Sha256::new();
sha.update(&body_bytes);
let digest_raw = sha.finalize();
use base64::prelude::*;
let digest_base64 = format!("sha-256={}", BASE64_STANDARD.encode(digest_raw));
let time_created = Utc::now();
let signed = match signing_algorithm {
SigningAlgorithm::RsaSha256 => self.sign_request(
signing_key,
signing_algorithm,
SigningInputPostRsaSha256 {
request_target: RequestTarget {
url: &url,
method: Method::POST,
},
host: HostPseudoHeader(&url),
date: DateHeader(time_created),
digest: DigestHeader(&digest_base64),
expires: expires.map(ExpiresPseudoHeader),
},
)?,
SigningAlgorithm::Hs2019 => self.sign_request(
signing_key,
signing_algorithm,
SigningInputPostHs2019 {
request_target: RequestTarget {
url: &url,
method: Method::POST,
},
host: HostPseudoHeader(&url),
created: CreatedPseudoHeader(time_created),
digest: DigestHeader(&digest_base64),
expires: expires.map(ExpiresPseudoHeader),
},
)?,
};
let mut headers = HeaderMap::new();
if !matches!(signing_algorithm, SigningAlgorithm::Hs2019) {
headers.insert(
http::header::DATE,
HeaderValue::try_from(httpdate::fmt_http_date(time_created.into()))
.expect("date should always be a valid header value"),
);
}
headers.insert(
HeaderName::from_lowercase(b"digest").unwrap(),
HeaderValue::try_from(digest_base64)?,
);
headers.insert(
HeaderName::from_lowercase(b"signature").unwrap(),
HeaderValue::try_from(signed.to_string())?,
);
Ok(self
.client
.builder(Method::POST, url)
.accept(ContentActivityStreams)
.content_type(ContentActivityStreams)
.headers(headers)
.body(body_bytes)
.json()
.await?)
}
}
#[cfg(test)]
mod test {
use std::{borrow::Cow, sync::Arc};
use headers::UserAgent;
use miette::IntoDiagnostic;
use rsa::pkcs8::DecodePrivateKey;
use crate::{
ap_client::ApClientServiceDefaultProvider,
client::federation_client::FederationClient,
crypto::{ApHttpPrivateKey, SigningAlgorithm},
ApClientService,
};
#[tokio::test]
async fn should_request() -> miette::Result<()> {
let key_id = std::env::var("MAG_TEST_KEY_ID").into_diagnostic()?;
let key = std::env::var("MAG_TEST_PRIVATE_KEY").into_diagnostic()?;
let url = std::env::var("MAG_TEST_FETCH_URL").into_diagnostic()?;
let rsa_key = rsa::RsaPrivateKey::from_pkcs8_pem(key.trim()).into_diagnostic()?;
let ap_client = ApClientServiceDefaultProvider {
client: Arc::new(
FederationClient::new(true, 128_000, 25, UserAgent::from_static("magnetar/0.42"))
.into_diagnostic()?,
),
};
let val = ap_client
.signed_get(
ApHttpPrivateKey::Rsa(Box::new(Cow::Owned(rsa_key)))
.create_signing_key(&key_id, SigningAlgorithm::Hs2019)
.into_diagnostic()?,
SigningAlgorithm::Hs2019,
None,
&url,
)
.await
.into_diagnostic()?;
println!("{:#?}", val);
Ok(())
}
}

View File

@ -1,8 +1,8 @@
use async_stream::stream;
use futures_util::{select, stream::StreamExt, FutureExt, Stream, TryStreamExt};
use headers::Header;
use headers::UserAgent;
use hyper::body::Bytes;
use magnetar_core::web_model::{content_type::ContentActivityStreams, ContentType};
use magnetar_core::web_model::ContentType;
use reqwest::{redirect::Policy, Client, RequestBuilder};
use serde_json::Value;
use thiserror::Error;
@ -14,6 +14,7 @@ pub struct FederationClient {
pub client: Client,
pub body_limit: usize,
pub timeout_seconds: u64,
user_agent: String,
}
#[derive(Debug, Error)]
@ -51,6 +52,7 @@ impl FederationClient {
force_https: bool,
body_limit: usize,
timeout_seconds: u64,
user_agent: UserAgent,
) -> Result<FederationClient, FederationClientBuilderError> {
let client = Client::builder()
.https_only(force_https)
@ -61,13 +63,17 @@ impl FederationClient {
client,
body_limit,
timeout_seconds,
user_agent: user_agent.to_string(),
})
}
pub fn builder(&self, method: reqwest::Method, url: Url) -> FederationRequestBuilder<'_> {
FederationRequestBuilder {
client: self,
builder: self.client.request(method, url),
builder: self
.client
.request(method, url)
.header(http::header::USER_AGENT, self.user_agent.clone()),
}
}
@ -81,12 +87,21 @@ impl FederationRequestBuilder<'_> {
Self {
client: self.client,
builder: self.builder.header(
headers::ContentType::name().to_string(),
http::header::CONTENT_TYPE,
content_type.mime_type().to_string(),
),
}
}
pub fn accept(self, content_type: impl ContentType) -> Self {
Self {
client: self.client,
builder: self
.builder
.header(http::header::ACCEPT, content_type.mime_type().to_string()),
}
}
pub fn headers(self, headers: reqwest::header::HeaderMap) -> Self {
Self {
client: self.client,
@ -94,6 +109,13 @@ impl FederationRequestBuilder<'_> {
}
}
pub fn body(self, body: Vec<u8>) -> Self {
Self {
client: self.client,
builder: self.builder.body(body),
}
}
async fn send_stream(
self,
) -> Result<impl Stream<Item = Result<Bytes, FederationClientError>>, FederationClientError>
@ -146,14 +168,7 @@ impl FederationRequestBuilder<'_> {
}
}
pub async fn dereference(self) -> Result<Value, FederationClientError> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static(ContentActivityStreams.as_ref()),
);
pub async fn json(self) -> Result<Value, FederationClientError> {
let data = self.send().await?;
let json = serde_json::from_slice::<Value>(&data)?;

View File

@ -0,0 +1,210 @@
use std::{borrow::Cow, fmt::Display};
use rsa::signature::Verifier;
use rsa::{
sha2::{Sha256, Sha512},
signature::Signer,
};
use serde::{Deserialize, Serialize};
use strum::AsRefStr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApVerificationError {
#[error("Signature algorithm and public key mismatch: {0} not compatible with {1}")]
KeyAlgorithmMismatch(SigningAlgorithm, String),
#[error("PKCS#1 v1.5 RSA signature verification failed: {0}")]
RsaSignatureError(#[from] rsa::signature::Error),
}
#[derive(Debug, Error)]
pub enum ApSigningError {
#[error("Signature algorithm and public key mismatch: {0} not compatible with {1}")]
KeyAlgorithmMismatch(SigningAlgorithm, String),
#[error("Signing error: {0}")]
RsaSigningError(#[from] rsa::signature::Error),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum SigningAlgorithm {
#[serde(rename = "hs2019")]
Hs2019,
#[serde(rename = "rsa-sha256")]
RsaSha256,
}
impl Display for SigningAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hs2019 => write!(f, "hs2019"),
Self::RsaSha256 => write!(f, "rsa-sha256"),
}
}
}
#[derive(Debug, Clone, AsRefStr)]
pub enum ApHttpVerificationKey<'a> {
#[strum(serialize = "rsa-sha256")]
RsaSha256(Cow<'a, rsa::pkcs1v15::VerifyingKey<Sha256>>),
#[strum(serialize = "rsa-sha512")]
RsaSha512(Cow<'a, rsa::pkcs1v15::VerifyingKey<Sha512>>),
#[strum(serialize = "ed25519")]
Ed25519(Cow<'a, ed25519_dalek::VerifyingKey>),
}
#[derive(Debug, Clone, AsRefStr)]
pub enum ApHttpPublicKey<'a> {
#[strum(serialize = "rsa")]
Rsa(Cow<'a, rsa::RsaPublicKey>),
#[strum(serialize = "ed25519")]
Ed25519(Cow<'a, ed25519_dalek::VerifyingKey>),
}
impl ApHttpVerificationKey<'_> {
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), ApVerificationError> {
match self {
ApHttpVerificationKey::RsaSha256(rsa_pubkey) => {
let sig = rsa::pkcs1v15::Signature::try_from(signature)?;
Ok(rsa_pubkey.verify(message, &sig)?)
}
ApHttpVerificationKey::RsaSha512(rsa_pubkey) => {
let sig = rsa::pkcs1v15::Signature::try_from(signature)?;
Ok(rsa_pubkey.verify(message, &sig)?)
}
ApHttpVerificationKey::Ed25519(ed25519_pubkey) => {
let sig = ed25519_dalek::Signature::try_from(signature)?;
Ok(ed25519_pubkey.verify_strict(message, &sig)?)
}
}
}
}
impl ApHttpPublicKey<'_> {
pub fn verify(
&self,
algorithm: SigningAlgorithm,
message: &[u8],
signature: &[u8],
) -> Result<(), ApVerificationError> {
match (self, algorithm) {
(Self::Rsa(key), SigningAlgorithm::Hs2019) => {
let verification_key = ApHttpVerificationKey::RsaSha256(Cow::Owned(
rsa::pkcs1v15::VerifyingKey::new(key.clone().into_owned()),
));
let Err(_) = verification_key.verify(message, signature) else {
return Ok(());
};
let verification_key = ApHttpVerificationKey::RsaSha512(Cow::Owned(
rsa::pkcs1v15::VerifyingKey::new(key.clone().into_owned()),
));
Ok(verification_key.verify(message, signature)?)
}
(Self::Rsa(key), SigningAlgorithm::RsaSha256) => {
let verification_key = ApHttpVerificationKey::RsaSha256(Cow::Owned(
rsa::pkcs1v15::VerifyingKey::new(key.clone().into_owned()),
));
Ok(verification_key.verify(message, signature)?)
}
(_, SigningAlgorithm::RsaSha256) => {
return Err(ApVerificationError::KeyAlgorithmMismatch(
algorithm,
self.as_ref().to_owned(),
));
}
(Self::Ed25519(key), SigningAlgorithm::Hs2019) => {
let verification_key = ApHttpVerificationKey::Ed25519(Cow::Borrowed(key.as_ref()));
Ok(verification_key.verify(message, signature)?)
}
}
}
}
#[derive(Debug, Clone, AsRefStr)]
pub enum ApHttpPrivateKey<'a> {
#[strum(serialize = "rsa")]
Rsa(Box<Cow<'a, rsa::RsaPrivateKey>>),
#[strum(serialize = "ed25519")]
Ed25519(Cow<'a, ed25519_dalek::SecretKey>),
}
#[derive(Debug, Clone, AsRefStr)]
pub enum ApHttpSigningKey<'a> {
#[strum(serialize = "rsa-sha256")]
RsaSha256(Cow<'a, rsa::pkcs1v15::SigningKey<rsa::sha2::Sha256>>),
#[strum(serialize = "rsa-sha512")]
RsaSha512(Cow<'a, rsa::pkcs1v15::SigningKey<rsa::sha2::Sha512>>),
#[strum(serialize = "ed25519")]
Ed25519(Cow<'a, ed25519_dalek::SigningKey>),
}
#[derive(Debug, Clone)]
pub struct ApSigningKey<'a> {
pub key: ApHttpSigningKey<'a>,
pub key_id: Cow<'a, str>,
}
impl ApHttpSigningKey<'_> {
pub fn sign(
&self,
algorithm: SigningAlgorithm,
message: &[u8],
) -> Result<Vec<u8>, ApSigningError> {
match (self, algorithm) {
(Self::RsaSha256(key), SigningAlgorithm::RsaSha256 | SigningAlgorithm::Hs2019) => {
Ok(Box::<[u8]>::from(key.sign(message)).into_vec())
}
(Self::RsaSha512(key), SigningAlgorithm::Hs2019) => {
Ok(Box::<[u8]>::from(key.sign(message)).into_vec())
}
(Self::Ed25519(key), SigningAlgorithm::Hs2019) => {
Ok(key.sign(message).to_bytes().to_vec())
}
(key, _) => Err(ApSigningError::KeyAlgorithmMismatch(
algorithm,
key.as_ref().to_string(),
)),
}
}
pub fn sign_base64(
&self,
algorithm: SigningAlgorithm,
message: &[u8],
) -> Result<String, ApSigningError> {
let signed = self.sign(algorithm, message)?;
use base64::prelude::*;
Ok(BASE64_STANDARD.encode(signed))
}
}
impl ApHttpPrivateKey<'_> {
pub fn create_signing_key<'a>(
&'a self,
key_id: &'a str,
algorithm: SigningAlgorithm,
) -> Result<ApSigningKey<'a>, ApSigningError> {
Ok(ApSigningKey {
key_id: Cow::Borrowed(key_id),
key: match (self, algorithm) {
(Self::Rsa(key), SigningAlgorithm::RsaSha256 | SigningAlgorithm::Hs2019) => {
ApHttpSigningKey::RsaSha256(Cow::Owned(rsa::pkcs1v15::SigningKey::new(
key.clone().into_owned(),
)))
}
(Self::Ed25519(key), SigningAlgorithm::Hs2019) => ApHttpSigningKey::Ed25519(
Cow::Owned(ed25519_dalek::SigningKey::from_bytes(key.as_ref())),
),
(key, _) => {
return Err(ApSigningError::KeyAlgorithmMismatch(
algorithm,
key.as_ref().to_owned(),
))
}
},
})
}
}

View File

@ -1,10 +1,18 @@
use chrono::{DateTime, Utc};
use indexmap::IndexSet;
use magnetar_common::util::{FediverseTag, ValidName};
use magnetar_host_meta::Xrd;
use magnetar_webfinger::webfinger::WebFinger;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use strum::AsRefStr;
use url::{Host, Url};
use crate::crypto::{ApSigningKey, SigningAlgorithm};
pub mod ap_client;
pub mod client;
pub mod crypto;
pub mod lookup_flow;
/// The *visible* domain of fediverse handles, that gets resolved by WebFinger
@ -90,3 +98,79 @@ pub trait FederationLookupService {
async fn map_fedi_tag(&self, user: &UnmappedUser) -> Result<MappedUser, Self::Error>;
}
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, AsRefStr)]
pub enum ApSigningField {
#[serde(rename = "(request-target)")]
#[strum(serialize = "(request-target)")]
PseudoRequestTarget,
#[serde(rename = "(expires)")]
#[strum(serialize = "(expires)")]
PseudoExpires,
#[serde(rename = "(created)")]
#[strum(serialize = "(created)")]
PseudoCreated,
#[serde(rename = "date")]
#[strum(serialize = "date")]
Date,
#[serde(rename = "host")]
#[strum(serialize = "host")]
Host,
#[serde(rename = "digest")]
#[strum(serialize = "digest")]
Digest,
#[serde(rename = "content-length")]
#[strum(serialize = "content-length")]
ContentLength,
}
#[derive(Debug)]
pub struct ApSignature {
pub key_id: String,
pub algorithm: Option<SigningAlgorithm>,
pub created: Option<DateTime<Utc>>,
pub expires: Option<DateTime<Utc>>,
pub headers: Option<ApSigningHeaders>,
pub signature: String,
}
#[derive(Debug)]
pub struct ApSigningHeaders(pub(crate) IndexSet<ApSigningField>);
pub trait SigningParts {
fn get_created(&self) -> Option<&DateTime<Utc>>;
fn get_expires(&self) -> Option<&DateTime<Utc>>;
}
pub trait SigningInput: SigningParts {
fn create_signing_input(&self) -> Vec<(ApSigningField, String)>;
}
#[async_trait::async_trait]
pub trait ApClientService: Send + Sync {
type Error;
fn sign_request(
&self,
signing_key: ApSigningKey<'_>,
signing_algorithm: SigningAlgorithm,
request: impl SigningInput,
) -> Result<ApSignature, Self::Error>;
async fn signed_get(
&self,
signing_key: ApSigningKey<'_>,
signing_algorithm: SigningAlgorithm,
expires: Option<chrono::DateTime<Utc>>,
url: &str,
) -> Result<Value, Self::Error>;
async fn signed_post(
&self,
signing_key: ApSigningKey<'_>,
signing_algorithm: SigningAlgorithm,
expires: Option<chrono::DateTime<Utc>>,
url: &str,
body: &Value,
) -> Result<Value, Self::Error>;
}

View File

@ -174,6 +174,7 @@ impl FederationLookupService for FederationLookupServiceProviderDefault {
mod test {
use std::sync::Arc;
use headers::UserAgent;
use magnetar_common::{config::MagnetarNetworkingProtocol, util::FediverseTag};
use tracing::{info, Level};
@ -193,7 +194,10 @@ mod test {
.with_max_level(Level::TRACE)
.init();
let client = Arc::new(FederationClient::new(true, 64000, 20).unwrap());
let client = Arc::new(
FederationClient::new(true, 64000, 20, UserAgent::from_static("magnetar/0.42"))
.unwrap(),
);
let federation_lookup = FederationLookupServiceProviderDefault::new(
Arc::new(HostMetaResolverProviderDefault {