From 14a123ad7e50069b5212acb96655f4f35228f7f2 Mon Sep 17 00:00:00 2001 From: silverpill Date: Thu, 10 Nov 2022 10:13:03 +0000 Subject: [PATCH] Implement minisign identity proofs --- Cargo.lock | 43 ++++++++- Cargo.toml | 4 + docs/openapi.yaml | 1 + src/identity/minisign.rs | 136 +++++++++++++++++++++++++++++ src/identity/mod.rs | 1 + src/mastodon_api/accounts/views.rs | 63 ++++++++----- 6 files changed, 222 insertions(+), 26 deletions(-) create mode 100644 src/identity/minisign.rs diff --git a/Cargo.lock b/Cargo.lock index 96d9583..3191a59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,6 +389,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -690,9 +699,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -798,6 +807,29 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e1069e39f1454367eb2de793ed062fac4c35c2934b76a81d90dd9abcd28816" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.5", + "zeroize", +] + [[package]] name = "either" version = "1.8.0" @@ -1687,6 +1719,7 @@ dependencies = [ "ammonia", "anyhow", "base64", + "blake2", "bs58", "chrono", "clap", @@ -1694,6 +1727,8 @@ dependencies = [ "deadpool", "deadpool-postgres", "dotenv", + "ed25519", + "ed25519-dalek", "env_logger", "hex", "log", @@ -3774,9 +3809,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index e4c968d..19420ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,10 @@ hex = { version = "0.4.3", features = ["serde"] } # Used for logging log = { version = "0.4.14", features = ["serde"] } env_logger = { version = "0.9.0", default-features = false } +# Used to verify minisign signatures +ed25519-dalek = "1.0.1" +ed25519 = "=1.3.0" +blake2 = "0.10.4" # Used to guess media type of a file mime_guess = "2.0.3" mime-sniffer = "0.1.2" diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 76a03c0..b6d118c 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -158,6 +158,7 @@ paths: type: string enum: - ethereum + - minisign - name: signer in: query description: Information about the signer. diff --git a/src/identity/minisign.rs b/src/identity/minisign.rs new file mode 100644 index 0000000..cd8e0a7 --- /dev/null +++ b/src/identity/minisign.rs @@ -0,0 +1,136 @@ +/// https://jedisct1.github.io/minisign/ +use blake2::{Blake2b512, Digest}; +use ed25519_dalek::{ + PublicKey, + Signature, + SignatureError, + Verifier, +}; + +use super::did_key::{DidKey, MulticodecError}; + +pub const IDENTITY_PROOF_MINISIGN: &str = "MinisignSignatureDemo0"; + +const MINISIGN_SIGNATURE_CODE: [u8; 2] = *b"Ed"; +const MINISIGN_SIGNATURE_HASHED_CODE: [u8; 2] = *b"ED"; + +#[derive(thiserror::Error, Debug)] +pub enum ParseError { + #[error("invalid encoding")] + InvalidEncoding(#[from] base64::DecodeError), + + #[error("invalid key length")] + InvalidKeyLength, + + #[error("invalid signature length")] + InvalidSignatureLength, + + #[error("invalid signature type")] + InvalidSignatureType, +} + +// Public key format: +// base64( || || ) +fn parse_minisign_public_key(key_b64: &str) + -> Result<[u8; 32], ParseError> +{ + let key_bin = base64::decode(key_b64)?; + if key_bin.len() != 42 { + return Err(ParseError::InvalidKeyLength); + }; + + let mut signature_algorithm = [0; 2]; + let mut _key_id = [0; 8]; + let mut key = [0; 32]; + signature_algorithm.copy_from_slice(&key_bin[0..2]); + _key_id.copy_from_slice(&key_bin[2..10]); + key.copy_from_slice(&key_bin[10..42]); + + if signature_algorithm.as_ref() != MINISIGN_SIGNATURE_CODE { + return Err(ParseError::InvalidSignatureType); + }; + Ok(key) +} + +pub fn minisign_key_to_did(key_b64: &str) -> Result { + let key = parse_minisign_public_key(key_b64)?; + let did_key = DidKey::from_ed25519_key(key); + Ok(did_key) +} + +// Signature format: +// base64( || || ) +fn parse_minisign_signature(signature_b64: &str) + -> Result<[u8; 64], ParseError> +{ + let signature_bin = base64::decode(signature_b64)?; + if signature_bin.len() != 74 { + return Err(ParseError::InvalidSignatureLength); + }; + + let mut signature_algorithm = [0; 2]; + let mut _key_id = [0; 8]; + let mut signature = [0; 64]; + signature_algorithm.copy_from_slice(&signature_bin[0..2]); + _key_id.copy_from_slice(&signature_bin[2..10]); + signature.copy_from_slice(&signature_bin[10..74]); + + if signature_algorithm.as_ref() != MINISIGN_SIGNATURE_HASHED_CODE { + return Err(ParseError::InvalidSignatureType); + }; + Ok(signature) +} + +fn verify_signature( + message: &str, + signer: [u8; 32], + signature: [u8; 64], +) -> Result<(), SignatureError> { + let signature = Signature::from_bytes(&signature)?; + let public_key = PublicKey::from_bytes(&signer)?; + let mut hasher = Blake2b512::new(); + hasher.update(message); + let hash = hasher.finalize(); + public_key.verify(&hash, &signature)?; + Ok(()) +} + +#[derive(thiserror::Error, Debug)] +pub enum VerificationError { + #[error(transparent)] + InvalidKey(#[from] MulticodecError), + + #[error(transparent)] + ParseError(#[from] ParseError), + + #[error(transparent)] + SignatureError(#[from] SignatureError), +} + +pub fn verify_minisign_identity_proof( + signer: &DidKey, + message: &str, + signature: &str, +) -> Result<(), VerificationError> { + let ed25519_key = signer.try_ed25519_key()?; + let ed25519_signature = parse_minisign_signature(signature)?; + let message = format!("{}\n", message); + verify_signature(&message, ed25519_key, ed25519_signature)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_verify_minisign_identity_proof() { + let minisign_key = + "RWSA58rRENpGFYwAjRjbdST7VHFoIuH9JBHfO2u6i5JgANPIoQhABAF/"; + let message = "test"; + let minisign_signature = + "RUSA58rRENpGFVKxdZGMG1WdIJ+dlyP83qOqw6GP0H/Li6Brug2A3mFKLtleIRLi6IIG0smzOlX5CEsisNnc897OUHIOSNLsQQs="; + let signer = minisign_key_to_did(minisign_key).unwrap(); + verify_minisign_identity_proof(&signer, message, minisign_signature).unwrap(); + } +} diff --git a/src/identity/mod.rs b/src/identity/mod.rs index d61f158..92b71c9 100644 --- a/src/identity/mod.rs +++ b/src/identity/mod.rs @@ -2,3 +2,4 @@ pub mod claims; pub mod did; pub mod did_key; pub mod did_pkh; +pub mod minisign; diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index 3b213b9..1839635 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -28,6 +28,11 @@ use crate::identity::{ claims::create_identity_claim, did::Did, did_pkh::DidPkh, + minisign::{ + minisign_key_to_did, + verify_minisign_identity_proof, + IDENTITY_PROOF_MINISIGN, + }, }; use crate::json_signatures::{ canonicalization::canonicalize_object, @@ -305,6 +310,11 @@ async fn get_identity_claim( ); Did::Pkh(did_pkh) }, + "minisign" => { + let did_key = minisign_key_to_did(&query_params.signer) + .map_err(|_| ValidationError("invalid key"))?; + Did::Key(did_key) + }, _ => return Err(ValidationError("unknown proof type").into()), }; let actor_id = current_user.profile.actor_id(&config.instance_url()); @@ -341,32 +351,41 @@ async fn create_identity_proof( .map_err(|_| ValidationError("invalid claim"))?; // Verify proof - let did_pkh = match did { - Did::Key(_) => return Err(ValidationError("unsupported DID type").into()), - Did::Pkh(ref did_pkh) => did_pkh, + let proof_type = match did { + Did::Key(ref did_key) => { + verify_minisign_identity_proof( + did_key, + &message, + &proof_data.signature, + ).map_err(|_| ValidationError("invalid signature"))?; + IDENTITY_PROOF_MINISIGN + }, + Did::Pkh(ref did_pkh) => { + if did_pkh.chain_id != ChainId::ethereum_mainnet() { + // DID must point to Ethereum Mainnet because it is a valid + // identifier on any Ethereum chain + return Err(ValidationError("unsupported chain ID").into()); + }; + let maybe_public_address = + current_user.public_wallet_address(&Currency::Ethereum); + if let Some(address) = maybe_public_address { + // Do not allow to add more than one address proof + if did_pkh.address != address { + return Err(ValidationError("DID doesn't match current identity").into()); + }; + }; + verify_eip191_identity_proof( + did_pkh, + &message, + &proof_data.signature, + )?; + ETHEREUM_EIP191_PROOF + }, }; - if did_pkh.chain_id != ChainId::ethereum_mainnet() { - // DID must point to Ethereum Mainnet because it is a valid - // identifier on any Ethereum chain - return Err(ValidationError("unsupported chain ID").into()); - }; - let maybe_public_address = - current_user.public_wallet_address(&Currency::Ethereum); - if let Some(address) = maybe_public_address { - // Do not allow to add more than one address proof - if did_pkh.address != address { - return Err(ValidationError("DID doesn't match current identity").into()); - }; - }; - verify_eip191_identity_proof( - did_pkh, - &message, - &proof_data.signature, - )?; let proof = IdentityProof { issuer: did, - proof_type: ETHEREUM_EIP191_PROOF.to_string(), + proof_type: proof_type.to_string(), value: proof_data.signature.clone(), }; let mut profile_data = ProfileUpdateData::from(¤t_user.profile);