diff --git a/src/functions.rs b/src/functions.rs index 9978a9c9..1f18c23d 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -78,16 +78,13 @@ static REFERENCE_HASH_FIELDS_TO_REMOVE: &[&str] = &["age_ts", "signatures", "uns /// A homeserver signs JSON with a key pair: /// /// ```rust -/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -/// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +/// const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ"; /// -/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -/// let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap(); /// /// // Create an Ed25519 key pair. /// let key_pair = ruma_signatures::Ed25519KeyPair::new( -/// &public_key, -/// &private_key, +/// &document, /// "1".to_string(), // The "version" of the key. /// ).unwrap(); /// @@ -404,16 +401,13 @@ pub fn reference_hash(value: &Value) -> Result { /// # Examples /// /// ```rust -/// const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; -/// const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; +/// const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ"; /// -/// let public_key = base64::decode_config(&PUBLIC_KEY, base64::STANDARD_NO_PAD).unwrap(); -/// let private_key = base64::decode_config(&PRIVATE_KEY, base64::STANDARD_NO_PAD).unwrap(); +/// let document = base64::decode_config(&PKCS8, base64::STANDARD_NO_PAD).unwrap(); /// /// // Create an Ed25519 key pair. /// let key_pair = ruma_signatures::Ed25519KeyPair::new( -/// &public_key, -/// &private_key, +/// &document, /// "1".to_string(), // The "version" of the key. /// ).unwrap(); /// diff --git a/src/keys.rs b/src/keys.rs index 8800831c..bb652ac7 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -5,7 +5,7 @@ use std::{ fmt::{Debug, Formatter, Result as FmtResult}, }; -use ring::signature::Ed25519KeyPair as RingEd25519KeyPair; +use ring::signature::{Ed25519KeyPair as RingEd25519KeyPair, KeyPair as _}; use crate::{signatures::Signature, Algorithm, Error}; @@ -20,13 +20,9 @@ pub trait KeyPair: Sized { } /// An Ed25519 key pair. -#[derive(Clone, PartialEq)] pub struct Ed25519KeyPair { - /// The public key. - public_key: Vec, - - /// The private key. - private_key: Vec, + /// Ring's Keypair type + keypair: RingEd25519KeyPair, /// The version of the key pair. version: String, @@ -37,8 +33,7 @@ impl Ed25519KeyPair { /// /// # Parameters /// - /// * public_key: The public key of the key pair. - /// * private_key: The private key of the key pair. + /// * document: PKCS8-formatted bytes containing the private & public keys. /// * version: The "version" of the key used for this signature. /// Versions are used as an identifier to distinguish signatures generated from different keys /// but using the same algorithm on the same homeserver. @@ -47,29 +42,35 @@ impl Ed25519KeyPair { /// /// Returns an error if the public and private keys provided are invalid for the implementing /// algorithm. - pub fn new(public_key: &[u8], private_key: &[u8], version: String) -> Result { - if let Err(error) = RingEd25519KeyPair::from_seed_and_public_key(private_key, public_key) { - return Err(Error::new(error.to_string())); - } + pub fn new(document: &[u8], version: String) -> Result { + let keypair = RingEd25519KeyPair::from_pkcs8(document) + .map_err(|error| Error::new(error.to_string()))?; - Ok(Self { - public_key: public_key.to_owned(), - private_key: private_key.to_owned(), - version, - }) + Ok(Self { keypair, version }) + } + + /// Generates a new key pair. + /// + /// # Returns + /// + /// Returns a Vec representing a pkcs8-encoded private/public keypair + /// + /// # Errors + /// + /// Returns an error if the generation failed. + pub fn generate() -> Result, Error> { + let document = RingEd25519KeyPair::generate_pkcs8(&ring::rand::SystemRandom::new()) + .map_err(|e| Error::new(e.to_string()))?; + + Ok(document.as_ref().to_vec()) } } impl KeyPair for Ed25519KeyPair { fn sign(&self, message: &[u8]) -> Signature { - // Okay to unwrap because we verified the input in `new`. - let ring_key_pair = - RingEd25519KeyPair::from_seed_and_public_key(&self.private_key, &self.public_key) - .unwrap(); - Signature { algorithm: Algorithm::Ed25519, - signature: ring_key_pair.sign(message).as_ref().to_vec(), + signature: self.keypair.sign(message).as_ref().to_vec(), version: self.version.clone(), } } @@ -79,7 +80,7 @@ impl Debug for Ed25519KeyPair { fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { formatter .debug_struct("Ed25519KeyPair") - .field("public_key", &self.public_key) + .field("public_key", &self.keypair.public_key()) .field("version", &self.version) .finish() } @@ -94,3 +95,13 @@ pub type PublicKeyMap = HashMap; /// /// This is represented as a map from key ID to Base64-encoded signature. pub type PublicKeySet = HashMap; + +#[cfg(test)] +mod tests { + use super::Ed25519KeyPair; + + #[test] + fn generate_key() { + Ed25519KeyPair::generate().unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6908df9b..907e8787 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,14 +181,26 @@ mod test { use std::collections::HashMap; use base64::{decode_config, STANDARD_NO_PAD}; + use ring::signature::{Ed25519KeyPair as RingEd25519KeyPair, KeyPair as _}; use serde_json::{from_str, json, to_string, to_value, Value}; use super::{ canonical_json, hash_and_sign_event, sign_json, verify_event, verify_json, Ed25519KeyPair, }; - const PUBLIC_KEY: &str = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; - const PRIVATE_KEY: &str = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA0"; + const PKCS8: &str = "MFMCAQEwBQYDK2VwBCIEINjozvdfbsGEt6DD+7Uf4PiJ/YvTNXV2mIPc/tA0T+6toSMDIQDdM+tpNzNWQM9NFpfgr4B9S7LHszOrVRp9NfKmeXS3aQ"; + + /// Convenience method for getting the public key as a string + fn public_key_string() -> String { + base64::encode_config( + &RingEd25519KeyPair::from_pkcs8( + &base64::decode_config(PKCS8, STANDARD_NO_PAD).unwrap(), + ) + .unwrap() + .public_key(), + STANDARD_NO_PAD, + ) + } /// Convenience for converting a string of JSON into its canonical form. fn test_canonical_json(input: &str) -> String { @@ -292,12 +304,7 @@ mod test { #[test] fn sign_empty_json() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -308,16 +315,16 @@ mod test { assert_eq!( to_string(&value).unwrap(), - r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"# + r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"# ); } #[test] fn verify_empty_json() { - let value = from_str(r#"{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}"#).unwrap(); + let value = from_str(r#"{"signatures":{"domain":{"ed25519:1":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}}"#).unwrap(); let mut signature_set = HashMap::new(); - signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + signature_set.insert("ed25519:1".to_string(), public_key_string()); let mut public_key_map = HashMap::new(); public_key_map.insert("domain".to_string(), signature_set); @@ -328,12 +335,7 @@ mod test { #[test] fn sign_minimal_json() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -353,7 +355,7 @@ mod test { assert_eq!( to_string(&alpha_value).unwrap(), - r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"# ); let mut reverse_alpha_value = @@ -362,18 +364,18 @@ mod test { assert_eq!( to_string(&reverse_alpha_value).unwrap(), - r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"# ); } #[test] fn verify_minimal_json() { let value = from_str( - r#"{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}"# + r#"{"one":1,"signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"two":"Two"}"# ).unwrap(); let mut signature_set = HashMap::new(); - signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + signature_set.insert("ed25519:1".to_string(), public_key_string()); let mut public_key_map = HashMap::new(); public_key_map.insert("domain".to_string(), signature_set); @@ -381,7 +383,7 @@ mod test { assert!(verify_json(&public_key_map, &value).is_ok()); let reverse_value = from_str( - r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"one":1}"# + r#"{"two":"Two","signatures":{"domain":{"ed25519:1":"t6Ehmh6XTDz7qNWI0QI5tNPSliWLPQP/+Fzz3LpdCS7q1k2G2/5b5Embs2j4uG3ZeivejrzqSVoBcdocRpa+AQ"}},"one":1}"# ).unwrap(); assert!(verify_json(&public_key_map, &reverse_value).is_ok()); @@ -389,10 +391,10 @@ mod test { #[test] fn fail_verify_json() { - let value = from_str(r#"{"not":"empty","signatures":{"domain":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}"#).unwrap(); + let value = from_str(r#"{"not":"empty","signatures":{"domain":"lXjsnvhVlz8t3etR+6AEJ0IT70WujeHC1CFjDDsVx0xSig1Bx7lvoi1x3j/2/GPNjQM4a2gD34UqsXFluaQEBA"}}"#).unwrap(); let mut signature_set = HashMap::new(); - signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + signature_set.insert("ed25519:1".to_string(), public_key_string()); let mut public_key_map = HashMap::new(); public_key_map.insert("domain".to_string(), signature_set); @@ -403,12 +405,7 @@ mod test { #[test] fn sign_minimal_event() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -435,19 +432,14 @@ mod test { assert_eq!( to_string(&value).unwrap(), - r#"{"auth_events":[],"content":{},"depth":3,"hashes":{"sha256":"5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"},"origin":"domain","origin_server_ts":1000000,"prev_events":[],"room_id":"!x:domain","sender":"@a:domain","signatures":{"domain":{"ed25519:1":"KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg"}},"type":"X","unsigned":{"age_ts":1000000}}"# + r#"{"auth_events":[],"content":{},"depth":3,"hashes":{"sha256":"5jM4wQpv6lnBo7CLIghJuHdW+s2CMBJPUOGOC89ncos"},"origin":"domain","origin_server_ts":1000000,"prev_events":[],"room_id":"!x:domain","sender":"@a:domain","signatures":{"domain":{"ed25519:1":"PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw"}},"type":"X","unsigned":{"age_ts":1000000}}"# ); } #[test] fn sign_redacted_event() { let key_pair = Ed25519KeyPair::new( - decode_config(&PUBLIC_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), - decode_config(&PRIVATE_KEY, STANDARD_NO_PAD) - .unwrap() - .as_slice(), + decode_config(&PKCS8, STANDARD_NO_PAD).unwrap().as_slice(), "1".to_string(), ) .unwrap(); @@ -473,14 +465,14 @@ mod test { assert_eq!( to_string(&value).unwrap(), - r#"{"content":{"body":"Here is the message content"},"event_id":"$0:domain","hashes":{"sha256":"onLKD1bGljeBWQhWZ1kaP9SorVmRQNdN5aM2JYU2n/g"},"origin":"domain","origin_server_ts":1000000,"room_id":"!r:domain","sender":"@u:domain","signatures":{"domain":{"ed25519:1":"Wm+VzmOUOz08Ds+0NTWb1d4CZrVsJSikkeRxh6aCcUwu6pNC78FunoD7KNWzqFn241eYHYMGCA5McEiVPdhzBA"}},"type":"m.room.message","unsigned":{"age_ts":1000000}}"# + r#"{"content":{"body":"Here is the message content"},"event_id":"$0:domain","hashes":{"sha256":"onLKD1bGljeBWQhWZ1kaP9SorVmRQNdN5aM2JYU2n/g"},"origin":"domain","origin_server_ts":1000000,"room_id":"!r:domain","sender":"@u:domain","signatures":{"domain":{"ed25519:1":"D2V+qWBJssVuK/pEUJtwaYMdww2q1fP4PRCo226ChlLz8u8AWmQdLKes19NMjs/X0Hv0HIjU0c1TDKFMtGuoCA"}},"type":"m.room.message","unsigned":{"age_ts":1000000}}"# ); } #[test] fn verify_minimal_event() { let mut signature_set = HashMap::new(); - signature_set.insert("ed25519:1".to_string(), PUBLIC_KEY.to_string()); + signature_set.insert("ed25519:1".to_string(), public_key_string()); let mut public_key_map = HashMap::new(); public_key_map.insert("domain".to_string(), signature_set); @@ -500,7 +492,7 @@ mod test { "sender": "@a:domain", "signatures": { "domain": { - "ed25519:1": "KxwGjPSDEtvnFgU00fwFz+l6d2pJM6XBIaMEn81SXPTRl16AqLAYqfIReFGZlHi5KLjAWbOoMszkwsQma+lYAg" + "ed25519:1": "PxOFMn6ORll8PFSQp0IRF6037MEZt3Mfzu/ROiT/gb/ccs1G+f6Ddoswez4KntLPBI3GKCGIkhctiK37JOy2Aw" } }, "type": "X",