diff --git a/Cargo.lock b/Cargo.lock index 90759a6..9f8ca0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1707,6 +1707,7 @@ dependencies = [ "rust-argon2", "secp256k1", "serde", + "serde_jcs", "serde_json", "serde_yaml", "serial_test", @@ -2742,6 +2743,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "ryu-js" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" + [[package]] name = "same-file" version = "1.0.6" @@ -2850,6 +2857,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_jcs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cacecf649bc1a7c5f0e299cc813977c6a78116abda2b93b1ee01735b71ead9a8" +dependencies = [ + "ryu-js", + "serde", + "serde_json", +] + [[package]] name = "serde_json" version = "1.0.64" diff --git a/Cargo.toml b/Cargo.toml index 73b139a..a9b22ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,8 @@ secp256k1 = { version = "0.21.3", features = ["rand", "rand-std"] } # Used for serialization/deserialization serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0" +# Used to create JCS representations +serde_jcs = "0.1.0" # Used to parse config file serde_yaml = "0.8.17" # Used to calculate SHA2 hashes diff --git a/src/activitypub/deliverer.rs b/src/activitypub/deliverer.rs index 14078e1..5610d99 100644 --- a/src/activitypub/deliverer.rs +++ b/src/activitypub/deliverer.rs @@ -12,6 +12,10 @@ use crate::http_signatures::create::{ create_http_signature, HttpSignatureError, }; +use crate::json_signatures::create::{ + sign_object, + JsonSignatureError, +}; use crate::models::users::types::User; use crate::utils::crypto::deserialize_private_key; use crate::utils::urls::get_hostname; @@ -27,6 +31,9 @@ pub enum DelivererError { #[error(transparent)] HttpSignatureError(#[from] HttpSignatureError), + #[error(transparent)] + JsonSignatureError(#[from] JsonSignatureError), + #[error("activity serialization error")] SerializationError(#[from] serde_json::Error), @@ -115,7 +122,8 @@ async fn deliver_activity_worker( ), ACTOR_KEY_SUFFIX, ); - let activity_json = serde_json::to_string(&activity)?; + let activity_signed = sign_object(&activity, &actor_key, &actor_key_id)?; + let activity_json = serde_json::to_string(&activity_signed)?; if recipients.is_empty() { return Ok(()); }; diff --git a/src/activitypub/signatures.rs b/src/activitypub/signatures.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/activitypub/signatures.rs @@ -0,0 +1 @@ + diff --git a/src/json_signatures/create.rs b/src/json_signatures/create.rs new file mode 100644 index 0000000..8199667 --- /dev/null +++ b/src/json_signatures/create.rs @@ -0,0 +1,94 @@ +use rsa::RsaPrivateKey; +use serde::Serialize; +use serde_json::Value; + +use crate::utils::crypto::sign_message; + +/// Data Integrity Proof +/// https://w3c.github.io/vc-data-integrity/ +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Proof { + #[serde(rename = "type")] + proof_type: String, + proof_purpose: String, + verification_method: String, + proof_value: String, +} + +// Similar to https://identity.foundation/JcsEd25519Signature2020/ +// - Canonicalization algorithm: JCS +// - Digest algorithm: SHA-256 +// - Signature algorithm: RSASSA-PKCS1-v1_5 +const PROOF_TYPE: &str = "JcsRsaSignature2022"; + +const PROOF_PURPOSE: &str = "assertionMethod"; + +#[derive(thiserror::Error, Debug)] +pub enum JsonSignatureError { + #[error(transparent)] + JsonError(#[from] serde_json::Error), + + #[error("signing error")] + SigningError(#[from] rsa::errors::Error), + + #[error("invalid value")] + InvalidValue, +} + +pub fn sign_object( + object: &impl Serialize, + signer_key: &RsaPrivateKey, + signer_key_id: &str, +) -> Result { + // Canonicalize + // JCS: https://www.rfc-editor.org/rfc/rfc8785 + let object_str = serde_jcs::to_string(object)?; + // Sign + let signature_b64 = sign_message(signer_key, &object_str)?; + // Insert proof + let proof = Proof { + proof_type: PROOF_TYPE.to_string(), + proof_purpose: PROOF_PURPOSE.to_string(), + verification_method: signer_key_id.to_string(), + proof_value: signature_b64, + }; + let proof_value = serde_json::to_value(proof)?; + let mut object_value = serde_json::to_value(object)?; + let object_map = object_value.as_object_mut() + .ok_or(JsonSignatureError::InvalidValue)?; + object_map.insert("proof".to_string(), proof_value); + Ok(object_value) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use crate::utils::crypto::generate_weak_private_key; + use super::*; + + #[test] + fn test_sign_object() { + let signer_key = generate_weak_private_key().unwrap(); + let signer_key_id = "https://example.org/users/test#main-key"; + let object = json!({ + "type": "Create", + "actor": "https://example.org/users/test", + "id": "https://example.org/objects/1", + "to": [ + "https://example.org/users/yyy", + "https://example.org/users/xxx", + ], + "object": { + "type": "Note", + "content": "test", + }, + }); + let result = sign_object(&object, &signer_key, signer_key_id).unwrap(); + let result_str = serde_json::to_string(&result).unwrap(); + assert_eq!( + result_str, + r#"{"actor":"https://example.org/users/test","id":"https://example.org/objects/1","object":{"content":"test","type":"Note"},"proof":{"proofPurpose":"assertionMethod","proofValue":"P4ye1hDvrGQCCClzHfCU9xobMAeqlUfgEWGlZfOTE3WmjH8JC/OJwlsjUMOUwTVlyKStp+AY+zzU4z6mjZN0Ug==","type":"JcsRsaSignature2022","verificationMethod":"https://example.org/users/test#main-key"},"to":["https://example.org/users/yyy","https://example.org/users/xxx"],"type":"Create"}"#, + ); + } +} diff --git a/src/json_signatures/mod.rs b/src/json_signatures/mod.rs new file mode 100644 index 0000000..c5fb369 --- /dev/null +++ b/src/json_signatures/mod.rs @@ -0,0 +1 @@ +pub mod create; diff --git a/src/lib.rs b/src/lib.rs index 2a92622..89819f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ mod frontend; pub mod http; mod http_signatures; mod ipfs; +mod json_signatures; pub mod logger; pub mod mastodon_api; mod models;