diff --git a/radicle/src/crypto.rs b/radicle/src/crypto.rs index ecee7d43..6ecfda0f 100644 --- a/radicle/src/crypto.rs +++ b/radicle/src/crypto.rs @@ -131,6 +131,8 @@ pub enum PublicKeyError { InvalidLength(usize), #[error("invalid multibase string: {0}")] Multibase(#[from] multibase::Error), + #[error("invalid multicodec prefix, expected {0:?}")] + Multicodec([u8; 2]), #[error("invalid key: {0}")] InvalidKey(#[from] ed25519::Error), } @@ -198,8 +200,21 @@ impl TryFrom<&[u8]> for PublicKey { } impl PublicKey { + /// Multicodec key type for Ed25519 keys. + pub const MULTICODEC_TYPE: [u8; 2] = [0xED, 0x1]; + + /// Encode public key in human-readable format. + /// + /// We use the format specified by the DID `key` method, which is described as: + /// + /// `did:key:MULTIBASE(base58-btc, MULTICODEC(public-key-type, raw-public-key-bytes))` + /// pub fn to_human(&self) -> String { - multibase::encode(multibase::Base::Base58Btc, self.0.deref()) + let mut buf = [0; 2 + ed25519::PublicKey::BYTES]; + buf[..2].copy_from_slice(&Self::MULTICODEC_TYPE); + buf[2..].copy_from_slice(self.0.deref()); + + multibase::encode(multibase::Base::Base58Btc, &buf) } } @@ -208,12 +223,14 @@ impl FromStr for PublicKey { fn from_str(s: &str) -> Result { let (_, bytes) = multibase::decode(s)?; - let array: [u8; 32] = bytes - .try_into() - .map_err(|v: Vec| PublicKeyError::InvalidLength(v.len()))?; - let key = ed25519::PublicKey::new(array); - Ok(Self(key)) + if let Some(bytes) = bytes.strip_prefix(&Self::MULTICODEC_TYPE) { + let key = ed25519::PublicKey::from_slice(bytes)?; + + Ok(Self(key)) + } else { + Err(PublicKeyError::Multicodec(Self::MULTICODEC_TYPE)) + } } } @@ -246,4 +263,12 @@ mod test { assert_eq!(input, decoded); } + + #[test] + fn test_encode_decode() { + let input = "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; + let key = PublicKey::from_str(input).unwrap(); + + assert_eq!(key.to_string(), input); + } } diff --git a/radicle/src/identity.rs b/radicle/src/identity.rs index 8bff7dbb..cacdc897 100644 --- a/radicle/src/identity.rs +++ b/radicle/src/identity.rs @@ -117,4 +117,19 @@ mod test { let json = serde_json::to_string(&did).unwrap(); assert_eq!(format!("\"{}\"", did), json); } + + #[test] + fn test_did_encode_decode() { + let input = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; + let Did(key) = Did::decode(input).unwrap(); + + assert_eq!(Did::from(key).encode(), input); + } + + #[test] + fn test_did_vectors() { + Did::decode("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp").unwrap(); + Did::decode("did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG").unwrap(); + Did::decode("did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf").unwrap(); + } } diff --git a/radicle/src/ssh.rs b/radicle/src/ssh.rs index 2dcd2727..483db5fe 100644 --- a/radicle/src/ssh.rs +++ b/radicle/src/ssh.rs @@ -53,7 +53,8 @@ pub mod fmt { #[test] fn test_key() { - let pk = PublicKey::from_str("zF4VJZgNEeL1niWmKu1NtT1B4ZyGpjACyhs2VEZvtsws5").unwrap(); + let pk = + PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); assert_eq!( key(&pk), @@ -63,8 +64,8 @@ pub mod fmt { #[test] fn test_fingerprint() { - let pk = PublicKey::from_str("zF4VJZgNEeL1niWmKu1NtT1B4ZyGpjACyhs2VEZvtsws5").unwrap(); - + let pk = + PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); assert_eq!( fingerprint(&pk), "SHA256:gE/Ty4fuXzww49lcnNe9/GI0L7xSEQdFp/v9tOjFwB4" @@ -360,7 +361,7 @@ mod test { fn test_agent_encoding_remove() { use std::str::FromStr; - let pk = PublicKey::from_str("zF4VJZgNEeL1niWmKu1NtT1B4ZyGpjACyhs2VEZvtsws5").unwrap(); + let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); let expected = [ 0, 0, 0, 56, // Message length 18, // Message type (remove identity) @@ -387,7 +388,7 @@ mod test { fn test_agent_encoding_sign() { use std::str::FromStr; - let pk = PublicKey::from_str("zF4VJZgNEeL1niWmKu1NtT1B4ZyGpjACyhs2VEZvtsws5").unwrap(); + let pk = PublicKey::from_str("z6MktWkM9vcfysWFq1c2aaLjJ6j4PYYg93TLPswR4qtuoAeT").unwrap(); let expected = [ 0, 0, 0, 73, // Message length 13, // Message type (sign request)