Implemented outbound request signing
This commit is contained in:
parent
aefef079a7
commit
ce46782318
|
@ -1726,6 +1726,7 @@ dependencies = [
|
|||
"futures-util",
|
||||
"headers",
|
||||
"http",
|
||||
"httpdate",
|
||||
"hyper",
|
||||
"indexmap",
|
||||
"magnetar_common",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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(),
|
||||
))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue