"derive_more 0.99.3 (registry+https://github.com/rust-lang/crates.io-index)",
"either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
"trust-dns-resolver 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"h2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytestring 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
[[package]]
name = "http"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "http-signature-normalization"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thiserror 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "httparse"
version = "1.3.4"
"futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
"actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"actix-web-actors 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bcrypt 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"comrak 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"hjson 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http-signature-normalization 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"isahc 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonwebtoken 7.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
"checksum hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
"checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
-"checksum http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b"
+"checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9"
+"checksum http-signature-normalization 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "257835255b5d40c6de712d90e56dc874ca5da2816121e7b9f3cfc7b3a55a5714"
"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
"checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
isahc = "0.9"
comrak = "0.7"
openssl = "0.10"
+http = "0.2.1"
+http-signature-normalization = "0.4.1"
+base64 = "0.12.0"
use super::*;
use crate::apub::activities::follow_community;
-use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType};
+use crate::apub::signatures::generate_actor_keypair;
+use crate::apub::{make_apub_endpoint, EndpointType};
use diesel::PgConnection;
use std::str::FromStr;
}
// When you create a community, make sure the user becomes a moderator and a follower
- let (community_public_key, community_private_key) = gen_keypair_str();
+ let keypair = generate_actor_keypair();
let community_form = CommunityForm {
name: data.name.to_owned(),
updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(),
local: true,
- private_key: Some(community_private_key),
- public_key: Some(community_public_key),
+ private_key: Some(keypair.private_key),
+ public_key: Some(keypair.public_key),
last_refreshed_at: None,
published: None,
};
use super::*;
-use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType};
+use crate::apub::signatures::generate_actor_keypair;
+use crate::apub::{make_apub_endpoint, EndpointType};
use crate::settings::Settings;
use crate::{generate_random_string, send_email};
use bcrypt::verify;
return Err(APIError::err("admin_already_created").into());
}
- let (user_public_key, user_private_key) = gen_keypair_str();
+ let keypair = generate_actor_keypair();
// Register the new user
let user_form = UserForm {
actor_id: make_apub_endpoint(EndpointType::User, &data.username).to_string(),
bio: None,
local: true,
- private_key: Some(user_private_key),
- public_key: Some(user_public_key),
+ private_key: Some(keypair.private_key),
+ public_key: Some(keypair.public_key),
last_refreshed_at: None,
};
}
};
- let (community_public_key, community_private_key) = gen_keypair_str();
+ let keypair = generate_actor_keypair();
// Create the main community if it doesn't exist
let main_community: Community = match Community::read(&conn, 2) {
updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, default_community_name).to_string(),
local: true,
- private_key: Some(community_private_key),
- public_key: Some(community_public_key),
+ private_key: Some(keypair.private_key),
+ public_key: Some(keypair.public_key),
last_refreshed_at: None,
published: None,
};
use crate::apub::is_apub_id_valid;
+use crate::apub::signatures::{sign, Keypair};
use crate::db::community::Community;
use crate::db::community_view::CommunityFollowerView;
use crate::db::post::Post;
use isahc::prelude::*;
use log::debug;
use serde::Serialize;
+use url::Url;
fn populate_object_props(
props: &mut ObjectProperties,
}
/// Send an activity to a list of recipients, using the correct headers etc.
-fn send_activity<A>(activity: &A, to: Vec<String>) -> Result<(), Error>
+fn send_activity<A>(
+ activity: &A,
+ keypair: &Keypair,
+ sender_id: &str,
+ to: Vec<String>,
+) -> Result<(), Error>
where
A: Serialize + Debug,
{
let json = serde_json::to_string(&activity)?;
debug!("Sending activitypub activity {} to {:?}", json, to);
for t in to {
- if !is_apub_id_valid(&t) {
+ let to_url = Url::parse(&t)?;
+ if !is_apub_id_valid(&to_url) {
debug!("Not sending activity to {} (invalid or blacklisted)", t);
continue;
}
- let res = Request::post(t)
+ let request = Request::post(t).header("Host", to_url.domain().unwrap());
+ let signature = sign(&request, keypair, sender_id)?;
+ let res = request
+ .header("Signature", signature)
.header("Content-Type", "application/json")
.body(json.to_owned())?
.send()?;
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
- send_activity(&create, get_follower_inboxes(conn, &community)?)?;
+ send_activity(
+ &create,
+ &creator.get_keypair().unwrap(),
+ &creator.actor_id,
+ get_follower_inboxes(conn, &community)?,
+ )?;
Ok(())
}
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(page)?;
- send_activity(&update, get_follower_inboxes(conn, &community)?)?;
+ send_activity(
+ &update,
+ &creator.get_keypair().unwrap(),
+ &creator.actor_id,
+ get_follower_inboxes(conn, &community)?,
+ )?;
Ok(())
}
.set_actor_xsd_any_uri(user.actor_id.clone())?
.set_object_xsd_any_uri(community.actor_id.clone())?;
let to = format!("{}/inbox", community.actor_id);
- send_activity(&follow, vec![to])?;
+ send_activity(
+ &follow,
+ &community.get_keypair().unwrap(),
+ &community.actor_id,
+ vec![to],
+ )?;
Ok(())
}
/// As a local community, accept the follow request from a remote user.
-pub fn accept_follow(follow: &Follow) -> Result<(), Error> {
+pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error> {
+ let community_uri = follow
+ .follow_props
+ .get_actor_xsd_any_uri()
+ .unwrap()
+ .to_string();
+ let community = Community::read_from_actor_id(conn, &community_uri)?;
let mut accept = Accept::new();
accept
.object_props
accept
.accept_props
.set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
- let to = format!(
- "{}/inbox",
- follow
- .follow_props
- .get_actor_xsd_any_uri()
- .unwrap()
- .to_string()
- );
- send_activity(&accept, vec![to])?;
+ let to = format!("{}/inbox", community_uri);
+ send_activity(
+ &accept,
+ &community.get_keypair().unwrap(),
+ &community.actor_id,
+ vec![to],
+ )?;
Ok(())
}
user_id: user.id,
};
CommunityFollower::follow(&conn, &community_follower_form)?;
- accept_follow(&follow)?;
+ accept_follow(&follow, conn)?;
Ok(HttpResponse::Ok().finish())
}
where
Response: for<'de> Deserialize<'de>,
{
- if !is_apub_id_valid(&url.to_string()) {
+ if !is_apub_id_valid(&url) {
return Err(format_err!("Activitypub uri invalid or blocked: {}", url));
}
// TODO: this function should return a future
use activitystreams::ext::Ext;
use actix_web::body::Body;
use actix_web::HttpResponse;
-use openssl::{pkey::PKey, rsa::Rsa};
use serde::ser::Serialize;
use url::Url;
}
}
-/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
-pub fn gen_keypair_str() -> (String, String) {
- let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error");
- let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error");
- let public_key = pkey
- .public_key_to_pem()
- .expect("sign::gen_keypair: public key encoding error");
- let private_key = pkey
- .private_key_to_pem_pkcs8()
- .expect("sign::gen_keypair: private key encoding error");
- (vec_bytes_to_str(public_key), vec_bytes_to_str(private_key))
-}
-
-fn vec_bytes_to_str(bytes: Vec<u8>) -> String {
- String::from_utf8_lossy(&bytes).into_owned()
-}
-
// Checks if the ID has a valid format, correct scheme, and is in the whitelist.
-fn is_apub_id_valid(apub_id: &str) -> bool {
- let url = match Url::parse(apub_id) {
- Ok(u) => u,
- Err(_) => return false,
- };
-
- if url.scheme() != get_apub_protocol_string() {
+fn is_apub_id_valid(apub_id: &Url) -> bool {
+ if apub_id.scheme() != get_apub_protocol_string() {
return false;
}
.split(',')
.map(|d| d.to_string())
.collect();
- match url.domain() {
+ match apub_id.domain() {
Some(d) => whitelist.contains(&d.to_owned()),
None => false,
}
-// For this example, we'll use the Extensible trait, the Extension trait, the Actor trait, and
-// the Person type
use activitystreams::{actor::Actor, ext::Extension};
+use failure::Error;
+use http::request::Builder;
+use http_signature_normalization::Config;
+use openssl::hash::MessageDigest;
+use openssl::sign::Signer;
+use openssl::{pkey::PKey, rsa::Rsa};
use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
+
+pub struct Keypair {
+ pub private_key: String,
+ pub public_key: String,
+}
+
+/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
+pub fn generate_actor_keypair() -> Keypair {
+ let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error");
+ let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error");
+ let public_key = pkey
+ .public_key_to_pem()
+ .expect("sign::gen_keypair: public key encoding error");
+ let private_key = pkey
+ .private_key_to_pem_pkcs8()
+ .expect("sign::gen_keypair: private key encoding error");
+ Keypair {
+ private_key: String::from_utf8_lossy(&private_key).into_owned(),
+ public_key: String::from_utf8_lossy(&public_key).into_owned(),
+ }
+}
+
+/// Signs request headers with the given keypair.
+pub fn sign(request: &Builder, keypair: &Keypair, sender_id: &str) -> Result<String, Error> {
+ let signing_key_id = format!("{}#main-key", sender_id);
+ let config = Config::new();
+
+ let headers = request
+ .headers_ref()
+ .unwrap()
+ .iter()
+ .map(|h| -> Result<(String, String), Error> {
+ Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned()))
+ })
+ .collect::<Result<BTreeMap<String, String>, Error>>()?;
+
+ let signature_header_value = config
+ .begin_sign(
+ request.method_ref().unwrap().as_str(),
+ request
+ .uri_ref()
+ .unwrap()
+ .path_and_query()
+ .unwrap()
+ .as_str(),
+ headers,
+ )
+ .sign(signing_key_id, |signing_string| {
+ let private_key = PKey::private_key_from_pem(keypair.private_key.as_bytes())?;
+ let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
+ signer.update(signing_string.as_bytes()).unwrap();
+ Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, Error>
+ })?
+ .signature_header();
+
+ Ok(signature_header_value)
+}
// The following is taken from here:
// https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
use super::post::Post;
use super::user::{UserForm, User_};
use super::*;
-use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType};
+use crate::apub::signatures::generate_actor_keypair;
+use crate::apub::{make_apub_endpoint, EndpointType};
use crate::naive_now;
use log::info;
.load::<User_>(conn)?;
for cuser in &incorrect_users {
- let (user_public_key, user_private_key) = gen_keypair_str();
+ let keypair = generate_actor_keypair();
let form = UserForm {
name: cuser.name.to_owned(),
actor_id: make_apub_endpoint(EndpointType::User, &cuser.name).to_string(),
bio: cuser.bio.to_owned(),
local: cuser.local,
- private_key: Some(user_private_key),
- public_key: Some(user_public_key),
+ private_key: Some(keypair.private_key),
+ public_key: Some(keypair.public_key),
last_refreshed_at: Some(naive_now()),
};
.load::<Community>(conn)?;
for ccommunity in &incorrect_communities {
- let (community_public_key, community_private_key) = gen_keypair_str();
+ let keypair = generate_actor_keypair();
let form = CommunityForm {
name: ccommunity.name.to_owned(),
updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, &ccommunity.name).to_string(),
local: ccommunity.local,
- private_key: Some(community_private_key),
- public_key: Some(community_public_key),
+ private_key: Some(keypair.private_key),
+ public_key: Some(keypair.public_key),
last_refreshed_at: Some(naive_now()),
published: None,
};
use super::*;
+use crate::apub::signatures::Keypair;
use crate::schema::{community, community_follower, community_moderator, community_user_ban};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
pub fn get_url(&self) -> String {
format!("https://{}/c/{}", Settings::get().hostname, self.name)
}
+
+ pub fn get_keypair(&self) -> Option<Keypair> {
+ if let Some(private) = self.private_key.to_owned() {
+ if let Some(public) = self.public_key.to_owned() {
+ Some(Keypair {
+ private_key: private,
+ public_key: public,
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ }
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
use super::*;
+use crate::apub::signatures::Keypair;
use crate::schema::user_;
use crate::schema::user_::dsl::*;
use crate::{is_email_regex, naive_now, Settings};
use crate::schema::user_::dsl::*;
user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
}
+
+ pub fn get_keypair(&self) -> Option<Keypair> {
+ if let Some(private) = self.private_key.to_owned() {
+ if let Some(public) = self.public_key.to_owned() {
+ Some(Keypair {
+ private_key: private,
+ public_key: public,
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ }
}
#[derive(Debug, Serialize, Deserialize)]