Secure mode federation support (WIP) (#39)
* First iteration of secure mode federation support * signing_actor: take request by reference * Implement secure mode fetch as a global config parameter * Implement secure mode federation example in actix-web example * fix clippy lints
This commit is contained in:
parent
19baec2138
commit
7b0b830597
8 changed files with 162 additions and 19 deletions
|
@ -4,11 +4,11 @@ use crate::{
|
||||||
objects::person::{DbUser, PersonAcceptedActivities},
|
objects::person::{DbUser, PersonAcceptedActivities},
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
actix_web::inbox::receive_activity,
|
actix_web::{inbox::receive_activity, signing_actor},
|
||||||
config::{Data, FederationConfig, FederationMiddleware},
|
config::{Data, FederationConfig, FederationMiddleware},
|
||||||
fetch::webfinger::{build_webfinger_response, extract_webfinger_name},
|
fetch::webfinger::{build_webfinger_response, extract_webfinger_name},
|
||||||
protocol::context::WithContext,
|
protocol::context::WithContext,
|
||||||
traits::Object,
|
traits::{Actor, Object},
|
||||||
FEDERATION_CONTENT_TYPE,
|
FEDERATION_CONTENT_TYPE,
|
||||||
};
|
};
|
||||||
use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer};
|
use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
@ -23,6 +23,7 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
|
||||||
let server = HttpServer::new(move || {
|
let server = HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(FederationMiddleware::new(config.clone()))
|
.wrap(FederationMiddleware::new(config.clone()))
|
||||||
|
.route("/", web::get().to(http_get_system_user))
|
||||||
.route("/{user}", web::get().to(http_get_user))
|
.route("/{user}", web::get().to(http_get_user))
|
||||||
.route("/{user}/inbox", web::post().to(http_post_user_inbox))
|
.route("/{user}/inbox", web::post().to(http_post_user_inbox))
|
||||||
.route("/.well-known/webfinger", web::get().to(webfinger))
|
.route("/.well-known/webfinger", web::get().to(webfinger))
|
||||||
|
@ -33,11 +34,28 @@ pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles requests to fetch system user json over HTTP
|
||||||
|
pub async fn http_get_system_user(data: Data<DatabaseHandle>) -> Result<HttpResponse, Error> {
|
||||||
|
let json_user = data.system_user.clone().into_json(&data).await?;
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(FEDERATION_CONTENT_TYPE)
|
||||||
|
.json(WithContext::new_default(json_user)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles requests to fetch user json over HTTP
|
/// Handles requests to fetch user json over HTTP
|
||||||
pub async fn http_get_user(
|
pub async fn http_get_user(
|
||||||
|
request: HttpRequest,
|
||||||
user_name: web::Path<String>,
|
user_name: web::Path<String>,
|
||||||
data: Data<DatabaseHandle>,
|
data: Data<DatabaseHandle>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let signed_by = signing_actor::<DbUser>(&request, None, &data).await?;
|
||||||
|
// here, checks can be made on the actor or the domain to which
|
||||||
|
// it belongs, to verify whether it is allowed to access this resource
|
||||||
|
info!(
|
||||||
|
"Fetch user request is signed by system account {}",
|
||||||
|
signed_by.id()
|
||||||
|
);
|
||||||
|
|
||||||
let db_user = data.local_user();
|
let db_user = data.local_user();
|
||||||
if user_name.into_inner() == db_user.name {
|
if user_name.into_inner() == db_user.name {
|
||||||
let json_user = db_user.into_json(&data).await?;
|
let json_user = db_user.into_json(&data).await?;
|
||||||
|
|
|
@ -15,13 +15,18 @@ pub fn new_instance(
|
||||||
hostname: &str,
|
hostname: &str,
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<FederationConfig<DatabaseHandle>, Error> {
|
) -> Result<FederationConfig<DatabaseHandle>, Error> {
|
||||||
|
let mut system_user = DbUser::new(hostname, "system".into())?;
|
||||||
|
system_user.ap_id = Url::parse(&format!("http://{}/", hostname))?.into();
|
||||||
|
|
||||||
let local_user = DbUser::new(hostname, name)?;
|
let local_user = DbUser::new(hostname, name)?;
|
||||||
let database = Arc::new(Database {
|
let database = Arc::new(Database {
|
||||||
|
system_user: system_user.clone(),
|
||||||
users: Mutex::new(vec![local_user]),
|
users: Mutex::new(vec![local_user]),
|
||||||
posts: Mutex::new(vec![]),
|
posts: Mutex::new(vec![]),
|
||||||
});
|
});
|
||||||
let config = FederationConfig::builder()
|
let config = FederationConfig::builder()
|
||||||
.domain(hostname)
|
.domain(hostname)
|
||||||
|
.signed_fetch_actor(&system_user)
|
||||||
.app_data(database)
|
.app_data(database)
|
||||||
.debug(true)
|
.debug(true)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
@ -32,6 +37,7 @@ pub type DatabaseHandle = Arc<Database>;
|
||||||
|
|
||||||
/// Our "database" which contains all known posts and users (local and federated)
|
/// Our "database" which contains all known posts and users (local and federated)
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
|
pub system_user: DbUser,
|
||||||
pub users: Mutex<Vec<DbUser>>,
|
pub users: Mutex<Vec<DbUser>>,
|
||||||
pub posts: Mutex<Vec<DbPost>>,
|
pub posts: Mutex<Vec<DbPost>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
config::Data,
|
config::Data,
|
||||||
error::Error,
|
error::Error,
|
||||||
fetch::object_id::ObjectId,
|
fetch::object_id::ObjectId,
|
||||||
http_signatures::{verify_inbox_hash, verify_signature},
|
http_signatures::{verify_body_hash, verify_signature},
|
||||||
traits::{ActivityHandler, Actor, Object},
|
traits::{ActivityHandler, Actor, Object},
|
||||||
};
|
};
|
||||||
use actix_web::{web::Bytes, HttpRequest, HttpResponse};
|
use actix_web::{web::Bytes, HttpRequest, HttpResponse};
|
||||||
|
@ -30,7 +30,7 @@ where
|
||||||
<ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
|
<ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
|
||||||
Datatype: Clone,
|
Datatype: Clone,
|
||||||
{
|
{
|
||||||
verify_inbox_hash(request.headers().get("Digest"), &body)?;
|
verify_body_hash(request.headers().get("Digest"), &body)?;
|
||||||
|
|
||||||
let activity: Activity = serde_json::from_slice(&body)?;
|
let activity: Activity = serde_json::from_slice(&body)?;
|
||||||
data.config.verify_url_and_domain(&activity).await?;
|
data.config.verify_url_and_domain(&activity).await?;
|
||||||
|
|
|
@ -3,3 +3,29 @@
|
||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Data,
|
||||||
|
error::Error,
|
||||||
|
http_signatures::{self, verify_body_hash},
|
||||||
|
traits::{Actor, Object},
|
||||||
|
};
|
||||||
|
use actix_web::{web::Bytes, HttpRequest};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Checks whether the request is signed by an actor of type A, and returns
|
||||||
|
/// the actor in question if a valid signature is found.
|
||||||
|
pub async fn signing_actor<A>(
|
||||||
|
request: &HttpRequest,
|
||||||
|
body: Option<Bytes>,
|
||||||
|
data: &Data<<A as Object>::DataType>,
|
||||||
|
) -> Result<A, <A as Object>::Error>
|
||||||
|
where
|
||||||
|
A: Object + Actor,
|
||||||
|
<A as Object>::Error: From<Error> + From<anyhow::Error>,
|
||||||
|
for<'de2> <A as Object>::Kind: Deserialize<'de2>,
|
||||||
|
{
|
||||||
|
verify_body_hash(request.headers().get("Digest"), &body.unwrap_or_default())?;
|
||||||
|
|
||||||
|
http_signatures::signing_actor(request.headers(), request.method(), request.uri(), data).await
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
config::Data,
|
config::Data,
|
||||||
error::Error,
|
error::Error,
|
||||||
fetch::object_id::ObjectId,
|
fetch::object_id::ObjectId,
|
||||||
http_signatures::{verify_inbox_hash, verify_signature},
|
http_signatures::{verify_body_hash, verify_signature},
|
||||||
traits::{ActivityHandler, Actor, Object},
|
traits::{ActivityHandler, Actor, Object},
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
|
@ -36,7 +36,7 @@ where
|
||||||
<ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
|
<ActorT as Object>::Error: From<Error> + From<anyhow::Error>,
|
||||||
Datatype: Clone,
|
Datatype: Clone,
|
||||||
{
|
{
|
||||||
verify_inbox_hash(activity_data.headers.get("Digest"), &activity_data.body)?;
|
verify_body_hash(activity_data.headers.get("Digest"), &activity_data.body)?;
|
||||||
|
|
||||||
let activity: Activity = serde_json::from_slice(&activity_data.body)?;
|
let activity: Activity = serde_json::from_slice(&activity_data.body)?;
|
||||||
data.config.verify_url_and_domain(&activity).await?;
|
data.config.verify_url_and_domain(&activity).await?;
|
||||||
|
|
|
@ -18,7 +18,7 @@ use crate::{
|
||||||
activity_queue::create_activity_queue,
|
activity_queue::create_activity_queue,
|
||||||
error::Error,
|
error::Error,
|
||||||
protocol::verification::verify_domains_match,
|
protocol::verification::verify_domains_match,
|
||||||
traits::ActivityHandler,
|
traits::{ActivityHandler, Actor},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use background_jobs::Manager;
|
use background_jobs::Manager;
|
||||||
|
@ -75,6 +75,11 @@ pub struct FederationConfig<T: Clone> {
|
||||||
/// <https://git.pleroma.social/pleroma/pleroma/-/issues/2939>
|
/// <https://git.pleroma.social/pleroma/pleroma/-/issues/2939>
|
||||||
#[builder(default = "false")]
|
#[builder(default = "false")]
|
||||||
pub(crate) http_signature_compat: bool,
|
pub(crate) http_signature_compat: bool,
|
||||||
|
/// Actor Id and private key to use to sign all federated fetch requests.
|
||||||
|
/// This can be used to implement secure mode federation.
|
||||||
|
/// <https://docs.joinmastodon.org/spec/activitypub/#secure-mode>
|
||||||
|
#[builder(default = "None", setter(custom))]
|
||||||
|
pub(crate) signed_fetch_actor: Option<Arc<(Url, String)>>,
|
||||||
/// Queue for sending outgoing activities. Only optional to make builder work, its always
|
/// Queue for sending outgoing activities. Only optional to make builder work, its always
|
||||||
/// present once constructed.
|
/// present once constructed.
|
||||||
#[builder(setter(skip))]
|
#[builder(setter(skip))]
|
||||||
|
@ -170,6 +175,15 @@ impl<T: Clone> FederationConfig<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone> FederationConfigBuilder<T> {
|
impl<T: Clone> FederationConfigBuilder<T> {
|
||||||
|
/// Sets an actor to use to sign all federated fetch requests
|
||||||
|
pub fn signed_fetch_actor<A: Actor>(&mut self, actor: &A) -> &mut Self {
|
||||||
|
let private_key_pem = actor
|
||||||
|
.private_key_pem()
|
||||||
|
.expect("actor does not have a private key to sign with");
|
||||||
|
self.signed_fetch_actor = Some(Some(Arc::new((actor.id(), private_key_pem))));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs a new config instance with the values supplied to builder.
|
/// Constructs a new config instance with the values supplied to builder.
|
||||||
///
|
///
|
||||||
/// Values which are not explicitly specified use the defaults. Also initializes the
|
/// Values which are not explicitly specified use the defaults. Also initializes the
|
||||||
|
|
|
@ -2,7 +2,13 @@
|
||||||
//!
|
//!
|
||||||
#![doc = include_str!("../../docs/07_fetching_data.md")]
|
#![doc = include_str!("../../docs/07_fetching_data.md")]
|
||||||
|
|
||||||
use crate::{config::Data, error::Error, reqwest_shim::ResponseExt, FEDERATION_CONTENT_TYPE};
|
use crate::{
|
||||||
|
config::Data,
|
||||||
|
error::Error,
|
||||||
|
http_signatures::sign_request,
|
||||||
|
reqwest_shim::ResponseExt,
|
||||||
|
FEDERATION_CONTENT_TYPE,
|
||||||
|
};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
@ -41,14 +47,25 @@ pub async fn fetch_object_http<T: Clone, Kind: DeserializeOwned>(
|
||||||
return Err(Error::RequestLimit);
|
return Err(Error::RequestLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = config
|
let req = config
|
||||||
.client
|
.client
|
||||||
.get(url.as_str())
|
.get(url.as_str())
|
||||||
.header("Accept", FEDERATION_CONTENT_TYPE)
|
.header("Accept", FEDERATION_CONTENT_TYPE)
|
||||||
.timeout(config.request_timeout)
|
.timeout(config.request_timeout);
|
||||||
.send()
|
|
||||||
.await
|
let res = if let Some((actor_id, private_key_pem)) = config.signed_fetch_actor.as_deref() {
|
||||||
.map_err(Error::other)?;
|
let req = sign_request(
|
||||||
|
req,
|
||||||
|
actor_id.clone(),
|
||||||
|
String::new(),
|
||||||
|
private_key_pem.clone(),
|
||||||
|
data.config.http_signature_compat,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
config.client.execute(req).await.map_err(Error::other)?
|
||||||
|
} else {
|
||||||
|
req.send().await.map_err(Error::other)?
|
||||||
|
};
|
||||||
|
|
||||||
if res.status() == StatusCode::GONE {
|
if res.status() == StatusCode::GONE {
|
||||||
return Err(Error::ObjectDeleted);
|
return Err(Error::ObjectDeleted);
|
||||||
|
|
|
@ -6,8 +6,11 @@
|
||||||
//! [receive_activity (axum)](crate::axum::inbox::receive_activity).
|
//! [receive_activity (axum)](crate::axum::inbox::receive_activity).
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::Data,
|
||||||
error::{Error, Error::ActivitySignatureInvalid},
|
error::{Error, Error::ActivitySignatureInvalid},
|
||||||
|
fetch::object_id::ObjectId,
|
||||||
protocol::public_key::main_key_id,
|
protocol::public_key::main_key_id,
|
||||||
|
traits::{Actor, Object},
|
||||||
};
|
};
|
||||||
use base64::{engine::general_purpose::STANDARD as Base64, Engine};
|
use base64::{engine::general_purpose::STANDARD as Base64, Engine};
|
||||||
use http::{header::HeaderName, uri::PathAndQuery, HeaderValue, Method, Uri};
|
use http::{header::HeaderName, uri::PathAndQuery, HeaderValue, Method, Uri};
|
||||||
|
@ -21,6 +24,7 @@ use openssl::{
|
||||||
};
|
};
|
||||||
use reqwest::Request;
|
use reqwest::Request;
|
||||||
use reqwest_middleware::RequestBuilder;
|
use reqwest_middleware::RequestBuilder;
|
||||||
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{collections::BTreeMap, fmt::Debug, io::ErrorKind};
|
use std::{collections::BTreeMap, fmt::Debug, io::ErrorKind};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
@ -91,7 +95,11 @@ pub(crate) async fn sign_request(
|
||||||
static CONFIG2: Lazy<http_signature_normalization::Config> =
|
static CONFIG2: Lazy<http_signature_normalization::Config> =
|
||||||
Lazy::new(http_signature_normalization::Config::new);
|
Lazy::new(http_signature_normalization::Config::new);
|
||||||
|
|
||||||
/// Verifies the HTTP signature on an incoming inbox request.
|
/// Verifies the HTTP signature on an incoming federation request
|
||||||
|
/// for a given actor's public key.
|
||||||
|
///
|
||||||
|
/// Internally, this just converts the headers to a BTreeMap and passes to
|
||||||
|
/// `verify_signature_inner` for actual signature verification.
|
||||||
pub(crate) fn verify_signature<'a, H>(
|
pub(crate) fn verify_signature<'a, H>(
|
||||||
headers: H,
|
headers: H,
|
||||||
method: &Method,
|
method: &Method,
|
||||||
|
@ -107,6 +115,60 @@ where
|
||||||
header_map.insert(name.to_string(), value.to_string());
|
header_map.insert(name.to_string(), value.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify_signature_inner(header_map, method, uri, public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the given federation request has a valid signature,
|
||||||
|
/// from any actor of type A, and returns that actor if a valid signature is found.
|
||||||
|
/// This function will return an `Err` variant when no signature is found
|
||||||
|
/// or if the signature could not be verified.
|
||||||
|
pub(crate) async fn signing_actor<'a, A, H>(
|
||||||
|
headers: H,
|
||||||
|
method: &Method,
|
||||||
|
uri: &Uri,
|
||||||
|
data: &Data<<A as Object>::DataType>,
|
||||||
|
) -> Result<A, <A as Object>::Error>
|
||||||
|
where
|
||||||
|
A: Object + Actor,
|
||||||
|
<A as Object>::Error: From<Error> + From<anyhow::Error>,
|
||||||
|
for<'de2> <A as Object>::Kind: Deserialize<'de2>,
|
||||||
|
H: IntoIterator<Item = (&'a HeaderName, &'a HeaderValue)>,
|
||||||
|
{
|
||||||
|
let mut header_map = BTreeMap::<String, String>::new();
|
||||||
|
for (name, value) in headers {
|
||||||
|
if let Ok(value) = value.to_str() {
|
||||||
|
header_map.insert(name.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let signature = header_map
|
||||||
|
.get("signature")
|
||||||
|
.ok_or(Error::ActivitySignatureInvalid)?;
|
||||||
|
|
||||||
|
let actor_id_re = regex::Regex::new("keyId=\"([^\"]+)#([^\"]+)\"").expect("regex error");
|
||||||
|
let actor_id = match actor_id_re.captures(signature) {
|
||||||
|
None => return Err(Error::ActivitySignatureInvalid.into()),
|
||||||
|
Some(caps) => caps.get(1).expect("regex error").as_str(),
|
||||||
|
};
|
||||||
|
let actor_url = Url::parse(actor_id).map_err(|_| Error::ActivitySignatureInvalid)?;
|
||||||
|
let actor_id: ObjectId<A> = actor_url.into();
|
||||||
|
|
||||||
|
let actor = actor_id.dereference(data).await?;
|
||||||
|
let public_key = actor.public_key_pem();
|
||||||
|
|
||||||
|
verify_signature_inner(header_map, method, uri, public_key)?;
|
||||||
|
|
||||||
|
Ok(actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies that the signature present in the request is valid for
|
||||||
|
/// the specified actor's public key.
|
||||||
|
fn verify_signature_inner(
|
||||||
|
header_map: BTreeMap<String, String>,
|
||||||
|
method: &Method,
|
||||||
|
uri: &Uri,
|
||||||
|
public_key: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let path_and_query = uri.path_and_query().map(PathAndQuery::as_str).unwrap_or("");
|
let path_and_query = uri.path_and_query().map(PathAndQuery::as_str).unwrap_or("");
|
||||||
|
|
||||||
let verified = CONFIG2
|
let verified = CONFIG2
|
||||||
|
@ -166,7 +228,7 @@ impl DigestPart {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify body of an inbox request against the hash provided in `Digest` header.
|
/// Verify body of an inbox request against the hash provided in `Digest` header.
|
||||||
pub(crate) fn verify_inbox_hash(
|
pub(crate) fn verify_body_hash(
|
||||||
digest_header: Option<&HeaderValue>,
|
digest_header: Option<&HeaderValue>,
|
||||||
body: &[u8],
|
body: &[u8],
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
@ -266,21 +328,21 @@ pub mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_verify_inbox_hash_valid() {
|
fn test_verify_body_hash_valid() {
|
||||||
let digest_header =
|
let digest_header =
|
||||||
HeaderValue::from_static("SHA-256=lzFT+G7C2hdI5j8M+FuJg1tC+O6AGMVJhooTCKGfbKM=");
|
HeaderValue::from_static("SHA-256=lzFT+G7C2hdI5j8M+FuJg1tC+O6AGMVJhooTCKGfbKM=");
|
||||||
let body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
|
let body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
|
||||||
let valid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
|
let valid = verify_body_hash(Some(&digest_header), body.as_bytes());
|
||||||
println!("{:?}", &valid);
|
println!("{:?}", &valid);
|
||||||
assert!(valid.is_ok());
|
assert!(valid.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_verify_inbox_hash_not_valid() {
|
fn test_verify_body_hash_not_valid() {
|
||||||
let digest_header =
|
let digest_header =
|
||||||
HeaderValue::from_static("SHA-256=Z9h7DJfYWjffXw2XftmWCnpEaK/yqOHKvzCIzIaqgbU=");
|
HeaderValue::from_static("SHA-256=Z9h7DJfYWjffXw2XftmWCnpEaK/yqOHKvzCIzIaqgbU=");
|
||||||
let body = "lorem ipsum";
|
let body = "lorem ipsum";
|
||||||
let invalid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
|
let invalid = verify_body_hash(Some(&digest_header), body.as_bytes());
|
||||||
assert_eq!(invalid, Err(Error::ActivityBodyDigestInvalid));
|
assert_eq!(invalid, Err(Error::ActivityBodyDigestInvalid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue