diff --git a/Cargo.lock b/Cargo.lock index c4c7bb2..6176c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2221,6 +2221,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -3080,8 +3089,10 @@ dependencies = [ name = "did-simple" version = "0.0.0" dependencies = [ + "bs58", "bytes", "eyre", + "hex-literal", "thiserror", ] @@ -4268,6 +4279,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hexasphere" version = "10.0.0" diff --git a/crates/did-simple/Cargo.toml b/crates/did-simple/Cargo.toml index 8918a09..d43c8f6 100644 --- a/crates/did-simple/Cargo.toml +++ b/crates/did-simple/Cargo.toml @@ -11,6 +11,8 @@ publish = false [dependencies] thiserror = "1.0.60" bytes = "1.6.0" +bs58 = "0.5.1" [dev-dependencies] eyre = "0.6.12" +hex-literal = "0.4.1" diff --git a/crates/did-simple/src/methods/key.rs b/crates/did-simple/src/methods/key.rs index 0f40eca..61d5e33 100644 --- a/crates/did-simple/src/methods/key.rs +++ b/crates/did-simple/src/methods/key.rs @@ -9,36 +9,66 @@ use crate::{ utf8bytes::Utf8Bytes, }; +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub enum PubKeyKind {} + /// An implementation of the `did:key` method. See the [module](self) docs for more /// info. #[derive(Debug, Eq, PartialEq, Hash, Clone)] pub struct DidKey { /// The string representation of the DID. s: Utf8Bytes, - /// The substring for method-specific-id. This is a range index into `s`. - method_specific_id: std::ops::RangeFrom, + /// The decoded multibase portion of the DID. + decoded: Vec, } impl DidKey { - const PREFIX: &'static str = "did:key:"; + pub const PREFIX: &'static str = "did:key:"; - /// Gets the buffer representing the uri as a str. + /// Gets the buffer representing the did:key uri as a str. pub fn as_str(&self) -> &str { self.s.as_str() } - /// Gets the buffer representing the uri as a byte slice. + /// Gets the buffer representing the did:key uri as a byte slice. pub fn as_slice(&self) -> &[u8] { self.s.as_slice() } - /// Gets the buffer representing the uri as a reference counted slice that - /// is guaranteed to be utf8. + /// Gets the buffer representing the did:key uri as a reference counted slice + /// that is guaranteed to be utf8. pub fn as_utf8_bytes(&self) -> &Utf8Bytes { &self.s } } +fn decode_multibase( + s: &Utf8Bytes, + out_buf: &mut Vec, +) -> Result<(), MultibaseDecodeError> { + out_buf.clear(); + // did:key only uses base58-btc, so its not actually any arbitrary multibase. + let multibase_part = &s.as_slice()[DidKey::PREFIX.len()..]; + // the first character should always be 'z' + let base = multibase_part[0]; + if base != b'z' { + return Err(MultibaseDecodeError::WrongBase(base)); + } + bs58::decode(&multibase_part[1..]) + .with_alphabet(bs58::Alphabet::BITCOIN) + .onto(out_buf)?; + Ok(()) +} +#[derive(thiserror::Error, Debug)] +pub enum MultibaseDecodeError { + #[error( + "Expected \"base58-btc\" encoding which should be identified in multibase as ascii 'z' (0x7a) but got {0:x}" + )] + WrongBase(u8), + #[error(transparent)] + Bs58(#[from] bs58::decode::Error), +} + impl TryFrom for DidKey { type Error = FromUriError; @@ -53,10 +83,11 @@ impl TryFrom for DidKey { "sanity check that prefix has expected length" ); - Ok(Self { - s: value.as_utf8_bytes().clone(), - method_specific_id: (Self::PREFIX.len()..), - }) + let s = value.as_utf8_bytes().clone(); + let mut decoded = Vec::new(); + decode_multibase(&s, &mut decoded)?; + + Ok(Self { s, decoded }) } } @@ -64,6 +95,8 @@ impl TryFrom for DidKey { pub enum FromUriError { #[error("Expected \"key\" method but got {0:?}")] WrongMethod(DidMethod), + #[error(transparent)] + MultibaseDecode(#[from] MultibaseDecodeError), } impl Display for DidKey { @@ -77,29 +110,62 @@ mod test { use super::*; use eyre::WrapErr; + use hex_literal::hex; use std::str::FromStr; - fn common_examples() -> Vec { - let p = DidKey::PREFIX; - let make_example = |s| DidKey { - s: format!("{p}{s}").into(), - method_specific_id: (p.len()..), - }; - ["deadbeef123", "yeet"] - .into_iter() - .map(make_example) - .collect() + // From: https://w3c-ccg.github.io/did-method-key/#example-5 + fn ed25519_examples() -> &'static [&'static str] { + &[ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG", + "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf", + ] } #[test] fn test_try_from_uri() -> eyre::Result<()> { - for example in common_examples() { - let uri = DidUri::from_str(example.as_str()) + for &example in ed25519_examples() { + let uri = DidUri::from_str(example) .wrap_err_with(|| format!("failed to parse DidUri from {example}"))?; + assert_eq!(example, uri.as_str()); let key_from_uri = DidKey::try_from(uri.clone()) .wrap_err_with(|| format!("failed to parse DidKey from {uri}"))?; - assert_eq!(uri.as_str(), key_from_uri.as_str()); + assert_eq!(example, key_from_uri.as_str()); } Ok(()) } + + #[test] + fn test_decode_multibase() -> eyre::Result<()> { + #[derive(Debug)] + struct Example { + decoded: &'static [u8], + encoded: &'static str, + } + // from: https://datatracker.ietf.org/doc/html/draft-msporny-base58-03#section-5 + let examples = [ + Example { + decoded: b"Hello World!", + encoded: "2NEpo7TZRRrLZSi2U", + }, + Example { + decoded: b"The quick brown fox jumps over the lazy dog.", + encoded: "USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z", + }, + Example { + decoded: &hex!("0000287fb4cd"), + encoded: "11233QC4", + }, + ]; + + let mut buf = Vec::new(); + for e @ Example { decoded, encoded } in examples { + let s = format!("{}z{encoded}", DidKey::PREFIX).into(); + decode_multibase(&s, &mut buf) + .wrap_err_with(|| format!("Failed to decode example {e:?}"))?; + assert_eq!(buf, decoded, "failed comparison in example {e:?}"); + } + + Ok(()) + } }