From 2fc704bd72398c4d3680de205fdec1d999798c8b Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Wed, 8 May 2024 01:50:36 -0400 Subject: [PATCH] Started work on did-simple and did-chain --- Cargo.lock | 32 +++-- Cargo.toml | 2 + crates/did-chain/Cargo.toml | 11 ++ crates/did-chain/src/lib.rs | 12 ++ crates/did-simple/Cargo.toml | 15 +++ crates/did-simple/src/lib.rs | 11 ++ crates/did-simple/src/methods/key.rs | 8 ++ crates/did-simple/src/methods/mod.rs | 10 ++ crates/did-simple/src/methods/web.rs | 8 ++ crates/did-simple/src/uri.rs | 159 ++++++++++++++++++++++ crates/did-simple/src/utf8bytes.rs | 192 +++++++++++++++++++++++++++ 11 files changed, 452 insertions(+), 8 deletions(-) create mode 100644 crates/did-chain/Cargo.toml create mode 100644 crates/did-chain/src/lib.rs create mode 100644 crates/did-simple/Cargo.toml create mode 100644 crates/did-simple/src/lib.rs create mode 100644 crates/did-simple/src/methods/key.rs create mode 100644 crates/did-simple/src/methods/mod.rs create mode 100644 crates/did-simple/src/methods/web.rs create mode 100644 crates/did-simple/src/uri.rs create mode 100644 crates/did-simple/src/utf8bytes.rs diff --git a/Cargo.lock b/Cargo.lock index 44ba0f0..c4c7bb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,9 +2255,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" dependencies = [ "serde", ] @@ -3069,6 +3069,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "did-chain" +version = "0.0.0" +dependencies = [ + "did-simple", +] + +[[package]] +name = "did-simple" +version = "0.0.0" +dependencies = [ + "bytes", + "eyre", + "thiserror", +] + [[package]] name = "digest" version = "0.9.0" @@ -3580,9 +3596,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -7346,18 +7362,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 8390e09..ff087e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ members = [ "apps/social/networking", "apps/social/server", "crates/bevy_egui_keyboard", + "crates/did-chain", + "crates/did-simple", "crates/egui-picking", "crates/picking-xr", "crates/replicate/client", diff --git a/crates/did-chain/Cargo.toml b/crates/did-chain/Cargo.toml new file mode 100644 index 0000000..41a992e --- /dev/null +++ b/crates/did-chain/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "did-chain" +version.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "A chain of Decentralized Identifiers" + +[dependencies] +did-simple.path = "../did-simple" diff --git a/crates/did-chain/src/lib.rs b/crates/did-chain/src/lib.rs new file mode 100644 index 0000000..af23525 --- /dev/null +++ b/crates/did-chain/src/lib.rs @@ -0,0 +1,12 @@ +use did_simple::{methods::key::DidKey, methods::DidDyn}; + +/// This is like an account UUID, it provides a unique identifier for the +/// account. Changing it is impossible. +#[derive(Debug)] +pub struct DidRoot(DidKey); + +#[derive(Debug)] +pub struct DidChain { + pub root: DidRoot, + pub chain: Vec, +} diff --git a/crates/did-simple/Cargo.toml b/crates/did-simple/Cargo.toml new file mode 100644 index 0000000..7ed4ee6 --- /dev/null +++ b/crates/did-simple/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "did-simple" +version.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "Dead simple DIDs" + +[dependencies] +thiserror = "1.0.60" +bytes = "1.6.0" + +[dev-dependencies] +eyre = "0.6.12" diff --git a/crates/did-simple/src/lib.rs b/crates/did-simple/src/lib.rs new file mode 100644 index 0000000..8c99283 --- /dev/null +++ b/crates/did-simple/src/lib.rs @@ -0,0 +1,11 @@ +#![forbid(unsafe_code)] + +use std::str::FromStr; + +pub mod methods; +pub mod uri; +pub mod utf8bytes; + +pub trait Did: FromStr { + fn uri(&self) -> self::uri::DidUri; +} diff --git a/crates/did-simple/src/methods/key.rs b/crates/did-simple/src/methods/key.rs new file mode 100644 index 0000000..6bbd911 --- /dev/null +++ b/crates/did-simple/src/methods/key.rs @@ -0,0 +1,8 @@ +//! An implementation of the [did:key] method. +//! +//! [did:key]: https://w3c-ccg.github.io/did-method-key/ + +/// An implementation of the `did:key` method. See the [module](self) docs for more +/// info. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub struct DidKey; diff --git a/crates/did-simple/src/methods/mod.rs b/crates/did-simple/src/methods/mod.rs new file mode 100644 index 0000000..e47f1df --- /dev/null +++ b/crates/did-simple/src/methods/mod.rs @@ -0,0 +1,10 @@ +pub mod key; +pub mod web; + +/// Dynamically typed did method. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +#[non_exhaustive] +pub enum DidDyn { + Key(self::key::DidKey), + Web(self::web::DidWeb), +} diff --git a/crates/did-simple/src/methods/web.rs b/crates/did-simple/src/methods/web.rs new file mode 100644 index 0000000..05794c0 --- /dev/null +++ b/crates/did-simple/src/methods/web.rs @@ -0,0 +1,8 @@ +//! An implementation of the [did:web] method. +//! +//! [did:web]: https://w3c-ccg.github.io/did-method-web + +/// An implementation of the `did:web` method. See the [module](self) docs for more +/// info. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub struct DidWeb; diff --git a/crates/did-simple/src/uri.rs b/crates/did-simple/src/uri.rs new file mode 100644 index 0000000..cdccce6 --- /dev/null +++ b/crates/did-simple/src/uri.rs @@ -0,0 +1,159 @@ +use std::str::FromStr; + +use bytes::Bytes; + +use crate::utf8bytes::Utf8Bytes; + +#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)] +pub enum DidMethod { + Key, + Web, +} + +impl FromStr for DidMethod { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + Ok(match s { + "key" => Self::Key, + "web" => Self::Web, + "" => return Err(ParseError::MissingMethod), + _ => return Err(ParseError::UnknownMethod), + }) + } +} + +/// Helper type to access data in the method-specific-id of a [`DidUri`]. +pub struct MethodSpecificId<'a>(&'a DidUri); + +impl MethodSpecificId<'_> { + pub fn as_str(&self) -> &str { + &(self.0.as_str()[self.0.method_specific_id.clone()]) + } + + pub fn as_slice(&self) -> &[u8] { + &(self.0.s.as_slice()[self.0.method_specific_id.clone()]) + } + + pub fn utf8_bytes(&self) -> Utf8Bytes { + self.0.s.clone().split_off(self.0.method_specific_id.start) + } +} + +#[derive(Debug, Eq, PartialEq, Hash)] +pub struct DidUri { + method: DidMethod, + /// 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, +} + +impl DidUri { + /// Gets the buffer representing the uri as a str. + pub fn as_str(&self) -> &str { + self.s.as_str() + } + + /// Gets the buffer representing the uri as a byte slice. + pub fn as_slice(&self) -> &[u8] { + self.s.as_slice() + } + + /// Gets the buffer representing the uri as a byte slice that is guaranteed to be utf8. + pub fn utf8_bytes(&self) -> &Utf8Bytes { + &self.s + } + + /// Gets the buffer representing the uri as bytes. + pub fn bytes(&self) -> &Bytes { + self.s.bytes() + } + + /// The method of the did. + pub fn method(&self) -> DidMethod { + self.method + } + + /// Method-specific identity info. + pub fn method_specific_id(&self) -> MethodSpecificId { + MethodSpecificId(self) + } + + pub fn into_inner(self) -> Utf8Bytes { + self.s + } +} + +impl FromStr for DidUri { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let (method, remaining) = s + .strip_prefix("did:") + .ok_or(ParseError::InvalidScheme)? + .split_once(':') + .ok_or(ParseError::MissingMethod)?; + let method = DidMethod::from_str(method)?; + let start_idx = s.len() - remaining.len(); + + Ok(DidUri { + method, + s: Utf8Bytes::from(s.to_owned()), + method_specific_id: (start_idx..), + }) + } +} + +impl TryFrom for DidUri { + type Error = ParseError; + + fn try_from(s: String) -> Result { + let (method, remaining) = s + .strip_prefix("did:") + .ok_or(ParseError::InvalidScheme)? + .split_once(':') + .ok_or(ParseError::MissingMethod)?; + let method = DidMethod::from_str(method)?; + let start_idx = s.len() - remaining.len(); + + Ok(DidUri { + method, + s: Utf8Bytes::from(s), + method_specific_id: (start_idx..), + }) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("expected the did: scheme")] + InvalidScheme, + #[error("expected did:method, but method was not present")] + MissingMethod, + #[error("encountered unknown did:method")] + UnknownMethod, +} + +#[cfg(test)] +mod test { + use super::*; + use eyre::{Result, WrapErr}; + + #[test] + fn test_parse() -> Result<()> { + let test_cases = [DidUri { + method: DidMethod::Key, + s: String::from("did:key:123456").into(), + method_specific_id: (8..), + }]; + for expected in test_cases { + let s = expected.s.as_str().to_owned(); + let from_str = DidUri::from_str(&s).wrap_err("failed to from_str")?; + let try_from = DidUri::try_from(s).wrap_err("failed to try_from")?; + assert_eq!(from_str, try_from); + assert_eq!(from_str, expected); + } + Ok(()) + } +} diff --git a/crates/did-simple/src/utf8bytes.rs b/crates/did-simple/src/utf8bytes.rs new file mode 100644 index 0000000..969b39a --- /dev/null +++ b/crates/did-simple/src/utf8bytes.rs @@ -0,0 +1,192 @@ +use std::fmt::Display; + +use bytes::Bytes; + +/// Wrapper around [`Bytes`] which is guaranteed to be UTF-8. +/// Like `Bytes`, it is cheaply cloneable and facilitates zero-copy. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub struct Utf8Bytes(Bytes); + +impl Utf8Bytes { + pub fn as_str(&self) -> &str { + // TODO: Consider changing to unsafe later, because this check is entirely unecessary. + std::str::from_utf8(self.0.as_ref()).expect("infallible") + } + + pub fn as_slice(&self) -> &[u8] { + self.0.as_ref() + } + + pub fn bytes(&self) -> &Bytes { + &self.0 + } + + pub fn into_inner(self) -> Bytes { + self.0 + } + + /// Same as [`Bytes::split_off()`], but panics if slicing doesn't result in valid utf8. + /// + /// Afterwards `self` contains elements `[0, at)`, and the returned `Utf8Bytes` + /// contains elements `[at, len)`. + pub fn split_off(&mut self, at: usize) -> Self { + assert!( + self.as_str().is_char_boundary(at), + "slicing would have created invalid UTF-8!" + ); + Self(self.0.split_off(at)) + } + + /// Same as [`Bytes::split_to()`], but panics if slicing doesn't result in valid utf8. + /// + /// Afterwards `self` contains elements `[at, len)`, and the returned `Utf8Bytes` + /// contains elements `[0, at)`. + pub fn split_to(&mut self, at: usize) -> Self { + assert!( + self.as_str().is_char_boundary(at), + "slicing would have created invalid UTF-8!" + ); + Self(self.0.split_to(at)) + } +} + +impl AsRef<[u8]> for Utf8Bytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From for Utf8Bytes { + /// This is zero-copy, and skips UTF-8 checks. + fn from(value: String) -> Self { + Self(Bytes::from(value)) + } +} + +impl From<&'static str> for Utf8Bytes { + /// This is zero-copy, and skips UTF-8 checks. + fn from(value: &'static str) -> Self { + Self(Bytes::from_static(value.as_bytes())) + } +} + +impl TryFrom for Utf8Bytes { + type Error = std::str::Utf8Error; + + /// This is zero-copy, and performs UTF-8 checks. + fn try_from(value: Bytes) -> Result { + let _s = std::str::from_utf8(value.as_ref())?; + Ok(Self(value)) + } +} + +impl Display for Utf8Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_str().fmt(f) + } +} + +#[cfg(test)] +mod test { + use super::*; + + const STRINGS: &[&str] = &["foobar", "", "\0", "Yellow ඞ Sus😂"]; + + #[test] + fn test_from_str() { + for &s in STRINGS { + let ub = Utf8Bytes::from(s); + assert_eq!(s, ub.as_str()); + assert_eq!(s.as_bytes(), ub.0); + } + } + + #[test] + fn test_from_string() { + for &s in STRINGS { + let s: String = s.to_owned(); + let ub = Utf8Bytes::from(s.clone()); + assert_eq!(s, ub.as_str()); + assert_eq!(s.as_bytes(), ub.0); + } + } + + #[test] + fn test_from_bytes() { + for &s in STRINGS { + let b: Bytes = Bytes::from(s); + let ub = Utf8Bytes::try_from(s).expect("failed conversion from bytes"); + assert_eq!(s, ub.as_str()); + assert_eq!(s.as_bytes(), ub.0); + assert_eq!(ub.0, b); + } + } + + #[test] + fn test_display() { + for &s in STRINGS { + let ub = Utf8Bytes::from(s); + assert_eq!(s, format!("{ub}")) + } + } + + #[test] + fn test_split_off() { + for &s in STRINGS { + let validity: Vec = + (0..s.len()).map(|idx| s.is_char_boundary(idx)).collect(); + for pos in 0..s.len() { + let mut original = Utf8Bytes::from(s); + let result = std::panic::catch_unwind(move || { + let ret = original.split_off(pos); + (original, ret) + }); + let is_valid = validity[pos]; + + if let Ok((original, ret)) = result { + assert_eq!(&s[0..pos], original.as_str()); + assert_eq!(&s[pos..], ret.as_str()); + assert!( + is_valid, + "split_off did not panic, so {pos} should be valid" + ); + } else { + assert!( + !is_valid, + "split_off panicked, so {pos} should not be valid" + ); + } + } + } + } + + #[test] + fn test_split_to() { + for &s in STRINGS { + let validity: Vec = + (0..s.len()).map(|idx| s.is_char_boundary(idx)).collect(); + for pos in 0..s.len() { + let mut original = Utf8Bytes::from(s); + let result = std::panic::catch_unwind(move || { + let ret = original.split_to(pos); + (original, ret) + }); + let is_valid = validity[pos]; + + if let Ok((original, ret)) = result { + assert_eq!(&s[0..pos], ret.as_str()); + assert_eq!(&s[pos..], original.as_str()); + assert!( + is_valid, + "split_to did not panic, so {pos} should be valid" + ); + } else { + assert!( + !is_valid, + "split_to panicked, so {pos} should not be valid" + ); + } + } + } + } +}