diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 548743c2ed..27747273e7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -19,18 +19,12 @@ jobs: matrix: project: [ - identity_account, - identity_common, - identity_communication, identity_core, identity_crypto, - identity_integration, - identity_proof, - identity_resolver, - identity_schema, - identity_vc, identity_derive, identity_diff, + identity_iota, + identity_proof, ] os: [ubuntu-latest, windows-latest] diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 1078c0c60f..437f32a745 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -19,18 +19,12 @@ jobs: matrix: project: [ - identity_account, - identity_common, - identity_communication, identity_core, identity_crypto, - identity_integration, - identity_proof, - identity_resolver, - identity_schema, - identity_vc, identity_derive, identity_diff, + identity_iota, + identity_proof, ] steps: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 71fb70f83e..a5b095f513 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -19,18 +19,12 @@ jobs: matrix: project: [ - identity_account, - identity_common, - identity_communication, identity_core, identity_crypto, - identity_integration, - identity_proof, - identity_resolver, - identity_schema, - identity_vc, identity_derive, identity_diff, + identity_iota, + identity_proof, ] steps: diff --git a/Cargo.toml b/Cargo.toml index 4a7d639ac7..feaa4140fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,9 @@ [workspace] members = [ - "identity_account", - "identity_communication", - "identity_resolver", - "identity_schema", - "identity_common", "identity_core", "identity_crypto", - "identity_integration", - "identity_proof", - "identity_vc", + "identity_derive", "identity_diff", - "identity_derive" + "identity_iota", + "identity_proof", ] diff --git a/identity_account/Cargo.toml b/identity_account/Cargo.toml deleted file mode 100644 index 6acd03a1a6..0000000000 --- a/identity_account/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "identity_account" -version = "0.1.0" -authors = ["IOTA Identity"] -edition = "2018" -description = "An account library that links to stronghold for DID" -readme = "../README.md" -repository = "https://github.com/iotaledger/identity.rs" -license = "Apache-2.0" -keywords = ["iota", "tangle", "identity"] -homepage = "https://www.iota.org" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/identity_account/src/lib.rs b/identity_account/src/lib.rs deleted file mode 100644 index 31e1bb209f..0000000000 --- a/identity_account/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/identity_common/Cargo.toml b/identity_common/Cargo.toml deleted file mode 100644 index 47e3136a99..0000000000 --- a/identity_common/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "identity_common" -version = "0.1.0" -authors = ["IOTA Identity"] -edition = "2018" -description = "Common types and traits for Identity libraries." -readme = "../README.md" -repository = "https://github.com/iotaledger/identity.rs" -license = "Apache-2.0" -keywords = ["iota", "tangle", "identity"] -homepage = "https://www.iota.org" - -[dependencies] -anyhow = "1.0" -chrono = { version = "0.4", features = ["serde"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", features = ["preserve_order"] } -thiserror = "1.0" diff --git a/identity_common/src/error.rs b/identity_common/src/error.rs deleted file mode 100644 index 9573b6ef7e..0000000000 --- a/identity_common/src/error.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Failed to encode JSON: {0}")] - EncodeJSON(serde_json::Error), - #[error("Failed to decode JSON: {0}")] - DecodeJSON(serde_json::Error), - #[error("Invalid Timestamp: {0}")] - InvalidTimestamp(#[from] chrono::ParseError), -} - -pub type Result = std::result::Result; diff --git a/identity_common/src/macros.rs b/identity_common/src/macros.rs deleted file mode 100644 index 476983fba9..0000000000 --- a/identity_common/src/macros.rs +++ /dev/null @@ -1,133 +0,0 @@ -#[macro_export] -macro_rules! object { - () => { - $crate::object::Object::default() - }; - ($($key:ident : $value:expr),* $(,)*) => { - { - let mut object = ::std::collections::HashMap::new(); - - $( - object.insert( - stringify!($key).to_string(), - $crate::value::Value::from($value), - ); - )* - - $crate::object::Object::from(object) - } - }; -} - -// create a line error with the file and the line number. Good for debugging. -#[macro_export] -macro_rules! line_error { - () => { - concat!("Error at ", file!(), ":", line!()) - }; - ($string:expr) => { - concat!($string, " @", file!(), ":", line!()) - }; -} - -// Creates a constructor function for an error enum -#[macro_export] -macro_rules! impl_error_ctor { - ($fn:ident, $ident:ident, Into<$ty:ty>) => { - pub fn $fn(inner: impl Into<$ty>) -> Self { - Self::$ident(inner.into()) - } - }; - ($fn:ident, $ident:ident, $ty:ty) => { - pub fn $fn(inner: $ty) -> Self { - Self::$ident(inner) - } - }; -} - -/// creates a simple HashMap using map! { "key" => "val", .. } -#[allow(unused_macros)] -#[macro_export] -macro_rules! map { - ($($key:expr => $val:expr),* $(,)?) => {{ - let mut map = HashMap::new(); - $( map.insert($key, $val); )* - map - }} -} - -/// Creates a simple HashSet using set! {"val_1", "val_2", ...}; -#[allow(unused_macros)] -#[macro_export] -macro_rules! set { - ($($val:expr),* $(,)?) => {{ #[allow(redundant_semicolons)] { - let mut set = HashSet::new(); - $( set.insert($val); )* ; - set - }}} -} - -#[macro_export] -macro_rules! impl_builder_setter { - ($fn:ident, $field:ident, Option<$ty:ty>) => { - impl_builder_setter!(@impl $fn, $field, $ty, Option); - }; - ($fn:ident, $field:ident, Vec<$ty:ty>) => { - impl_builder_setter!(@impl $fn, $field, $ty, Vec); - }; - ($fn:ident, $field:ident, $ty:ty) => { - impl_builder_setter!(@impl $fn, $field, $ty, None); - }; - (@impl $fn:ident, $field:ident, $inner:ty, $outer:ident) => { - pub fn $fn(mut self, value: impl Into<$inner>) -> Self { - impl_builder_setter!(@expr self, $field, value, $outer); - self - } - }; - (@expr $self:ident, $field:ident, $value:expr, Option) => { - $self.$field = Some($value.into()); - }; - (@expr $self:ident, $field:ident, $value:expr, Vec) => { - $self.$field.push($value.into()); - }; - (@expr $self:ident, $field:ident, $value:expr, None) => { - $self.$field = $value.into(); - }; -} - -#[macro_export] -macro_rules! impl_builder_try_setter { - ($fn:ident, $field:ident, Option<$ty:ty>) => { - impl_builder_try_setter!(@impl $fn, $field, $ty, Option); - }; - - ($fn:ident, $field:ident, Vec<$ty:ty>) => { - impl_builder_try_setter!(@impl $fn, $field, $ty, Vec); - }; - - ($fn:ident, $field:ident, $ty:ty) => { - impl_builder_try_setter!(@impl $fn, $field, $ty, None); - }; - (@impl $fn:ident, $field:ident, $inner:ty, $outer:ident) => { - pub fn $fn(mut self, value: T) -> ::std::result::Result - where - T: ::std::convert::TryInto<$inner> - { - value.try_into() - .map(|value| { - impl_builder_try_setter!(@expr self, $field, value, $outer); - self - }) - .map_err(Into::into) - } - }; - (@expr $self:ident, $field:ident, $value:expr, Option) => { - $self.$field = Some($value); - }; - (@expr $self:ident, $field:ident, $value:expr, Vec) => { - $self.$field.push($value); - }; - (@expr $self:ident, $field:ident, $value:expr, None) => { - $self.$field = $value; - }; -} diff --git a/identity_common/src/object.rs b/identity_common/src/object.rs deleted file mode 100644 index 9719838e3a..0000000000 --- a/identity_common/src/object.rs +++ /dev/null @@ -1,83 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Map; -use std::{ - collections::HashMap, - fmt, - iter::FromIterator, - ops::{Deref, DerefMut}, -}; - -use crate::value::Value; - -type Inner = HashMap; - -// An String -> Value `HashMap` wrapper -#[derive(Clone, Default, PartialEq, Deserialize, Serialize)] -#[repr(transparent)] -#[serde(transparent)] -pub struct Object(Inner); - -impl Object { - pub fn into_inner(self) -> Inner { - self.0 - } -} - -impl fmt::Debug for Object { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl Deref for Object { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Object { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From for Object { - fn from(other: Inner) -> Self { - Self(other) - } -} - -impl From for Inner { - fn from(other: Object) -> Self { - other.into_inner() - } -} - -impl From> for Object { - fn from(other: Map) -> Self { - Self::from_iter(other.into_iter()) - } -} - -impl From for Map { - fn from(other: Object) -> Self { - Self::from_iter(other.into_inner().into_iter()) - } -} - -impl From for Value { - fn from(other: Object) -> Self { - Value::Object(other.into()) - } -} - -impl FromIterator<(String, Value)> for Object { - fn from_iter(iter: T) -> Self - where - T: IntoIterator, - { - Self(Inner::from_iter(iter)) - } -} diff --git a/identity_common/src/one_or_many.rs b/identity_common/src/one_or_many.rs deleted file mode 100644 index 3fc57b6154..0000000000 --- a/identity_common/src/one_or_many.rs +++ /dev/null @@ -1,120 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt; - -/// A generic container that stores one or many values of a given type. -#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] -#[serde(untagged)] -pub enum OneOrMany { - One(T), - Many(Vec), -} - -impl OneOrMany { - /// Returns the number of elements in the collection - pub fn len(&self) -> usize { - match self { - Self::One(_) => 1, - Self::Many(inner) => inner.len(), - } - } - - /// Returns `true` if the collection is empty - pub fn is_empty(&self) -> bool { - match self { - Self::One(_) => false, - Self::Many(inner) => inner.is_empty(), - } - } - - /// Returns a reference to the element at the given index. - pub fn get(&self, index: usize) -> Option<&T> { - match self { - Self::One(inner) if index == 0 => Some(inner), - Self::One(_) => None, - Self::Many(inner) => inner.get(index), - } - } - - /// Returns `true` if the given value is represented in the collection. - pub fn contains(&self, value: &T) -> bool - where - T: PartialEq, - { - match self { - Self::One(inner) => inner == value, - Self::Many(inner) => inner.contains(value), - } - } - - pub fn iter(&self) -> impl Iterator + '_ { - OneOrManyIter::new(self) - } - - /// Consumes the `OneOrMany`, returning the contents as a `Vec`. - pub fn into_vec(self) -> Vec { - match self { - Self::One(inner) => vec![inner], - Self::Many(inner) => inner, - } - } -} - -impl fmt::Debug for OneOrMany -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::One(inner) => fmt::Debug::fmt(inner, f), - Self::Many(inner) => fmt::Debug::fmt(inner, f), - } - } -} - -impl Default for OneOrMany { - fn default() -> Self { - Self::Many(Vec::new()) - } -} - -impl From for OneOrMany { - fn from(other: T) -> Self { - Self::One(other) - } -} - -impl From> for OneOrMany { - fn from(mut other: Vec) -> Self { - if other.len() == 1 { - Self::One(other.pop().expect("infallible")) - } else { - Self::Many(other) - } - } -} - -impl From> for Vec { - fn from(other: OneOrMany) -> Self { - other.into_vec() - } -} - -struct OneOrManyIter<'a, T> { - inner: &'a OneOrMany, - index: usize, -} - -impl<'a, T> OneOrManyIter<'a, T> { - pub fn new(inner: &'a OneOrMany) -> Self { - Self { inner, index: 0 } - } -} - -impl<'a, T> Iterator for OneOrManyIter<'a, T> { - type Item = &'a T; - - fn next(&mut self) -> Option { - self.index += 1; - self.inner.get(self.index - 1) - } -} diff --git a/identity_common/src/uri.rs b/identity_common/src/uri.rs deleted file mode 100644 index 6d4220c2de..0000000000 --- a/identity_common/src/uri.rs +++ /dev/null @@ -1,51 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{fmt, ops::Deref}; - -/// A simple wrapper for URIs adhering to RFC 3986 -/// -/// TODO: Parse/Validate according to RFC 3986 -#[derive(Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] -#[repr(transparent)] -#[serde(transparent)] -pub struct Uri(String); - -impl Uri { - pub fn into_inner(self) -> String { - self.0 - } -} - -impl fmt::Debug for Uri { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Uri({:?})", self.0) - } -} - -impl Deref for Uri { - type Target = String; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From<&'_ str> for Uri { - fn from(other: &'_ str) -> Self { - Self(other.into()) - } -} - -impl From for Uri { - fn from(other: String) -> Self { - Self(other) - } -} - -impl PartialEq for Uri -where - T: AsRef + ?Sized, -{ - fn eq(&self, other: &T) -> bool { - self.0.eq(other.as_ref()) - } -} diff --git a/identity_communication/Cargo.toml b/identity_communication/Cargo.toml deleted file mode 100644 index 9a95dd0cfd..0000000000 --- a/identity_communication/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "identity_communication" -version = "0.1.0" -authors = ["IOTA Identity"] -edition = "2018" -description = "A DID communication library" -readme = "../README.md" -repository = "https://github.com/iotaledger/identity.rs" -license = "Apache-2.0" -keywords = ["iota", "tangle", "identity"] -homepage = "https://www.iota.org" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/identity_communication/src/lib.rs b/identity_communication/src/lib.rs deleted file mode 100644 index 31e1bb209f..0000000000 --- a/identity_communication/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index 5d91be410f..f5075ddef2 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -17,20 +17,26 @@ homepage = "https://www.iota.org" thiserror = "1.0" anyhow = "1.0" +async-trait = { version = "0.1", default-features = false } +base64 = { version = "0.12", default-features = false, features = ["std"] } +bs58 = { version = "0.3", default-features = false, features = ["std"] } +chrono = { version = "0.4", features = ["serde"] } +derive_builder = { version = "0.9", default-features = false } +hex = { version = "0.4", default-features = false , features = ["std"] } +percent-encoding = { version = "2.1", default-features = false } +url = { version = "2.1", default-features = false, features = ["serde"] } + # serialization -serde = {version = "1.0", features = ["derive"]} +serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } -serde_cbor = "0.11" -# diff macro and trait -identity_diff = {path = "../identity_diff", version = "0.1.0", features = ["diff_derive"]} +identity_crypto = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/identity-signature-suites" } +identity_diff = { path = "../identity_diff", version = "0.1.0", features = ["diff_derive"] } # parser crates pest = "2.1" pest_derive = "2.1" -identity_common = { path = "../identity_common" } - [dev-dependencies] # generative/property testing proptest = "0.10" diff --git a/identity_vc/src/common/context.rs b/identity_core/src/common/context.rs similarity index 58% rename from identity_vc/src/common/context.rs rename to identity_core/src/common/context.rs index bc66784d15..b04cbbfee2 100644 --- a/identity_vc/src/common/context.rs +++ b/identity_core/src/common/context.rs @@ -1,39 +1,43 @@ -use identity_common::{Object, Uri}; +use core::fmt; +use identity_diff::Diff; use serde::{Deserialize, Serialize}; -use std::fmt; + +use crate::common::{Object, Url}; /// A reference to a JSON-LD context -#[derive(Clone, PartialEq, Deserialize, Serialize)] +/// +/// [More Info](https://www.w3.org/TR/vc-data-model/#contexts) +#[derive(Clone, PartialEq, Deserialize, Serialize, Diff)] #[serde(untagged)] pub enum Context { - Uri(Uri), + Url(Url), Obj(Object), } impl fmt::Debug for Context { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::Uri(inner) => fmt::Debug::fmt(inner, f), + Self::Url(inner) => fmt::Debug::fmt(inner, f), Self::Obj(inner) => fmt::Debug::fmt(inner, f), } } } -impl From for Context { - fn from(other: Uri) -> Self { - Self::Uri(other) +impl From for Context { + fn from(other: Url) -> Self { + Self::Url(other) } } impl From<&'_ str> for Context { fn from(other: &'_ str) -> Self { - Self::Uri(other.into()) + Self::Url(other.into()) } } impl From for Context { fn from(other: String) -> Self { - Self::Uri(other.into()) + Self::Url(other.into()) } } diff --git a/identity_common/src/convert/as_json.rs b/identity_core/src/common/convert/as_json.rs similarity index 77% rename from identity_common/src/convert/as_json.rs rename to identity_core/src/common/convert/as_json.rs index 180a050d55..6477d51785 100644 --- a/identity_common/src/convert/as_json.rs +++ b/identity_core/src/common/convert/as_json.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use serde_json::{from_str, to_string, to_string_pretty}; +use serde_json::{from_str, to_string, to_string_pretty, to_vec}; use crate::error::{Error, Result}; @@ -12,6 +12,10 @@ pub trait AsJson: for<'de> Deserialize<'de> + Serialize + Sized { to_string(self).map_err(Error::EncodeJSON) } + fn to_json_vec(&self) -> Result> { + to_vec(self).map_err(Error::EncodeJSON) + } + fn to_json_pretty(&self) -> Result { to_string_pretty(self).map_err(Error::EncodeJSON) } diff --git a/identity_common/src/convert/mod.rs b/identity_core/src/common/convert/mod.rs similarity index 100% rename from identity_common/src/convert/mod.rs rename to identity_core/src/common/convert/mod.rs diff --git a/identity_common/src/convert/serde_into.rs b/identity_core/src/common/convert/serde_into.rs similarity index 100% rename from identity_common/src/convert/serde_into.rs rename to identity_core/src/common/convert/serde_into.rs diff --git a/identity_core/src/common/macros.rs b/identity_core/src/common/macros.rs new file mode 100644 index 0000000000..a6365604a2 --- /dev/null +++ b/identity_core/src/common/macros.rs @@ -0,0 +1,68 @@ +#[macro_export] +macro_rules! object { + () => { + $crate::common::Object::default() + }; + ($($key:ident : $value:expr),* $(,)*) => { + { + let mut object = ::std::collections::HashMap::new(); + + $( + object.insert( + stringify!($key).to_string(), + $crate::common::Value::from($value), + ); + )* + + $crate::common::Object::from(object) + } + }; +} + +// create a line error with the file and the line number. Good for debugging. +#[macro_export] +macro_rules! line_error { + () => { + concat!("Error at ", file!(), ":", line!()) + }; + ($string:expr) => { + concat!($string, " @", file!(), ":", line!()) + }; +} + +// Creates a constructor function for an error enum +#[macro_export] +macro_rules! impl_error_ctor { + ($fn:ident, $ident:ident, Into<$ty:ty>) => { + pub fn $fn(inner: impl Into<$ty>) -> Self { + Self::$ident(inner.into()) + } + }; + ($fn:ident, $ident:ident, $ty:ty) => { + pub fn $fn(inner: $ty) -> Self { + Self::$ident(inner) + } + }; +} + +/// creates a simple HashMap using map! { "key" => "val", .. } +#[allow(unused_macros)] +#[macro_export] +macro_rules! map { + ($($key:expr => $val:expr),* $(,)?) => {{ + let mut map = HashMap::new(); + $( map.insert($key, $val); )* + map + }} +} + +/// Creates a simple HashSet using set! {"val_1", "val_2", ...}; +#[allow(unused_macros)] +#[macro_export] +macro_rules! set { + ($($val:expr),* $(,)?) => {{ #[allow(redundant_semicolons)] { + let mut set = HashSet::new(); + $( set.insert($val); )* ; + set + }}} +} diff --git a/identity_common/src/lib.rs b/identity_core/src/common/mod.rs similarity index 76% rename from identity_common/src/lib.rs rename to identity_core/src/common/mod.rs index dc818f60b7..b7624ba60d 100644 --- a/identity_common/src/lib.rs +++ b/identity_core/src/common/mod.rs @@ -1,18 +1,18 @@ #[macro_use] mod macros; +pub mod context; pub mod convert; -pub mod error; pub mod object; pub mod one_or_many; pub mod timestamp; -pub mod uri; +pub mod url; pub mod value; +pub use self::url::Url; +pub use context::Context; pub use convert::{AsJson, SerdeInto}; -pub use error::{Error, Result}; pub use object::Object; pub use one_or_many::OneOrMany; pub use timestamp::Timestamp; -pub use uri::Uri; pub use value::Value; diff --git a/identity_core/src/common/object.rs b/identity_core/src/common/object.rs new file mode 100644 index 0000000000..3bb3d7d156 --- /dev/null +++ b/identity_core/src/common/object.rs @@ -0,0 +1,230 @@ +use core::{ + fmt, + iter::FromIterator, + ops::{Deref, DerefMut}, +}; +use identity_diff::{ + self as diff, + hashmap::{DiffHashMap, InnerValue}, + Diff, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Map; + +use crate::{ + common::{OneOrMany, Value}, + error::{Error, Result}, +}; + +type Inner = Map; + +// An String -> Value `HashMap` wrapper +#[derive(Clone, Default, PartialEq, Deserialize, Serialize)] +#[repr(transparent)] +#[serde(transparent)] +pub struct Object(Inner); + +impl Object { + pub fn new() -> Self { + Self(Map::new()) + } + + pub fn into_inner(self) -> Inner { + self.0 + } + + pub fn take_object_id(&mut self) -> Option { + match self.0.remove("id") { + Some(Value::String(id)) => Some(id), + Some(_) | None => None, + } + } + + pub fn try_take_object_id(&mut self) -> Result { + self.take_object_id().ok_or(Error::InvalidObjectId) + } + + pub fn take_object_type(&mut self) -> Option { + match self.0.remove("type") { + Some(Value::String(value)) => Some(value), + Some(_) | None => None, + } + } + + pub fn try_take_object_type(&mut self) -> Result { + self.take_object_type().ok_or(Error::InvalidObjectType) + } + + pub fn take_object_types(&mut self) -> Option> { + match self.remove("type") { + Some(Value::String(value)) => Some(value.into()), + Some(Value::Array(values)) => Some(Self::collect_types(values)), + Some(_) | None => None, + } + } + + pub fn try_take_object_types(&mut self) -> Result> { + self.take_object_types().ok_or(Error::InvalidObjectType) + } + + fn collect_types(values: Vec) -> OneOrMany { + let mut types: Vec = Vec::with_capacity(values.len()); + + for value in values { + if let Value::String(value) = value { + types.push(value); + } + } + + types.into() + } +} + +impl fmt::Debug for Object { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl Deref for Object { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Object { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Inner { + fn from(other: Object) -> Self { + other.into_inner() + } +} + +impl From for Object +where + I: IntoIterator, + T: Into, +{ + fn from(other: I) -> Self { + Self::from_iter(other) + } +} + +impl From for Value { + fn from(other: Object) -> Self { + Value::Object(other.into()) + } +} + +impl FromIterator<(String, T)> for Object +where + T: Into, +{ + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + let inner: Inner = iter.into_iter().map(|(key, value)| (key, value.into())).collect(); + + Self(inner) + } +} + +impl Diff for Object { + type Type = DiffHashMap; + + fn diff(&self, other: &Self) -> diff::Result { + use std::collections::HashSet; + + let old: HashSet<&String> = self.keys().collect(); + let new: HashSet<&String> = other.keys().collect(); + let changed_keys = old.intersection(&new).filter(|key| self[**key] != other[**key]); + + let removed_keys = old.difference(&new); + let added_keys = new.difference(&old); + + let mut changes: Vec> = Vec::new(); + + for key in changed_keys { + changes.push(InnerValue::Change { + key: key.to_string(), + value: self[*key].diff(&other[*key])?, + }); + } + + for key in added_keys { + changes.push(InnerValue::Add { + key: key.to_string(), + value: other[*key].clone().into_diff()?, + }); + } + + for key in removed_keys { + changes.push(InnerValue::Remove { key: key.to_string() }); + } + + if changes.is_empty() { + Ok(DiffHashMap(None)) + } else { + Ok(DiffHashMap(Some(changes))) + } + } + + fn merge(&self, diff: Self::Type) -> diff::Result { + let mut this: Self = self.clone(); + + for change in diff.0.into_iter().flatten() { + match change { + InnerValue::Change { key, value } => { + if let Some(entry) = this.get_mut(&key) { + *entry = Value::from_diff(value)?; + } + } + InnerValue::Add { key, value } => { + this.insert(key, Value::from_diff(value)?); + } + InnerValue::Remove { key } => { + this.remove(&key); + } + } + } + + Ok(this) + } + + fn from_diff(diff: Self::Type) -> diff::Result { + let mut this: Self = Self::new(); + + if let Some(diff) = diff.0 { + for (index, inner) in diff.into_iter().enumerate() { + if let InnerValue::Add { key, value } = inner { + this.insert(key, Value::from_diff(value)?); + } else { + panic!("Unable to create Diff at index: {:?}", index); + } + } + } + + Ok(this) + } + + fn into_diff(self) -> diff::Result { + let changes: Vec<_> = self + .0 + .into_iter() + .map(|(key, value)| value.into_diff().map(|value| InnerValue::Add { key, value })) + .collect::>()?; + + if changes.is_empty() { + Ok(DiffHashMap(None)) + } else { + Ok(DiffHashMap(Some(changes))) + } + } +} diff --git a/identity_core/src/common/one_or_many.rs b/identity_core/src/common/one_or_many.rs new file mode 100644 index 0000000000..15ac7269f8 --- /dev/null +++ b/identity_core/src/common/one_or_many.rs @@ -0,0 +1,193 @@ +use core::{fmt, hash::Hash, slice::from_ref}; +use identity_diff::{Diff, Error}; +use serde::{Deserialize, Serialize}; + +use crate::error::Result; + +/// A generic container that stores one or many values of a given type. +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[serde(untagged)] +pub enum OneOrMany { + One(T), + Many(Vec), +} + +impl OneOrMany { + /// Returns the number of elements in the collection + pub fn len(&self) -> usize { + match self { + Self::One(_) => 1, + Self::Many(inner) => inner.len(), + } + } + + /// Returns `true` if the collection is empty + pub fn is_empty(&self) -> bool { + match self { + Self::One(_) => false, + Self::Many(inner) => inner.is_empty(), + } + } + + /// Returns a reference to the element at the given index. + pub fn get(&self, index: usize) -> Option<&T> { + match self { + Self::One(inner) if index == 0 => Some(inner), + Self::One(_) => None, + Self::Many(inner) => inner.get(index), + } + } + + /// Returns `true` if the given value is represented in the collection. + pub fn contains(&self, value: &T) -> bool + where + T: PartialEq, + { + match self { + Self::One(inner) => inner == value, + Self::Many(inner) => inner.contains(value), + } + } + + pub fn iter(&self) -> impl Iterator + '_ { + OneOrManyIter::new(self) + } + + pub fn as_slice(&self) -> &[T] { + match self { + Self::One(inner) => from_ref(inner), + Self::Many(inner) => inner.as_slice(), + } + } + + /// Consumes the `OneOrMany`, returning the contents as a `Vec`. + pub fn into_vec(self) -> Vec { + match self { + Self::One(inner) => vec![inner], + Self::Many(inner) => inner, + } + } +} + +impl fmt::Debug for OneOrMany +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::One(inner) => fmt::Debug::fmt(inner, f), + Self::Many(inner) => fmt::Debug::fmt(inner, f), + } + } +} + +impl Default for OneOrMany { + fn default() -> Self { + Self::Many(Vec::new()) + } +} + +impl From for OneOrMany { + fn from(other: T) -> Self { + Self::One(other) + } +} + +impl From> for OneOrMany { + fn from(mut other: Vec) -> Self { + if other.len() == 1 { + Self::One(other.pop().expect("infallible")) + } else { + Self::Many(other) + } + } +} + +impl From> for Vec { + fn from(other: OneOrMany) -> Self { + other.into_vec() + } +} + +// ============================================================================= +// Iterator +// ============================================================================= + +struct OneOrManyIter<'a, T> { + inner: &'a OneOrMany, + index: usize, +} + +impl<'a, T> OneOrManyIter<'a, T> { + pub fn new(inner: &'a OneOrMany) -> Self { + Self { inner, index: 0 } + } +} + +impl<'a, T> Iterator for OneOrManyIter<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + self.index += 1; + self.inner.get(self.index - 1) + } +} + +// ============================================================================= +// Diff Support +// ============================================================================= + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(bound(deserialize = "DiffOneOrMany: Clone + fmt::Debug + PartialEq"))] +// #[serde(from = "OneOrMany", into = "OneOrMany")] +#[serde(untagged)] +pub enum DiffOneOrMany +where + T: Diff + for<'a> Deserialize<'a> + Serialize, +{ + One(#[serde(skip_serializing_if = "Option::is_none")] Option<::Type>), + Many(#[serde(skip_serializing_if = "Option::is_none")] Option< as Diff>::Type>), +} + +impl Diff for OneOrMany +where + T: Clone + fmt::Debug + PartialEq + Diff + for<'a> Deserialize<'a> + Serialize, +{ + type Type = DiffOneOrMany; + + fn diff(&self, other: &Self) -> Result { + match (self, other) { + (Self::One(lhs), Self::One(rhs)) if lhs == rhs => Ok(DiffOneOrMany::One(None)), + (Self::One(lhs), Self::One(rhs)) => lhs.diff(rhs).map(Some).map(DiffOneOrMany::One), + (Self::Many(lhs), Self::Many(rhs)) if lhs == rhs => Ok(DiffOneOrMany::Many(None)), + (Self::Many(lhs), Self::Many(rhs)) => lhs.diff(rhs).map(Some).map(DiffOneOrMany::Many), + (_, diff) => diff.clone().into_diff(), + } + } + + fn merge(&self, diff: Self::Type) -> Result { + match (self, diff) { + (Self::One(lhs), Self::Type::One(Some(ref rhs))) => Ok(Self::One(lhs.merge(rhs.clone())?)), + (lhs @ Self::One(_), Self::Type::One(None)) => Ok(lhs.clone()), + (Self::Many(lhs), Self::Type::Many(Some(ref rhs))) => Ok(Self::Many(lhs.merge(rhs.clone())?)), + (lhs @ Self::Many(_), Self::Type::Many(None)) => Ok(lhs.clone()), + (_, diff) => Self::from_diff(diff), + } + } + + fn from_diff(diff: Self::Type) -> Result { + match diff { + DiffOneOrMany::One(Some(inner)) => T::from_diff(inner).map(Self::One), + DiffOneOrMany::One(None) => Ok(Default::default()), + DiffOneOrMany::Many(Some(inner)) => >::from_diff(inner).map(Self::Many), + DiffOneOrMany::Many(None) => Ok(Default::default()), + } + } + + fn into_diff(self) -> Result { + match self { + Self::One(inner) => inner.into_diff().map(Some).map(DiffOneOrMany::One), + Self::Many(inner) => inner.into_diff().map(Some).map(DiffOneOrMany::Many), + } + } +} diff --git a/identity_common/src/timestamp.rs b/identity_core/src/common/timestamp.rs similarity index 56% rename from identity_common/src/timestamp.rs rename to identity_core/src/common/timestamp.rs index 91c4816f11..7ebe342ddd 100644 --- a/identity_common/src/timestamp.rs +++ b/identity_core/src/common/timestamp.rs @@ -1,8 +1,9 @@ use chrono::{DateTime, SecondsFormat, Utc}; +use core::{convert::TryFrom, fmt, ops::Deref, str::FromStr}; +use identity_diff::{self as diff, string::DiffString, Diff}; use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, fmt, ops::Deref}; -use crate::error::Error; +use crate::error::{Error, Result}; type Inner = DateTime; @@ -12,9 +13,16 @@ type Inner = DateTime; pub struct Timestamp(Inner); impl Timestamp { + pub fn parse(string: &str) -> Result { + match DateTime::parse_from_rfc3339(string) { + Ok(datetime) => Ok(Self(datetime.into())), + Err(error) => Err(Error::InvalidTimestamp(error)), + } + } + /// Creates a new `Timestamp` of the current time. pub fn now() -> Self { - Self(Utc::now()) + Self::parse(&Self::to_rfc3339(&Self(Utc::now()))).unwrap() } /// Consumes the `Timestamp` and returns the inner `DateTime`. @@ -36,7 +44,7 @@ impl Default for Timestamp { impl fmt::Debug for Timestamp { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self.to_rfc3339()) + write!(f, "{:?}", self.0) } } @@ -70,9 +78,38 @@ impl TryFrom<&'_ str> for Timestamp { type Error = Error; fn try_from(string: &'_ str) -> Result { - match DateTime::parse_from_rfc3339(string) { - Ok(datetime) => Ok(Self(datetime.into())), - Err(error) => Err(Error::InvalidTimestamp(error)), - } + Self::parse(string) + } +} + +impl FromStr for Timestamp { + type Err = Error; + + fn from_str(string: &str) -> Result { + Self::parse(string) + } +} + +impl Diff for Timestamp { + type Type = DiffString; + + fn diff(&self, other: &Self) -> Result { + self.to_rfc3339().diff(&other.to_rfc3339()) + } + + fn merge(&self, diff: Self::Type) -> Result { + let this: String = self.to_rfc3339().merge(diff)?; + + Self::from_str(this.as_str()).map_err(|error| diff::Error::MergeError(format!("{}", error))) + } + + fn from_diff(diff: Self::Type) -> Result { + let this: String = String::from_diff(diff)?; + + Self::from_str(this.as_str()).map_err(|error| diff::Error::MergeError(format!("{}", error))) + } + + fn into_diff(self) -> Result { + self.to_rfc3339().into_diff() } } diff --git a/identity_core/src/common/url.rs b/identity_core/src/common/url.rs new file mode 100644 index 0000000000..59ef70cdb4 --- /dev/null +++ b/identity_core/src/common/url.rs @@ -0,0 +1,130 @@ +use core::{ + fmt::{Debug, Display, Formatter, Result as FmtResult}, + ops::{Deref, DerefMut}, + str::FromStr, +}; +use identity_diff::{self as diff, string::DiffString, Diff}; +use serde::{Deserialize, Serialize}; + +use crate::{ + did::DID, + error::{Error, Result}, +}; + +/// A simple wrapper for URIs adhering to RFC 3986 +/// +/// TODO: Parse/Validate according to RFC 3986 +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[repr(transparent)] +#[serde(transparent)] +pub struct Url(url::Url); + +impl Url { + pub fn parse(input: impl AsRef) -> Result { + url::Url::parse(input.as_ref()).map_err(Into::into).map(Self) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn into_string(self) -> String { + self.0.into_string() + } + + pub fn clone_into_string(&self) -> String { + self.0.clone().into_string() + } + + pub fn join(&self, input: impl AsRef) -> Result { + self.0.join(input.as_ref()).map_err(Into::into).map(Self) + } +} + +impl Debug for Url { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + Debug::fmt(&self.0, f) + } +} + +impl Display for Url { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + Display::fmt(&self.0, f) + } +} + +impl Default for Url { + fn default() -> Self { + Self::parse("did:").unwrap() + } +} + +impl Deref for Url { + type Target = url::Url; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Url { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FromStr for Url { + type Err = Error; + + fn from_str(string: &str) -> Result { + Self::parse(string) + } +} + +impl From<&'_ str> for Url { + fn from(other: &'_ str) -> Self { + Self::parse(other).unwrap() + } +} + +impl From for Url { + fn from(other: String) -> Self { + Self::parse(other).unwrap() + } +} + +impl From for Url { + fn from(other: DID) -> Url { + Self::parse(other.to_string()).unwrap() + } +} + +impl PartialEq for Url +where + T: AsRef + ?Sized, +{ + fn eq(&self, other: &T) -> bool { + self.as_str() == other.as_ref() + } +} + +impl Diff for Url { + type Type = DiffString; + + fn diff(&self, other: &Self) -> Result { + self.clone_into_string().diff(&other.clone_into_string()) + } + + fn merge(&self, diff: Self::Type) -> Result { + Self::parse(self.clone_into_string().merge(diff)?) + .map_err(|error| diff::Error::MergeError(format!("{}", error))) + } + + fn from_diff(diff: Self::Type) -> Result { + Self::parse(String::from_diff(diff)?).map_err(|error| diff::Error::MergeError(format!("{}", error))) + } + + fn into_diff(self) -> Result { + self.clone_into_string().into_diff() + } +} diff --git a/identity_common/src/value.rs b/identity_core/src/common/value.rs similarity index 100% rename from identity_common/src/value.rs rename to identity_core/src/common/value.rs diff --git a/identity_core/src/did/authentication.rs b/identity_core/src/did/authentication.rs new file mode 100644 index 0000000000..cafcb1ada0 --- /dev/null +++ b/identity_core/src/did/authentication.rs @@ -0,0 +1,42 @@ +use identity_diff::Diff; +use serde::{Deserialize, Serialize}; + +use crate::{did::DID, key::PublicKey, utils::HasId}; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Diff, Serialize, Deserialize)] +#[serde(untagged)] +#[diff(from_into)] +pub enum Authentication { + DID(DID), + Key(PublicKey), +} + +impl Default for Authentication { + fn default() -> Self { + Self::DID(Default::default()) + } +} + +impl From for Authentication { + fn from(other: DID) -> Self { + Self::DID(other) + } +} + +impl From for Authentication { + fn from(other: PublicKey) -> Self { + Self::Key(other) + } +} + +impl HasId for Authentication { + type Id = DID; + + fn id(&self) -> &Self::Id { + match self { + Authentication::DID(subject) => subject, + Authentication::Key(key) => key.id(), + } + } +} diff --git a/identity_core/src/did.pest b/identity_core/src/did/did-syntax.pest similarity index 88% rename from identity_core/src/did.pest rename to identity_core/src/did/did-syntax.pest index ddacc19aed..a2e19b66df 100644 --- a/identity_core/src/did.pest +++ b/identity_core/src/did/did-syntax.pest @@ -1,6 +1,6 @@ pct_encoded = { "%" ~ ASCII_HEX_DIGIT ~ ASCII_HEX_DIGIT } shard_char = { path_char | "/" | "?" } -path_char = { ASCII_ALPHA | ASCII_DIGIT | "-" | "." | "_" | +path_char = { ASCII_ALPHA | ASCII_DIGIT | "-" | "." | "_" | "!" | "~" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | ";" | "," | "=" | ":" | "@" | pct_encoded} @@ -17,7 +17,7 @@ id_segment = { id_segment_char* } param_char = { ASCII_ALPHA | ASCII_DIGIT | "." | "-" | "_" | ":" | "!" | "~" | "$" | "'" | "(" | ")" | "*" | "+" | - ";" | "," | "@" | + ";" | "," | "@" | pct_encoded } param_name = { param_char+ } param_value = { param_char* } @@ -27,4 +27,4 @@ query = { param ~ ("&" ~ param)* } did = _{"did:" ~ method_name ~ ":" ~ (id_segment ~ (":" ~ id_segment)*) ~ ("/" ~ path_segment)* ~ ("?" ~ query)* ~ - ("#" ~ fragment)? } \ No newline at end of file + ("#" ~ fragment)? } diff --git a/identity_core/src/did.rs b/identity_core/src/did/did.rs similarity index 85% rename from identity_core/src/did.rs rename to identity_core/src/did/did.rs index 0453095504..732c4033b8 100644 --- a/identity_core/src/did.rs +++ b/identity_core/src/did/did.rs @@ -1,6 +1,5 @@ -use identity_common::uri::Uri; +use core::str::FromStr; use identity_diff::Diff; - use serde::{ de::{self, Deserialize, Deserializer, Visitor}, ser::{Serialize, Serializer}, @@ -11,14 +10,14 @@ use std::{ hash::Hash, }; -use crate::did_parser::parse; +use crate::did::parser::parse; const LEADING_TOKENS: &str = "did"; /// An aliased tuple the converts into a `Param` type. type DIDTuple = (String, Option); -/// a Decentralized identity structure. +/// a Decentralized identity structure. #[derive(Debug, PartialEq, Default, Eq, Clone, Diff, Hash, Ord, PartialOrd)] #[diff(from_into)] pub struct DID { @@ -29,15 +28,11 @@ pub struct DID { pub fragment: Option, } -/// a DID Params struct. -#[derive(Debug, PartialEq, Eq, Clone, Default, Hash, PartialOrd, Ord, Diff, DDeserialize, DSerialize)] -#[diff(from_into)] -pub struct Param { - pub key: String, - pub value: Option, -} - impl DID { + pub const BASE_CONTEXT: &'static str = "https://www.w3.org/ns/did/v1"; + + pub const SECURITY_CONTEXT: &'static str = "https://w3id.org/security/v1"; + /// Initializes the DID struct with the filled out fields. Also runs parse_from_str to validate the fields. pub fn init(self) -> crate::Result { let did = DID { @@ -51,6 +46,17 @@ impl DID { DID::parse_from_str(did) } + // TODO: Fix this + pub fn join_relative(base: &Self, relative: &Self) -> crate::Result { + Ok(Self { + method_name: base.method_name.clone(), + id_segments: base.id_segments.clone(), + fragment: relative.fragment.clone(), + path_segments: relative.path_segments.clone(), + query: relative.query.clone(), + }) + } + pub fn parse_from_str(input: T) -> crate::Result where T: ToString, @@ -86,7 +92,7 @@ impl DID { self.path_segments = Some(ps.clone()); } - /// Method to add a fragment to the DID. + /// Method to add a fragment to the DID. pub fn add_fragment(&mut self, fragment: String) { self.fragment = Some(fragment); } @@ -153,15 +159,11 @@ impl Display for DID { } } -/// Display trait for the param struct. -impl Display for Param { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let val = match &self.value { - Some(v) => format!("={}", v), - None => String::new(), - }; +impl FromStr for DID { + type Err = crate::Error; - write!(f, "{}{}", self.key, val) + fn from_str(string: &str) -> crate::Result { + Self::parse_from_str(string) } } @@ -207,14 +209,34 @@ impl Serialize for DID { } } -impl From for Param { - fn from((key, value): DIDTuple) -> Param { - Param { key, value } +/// a DID Params struct. +#[derive(Debug, PartialEq, Eq, Clone, Default, Hash, PartialOrd, Ord, Diff, DDeserialize, DSerialize)] +#[diff(from_into)] +pub struct Param { + pub key: String, + pub value: Option, +} + +impl Param { + pub fn pair(&self) -> (&str, &str) { + (self.key.as_str(), self.value.as_deref().unwrap_or_default()) } } -impl From for Uri { - fn from(other: DID) -> Uri { - Uri::from(other.to_string()) +/// Display trait for the param struct. +impl Display for Param { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let val = match &self.value { + Some(v) => format!("={}", v), + None => String::new(), + }; + + write!(f, "{}{}", self.key, val) + } +} + +impl From for Param { + fn from((key, value): DIDTuple) -> Param { + Param { key, value } } } diff --git a/identity_core/src/did/document.rs b/identity_core/src/did/document.rs new file mode 100644 index 0000000000..04590e1175 --- /dev/null +++ b/identity_core/src/did/document.rs @@ -0,0 +1,275 @@ +use core::{slice::Iter, str::FromStr}; +use derive_builder::Builder; +use identity_diff::Diff; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Context, Object, OneOrMany, Timestamp, Value}, + did::{Authentication, Service, DID}, + error::Result, + key::{KeyIndex, KeyRelation, PublicKey}, + utils::{AddUnique as _, HasId}, +}; + +/// A struct that represents a DID Document. Contains the fields `context`, `id`, `created`, `updated`, +/// `public_keys`, services and metadata. Only `context` and `id` are required to create a DID document. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Diff, Builder)] +#[builder(pattern = "owned")] +pub struct DIDDocument { + #[serde(rename = "@context")] + #[builder(setter(into))] + pub context: OneOrMany, + #[builder(try_setter)] + pub id: DID, + #[serde(skip_serializing_if = "Option::is_none")] + #[diff(should_ignore)] + #[builder(default, setter(into, strip_option))] + pub created: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub updated: Option, + #[serde(rename = "publicKey", skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub public_keys: Vec, + #[serde(rename = "authentication", skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub auth: Vec, + #[serde(rename = "assertionMethod", skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub assert: Vec, + #[serde(rename = "verificationMethod", skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub verification: Vec, + #[serde(rename = "capabilityDelegation", skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub delegation: Vec, + #[serde(rename = "capabilityInvocation", skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub invocation: Vec, + #[serde(rename = "keyAgreement", skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub agreement: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + #[builder(default)] + pub services: Vec, + #[serde(flatten)] + #[builder(default)] + pub metadata: Object, +} + +impl DIDDocument { + pub fn resolve_key<'a>(&self, key: impl Into>, relation: KeyRelation) -> Option<&PublicKey> { + self.resolve_key_(key.into(), relation) + } + + /// gets the inner value of the `context` from the `DIDDocument`. + pub fn context(&self) -> &[Context] { + self.context.as_slice() + } + + /// sets a new `service` of type `Service` into the `DIDDocument`. + pub fn update_service(&mut self, service: Service) { + self.services.set_unique(service); + } + + /// remove all of the services from the `DIDDocument`. + pub fn clear_services(&mut self) { + self.services.clear(); + } + + /// sets a new `key_pair` of type `PublicKey` into the `DIDDocument`. + pub fn update_public_key(&mut self, key_pair: PublicKey) { + self.public_keys.set_unique(key_pair); + } + + /// remove all of the public keys from the `DIDDocument`. + pub fn clear_public_keys(&mut self) { + self.public_keys.clear(); + } + + /// sets in a new `auth` of type `Authentication` into the `DIDDocument`. + pub fn update_auth(&mut self, auth: impl Into) { + self.auth.set_unique(auth.into()); + } + + /// remove all of the authentications from the `DIDDocument`. + pub fn clear_auth(&mut self) { + self.auth.clear(); + } + + /// sets in a new `assert` of type `Authentication` into the `DIDDocument`. + pub fn update_assert(&mut self, assert: impl Into) { + self.assert.set_unique(assert.into()); + } + + /// remove all of the assertion methods from the `DIDDocument`. + pub fn clear_assert(&mut self) { + self.assert.clear(); + } + + /// sets in a new `verification` of type `Authentication` into the `DIDDocument`. + pub fn update_verification(&mut self, verification: impl Into) { + self.verification.set_unique(verification.into()); + } + + /// remove all of the verification methods from the `DIDDocument`. + pub fn clear_verification(&mut self) { + self.verification.clear(); + } + + /// sets in a new `delegation` of type `Authentication` into the `DIDDocument`. + pub fn update_delegation(&mut self, delegation: impl Into) { + self.delegation.set_unique(delegation.into()); + } + + /// remove all of the capability delegations from the `DIDDocument`. + pub fn clear_delegation(&mut self) { + self.delegation.clear(); + } + + /// sets in a new `invocation` of type `Authentication` into the `DIDDocument`. + pub fn update_invocation(&mut self, invocation: impl Into) { + self.invocation.set_unique(invocation.into()); + } + + /// remove all of the capability invocations from the `DIDDocument`. + pub fn clear_invocation(&mut self) { + self.invocation.clear(); + } + + /// sets in a new `agreement` of type `Authentication` into the `DIDDocument`. + pub fn update_agreement(&mut self, agreement: impl Into) { + self.agreement.set_unique(agreement.into()); + } + + /// remove all of the key agreements from the `DIDDocument`. + pub fn clear_agreement(&mut self) { + self.agreement.clear(); + } + + /// get the ID from the Document as a DID. + pub fn did(&self) -> &DID { + &self.id + } + + /// Updates the `updated` time for the `DIDDocument`. + pub fn update_time(&mut self) { + self.updated = Some(Timestamp::now()); + } + + pub fn set_metadata(&mut self, key: T, value: U) + where + T: Into, + U: Into, + { + self.metadata.insert(key.into(), value.into()); + } + + pub fn remove_metadata(&mut self, key: &str) { + self.metadata.remove(key); + } + + pub fn clear_metadata(&mut self) { + self.metadata.clear(); + } + + pub fn metadata(&self) -> &Object { + &self.metadata + } + + pub fn metadata_mut(&mut self) -> &mut Object { + &mut self.metadata + } + + /// initialize the `created` and `updated` timestamps to publish the did document. + pub fn init_timestamps(&mut self) { + self.created = Some(Timestamp::now()); + self.updated = Some(Timestamp::now()); + } + + pub fn get_diff_from_str(json: impl AsRef) -> Result { + serde_json::from_str(json.as_ref()).map_err(crate::Error::DecodeJSON) + } + + fn resolve_key_(&self, key: KeyIndex, relation: KeyRelation) -> Option<&PublicKey> { + for (index, method) in self.key_iter(relation).enumerate() { + if key == index || Self::matches_fragment(method, key) { + return self.extract_key(method); + } + } + + if relation == KeyRelation::VerificationMethod { + for (index, method) in self.public_keys.iter().enumerate() { + if key == index || Self::matches_fragment(method, key) { + return Some(method); + } + } + } + + None + } + + fn matches_fragment(method: &T, key: KeyIndex) -> bool + where + T: HasId, + { + matches!(method.id().fragment.as_deref(), Some(fragment) if key == fragment) + } + + fn key_iter(&self, relation: KeyRelation) -> Iter { + match relation { + KeyRelation::VerificationMethod => self.verification.iter(), + KeyRelation::Authentication => self.auth.iter(), + KeyRelation::AssertionMethod => self.assert.iter(), + KeyRelation::KeyAgreement => self.agreement.iter(), + KeyRelation::CapabilityInvocation => self.invocation.iter(), + KeyRelation::CapabilityDelegation => self.delegation.iter(), + } + } + + fn extract_key<'a>(&'a self, method: &'a Authentication) -> Option<&'a PublicKey> { + match method { + Authentication::DID(did) => did + .fragment + .as_deref() + .and_then(|fragment| self.resolve_key_(fragment.into(), Default::default())), + Authentication::Key(key) => Some(key), + } + } +} + +impl Default for DIDDocument { + fn default() -> Self { + Self { + context: OneOrMany::One(DID::BASE_CONTEXT.into()), + id: Default::default(), + created: None, + updated: None, + public_keys: Vec::new(), + auth: Vec::new(), + assert: Vec::new(), + verification: Vec::new(), + delegation: Vec::new(), + invocation: Vec::new(), + agreement: Vec::new(), + services: Vec::new(), + metadata: Default::default(), + } + } +} + +/// converts a `DIDDocument` into a string using the `to_string()` method. +impl ToString for DIDDocument { + fn to_string(&self) -> String { + serde_json::to_string(&self).expect("Unable to serialize document") + } +} + +/// takes a &str and converts it into a `DIDDocument` given the proper format. +impl FromStr for DIDDocument { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(crate::Error::DecodeJSON) + } +} diff --git a/identity_core/src/did/mod.rs b/identity_core/src/did/mod.rs new file mode 100644 index 0000000000..fcab3d2fd6 --- /dev/null +++ b/identity_core/src/did/mod.rs @@ -0,0 +1,11 @@ +mod authentication; +#[allow(clippy::module_inception)] +mod did; +mod document; +mod parser; +mod service; + +pub use authentication::*; +pub use did::*; +pub use document::*; +pub use service::*; diff --git a/identity_core/src/did_parser.rs b/identity_core/src/did/parser.rs similarity index 96% rename from identity_core/src/did_parser.rs rename to identity_core/src/did/parser.rs index 3bf5edef65..7afb040107 100644 --- a/identity_core/src/did_parser.rs +++ b/identity_core/src/did/parser.rs @@ -5,10 +5,10 @@ use crate::did::{Param, DID}; /// a derived parser for the `DID` struct. Rules are derived from the `did.pest` file using the `pest` crate. #[derive(Parser)] -#[grammar = "did.pest"] +#[grammar = "did/did-syntax.pest"] struct DIDParser; -/// parses a `ToString` type into a `DID` if it follows the proper format. +/// parses a `ToString` type into a `DID` if it follows the proper format. pub fn parse(input: T) -> crate::Result where T: ToString, @@ -18,7 +18,7 @@ where match pairs { Ok(p) => Ok(parse_pairs(p)?), - Err(e) => Err(crate::Error::ParseError(e)), + Err(e) => Err(crate::Error::ParseError(e.into())), } } diff --git a/identity_core/src/did/service.rs b/identity_core/src/did/service.rs new file mode 100644 index 0000000000..df8708bac9 --- /dev/null +++ b/identity_core/src/did/service.rs @@ -0,0 +1,121 @@ +use derive_builder::Builder; +use identity_diff::Diff; +use serde::{Deserialize, Serialize}; + +use crate::{common::Url, did::DID, utils::HasId}; + +/// Describes a `Service` in a `DIDDocument` type. +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Diff, Builder)] +#[diff(from_into)] +#[builder(pattern = "owned")] +pub struct Service { + #[serde(default)] + #[builder(try_setter)] + id: DID, + #[serde(rename = "type")] + #[builder(setter(into))] + service_type: String, + #[serde(rename = "serviceEndpoint")] + #[builder(setter(into))] + endpoint: ServiceEndpoint, +} + +impl Service { + pub fn id(&self) -> &DID { + &self.id + } + + pub fn service_type(&self) -> &str { + &*self.service_type + } + + pub fn endpoint(&self) -> &ServiceEndpoint { + &self.endpoint + } +} + +impl HasId for Service { + type Id = DID; + + fn id(&self) -> &Self::Id { + &self.id + } +} + +/// Describes the `ServiceEndpoint` struct type. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Diff)] +#[serde(untagged)] +#[diff(from_into)] +pub enum ServiceEndpoint { + Url(Url), + Obj(ServiceEndpointObject), +} + +impl ServiceEndpoint { + pub fn context(&self) -> &Url { + match self { + Self::Url(url) => &url, + Self::Obj(inner) => inner.context(), + } + } + + pub fn endpoint_type(&self) -> Option<&str> { + match self { + Self::Url(_) => None, + Self::Obj(inner) => inner.endpoint_type(), + } + } + + pub fn instances(&self) -> Option<&[String]> { + match self { + Self::Url(_) => None, + Self::Obj(inner) => inner.instances(), + } + } +} + +impl Default for ServiceEndpoint { + fn default() -> Self { + Self::Url(Default::default()) + } +} + +impl From for ServiceEndpoint { + fn from(other: Url) -> Self { + Self::Url(other) + } +} + +impl From for ServiceEndpoint { + fn from(other: ServiceEndpointObject) -> Self { + Self::Obj(other) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Diff, Builder)] +#[diff(from_into)] +#[builder(pattern = "owned", name = "ServiceEndpointBuilder")] +pub struct ServiceEndpointObject { + #[serde(rename = "@context")] + #[builder(try_setter)] + context: Url, + #[serde(rename = "type")] + #[builder(default, setter(into, strip_option))] + endpoint_type: Option, + #[builder(default, setter(into, strip_option))] + instances: Option>, +} + +impl ServiceEndpointObject { + pub fn context(&self) -> &Url { + &self.context + } + + pub fn endpoint_type(&self) -> Option<&str> { + self.endpoint_type.as_deref() + } + + pub fn instances(&self) -> Option<&[String]> { + self.instances.as_deref() + } +} diff --git a/identity_core/src/document.rs b/identity_core/src/document.rs deleted file mode 100644 index 6dfc48be48..0000000000 --- a/identity_core/src/document.rs +++ /dev/null @@ -1,229 +0,0 @@ -use identity_common::Timestamp; -use identity_diff::Diff; - -use serde::{Deserialize, Serialize}; -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, -}; - -use crate::{ - did::DID, - utils::{ - add_unique_to_vec, helpers::string_or_list, Authentication, Context, IdCompare, PublicKey, Service, Subject, - }, -}; - -/// A struct that represents a DID Document. Contains the fields `context`, `id`, `created`, `updated`, -/// `public_keys`, services and metadata. Only `context` and `id` are required to create a DID document. -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Diff)] -pub struct DIDDocument { - #[serde(rename = "@context", deserialize_with = "string_or_list", default)] - pub context: Context, - pub id: Subject, - #[serde(skip_serializing_if = "Option::is_none")] - #[diff(should_ignore)] - pub created: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub updated: Option, - #[serde(rename = "publicKey", skip_serializing_if = "HashSet::is_empty", default)] - pub public_keys: HashSet>, - #[serde(rename = "authentication", skip_serializing_if = "Vec::is_empty", default)] - pub auth: Vec>, - #[serde(rename = "assertionMethod", skip_serializing_if = "Vec::is_empty", default)] - pub assert: Vec>, - #[serde(rename = "verificationMethod", skip_serializing_if = "Vec::is_empty", default)] - pub verification: Vec>, - #[serde(rename = "capabilityDelegation", skip_serializing_if = "Vec::is_empty", default)] - pub delegation: Vec>, - #[serde(rename = "capabilityInvocation", skip_serializing_if = "Vec::is_empty", default)] - pub invocation: Vec>, - #[serde(rename = "keyAgreement", skip_serializing_if = "Vec::is_empty", default)] - pub agreement: Vec>, - #[serde(skip_serializing_if = "HashSet::is_empty", default)] - pub services: HashSet>, - #[serde(flatten)] - pub metadata: HashMap, -} - -impl DIDDocument { - /// Initialize the DIDDocument. - pub fn init(self) -> Self { - DIDDocument { - context: self.context, - id: self.id, - created: self.created, - updated: self.updated, - auth: self.auth, - assert: self.assert, - verification: self.verification, - delegation: self.delegation, - invocation: self.invocation, - agreement: self.agreement, - metadata: self.metadata, - public_keys: self.public_keys, - services: self.services, - } - } - - /// gets the inner value of the `context` from the `DIDDocument`. - pub fn context(&self) -> &Vec { - &self.context.as_inner() - } - - /// sets a new `service` of type `Service` into the `DIDDocument`. - pub fn update_service(&mut self, service: Service) { - let service = IdCompare::new(service); - - self.services.insert(service); - } - - /// remove all of the services from the `DIDDocument`. - pub fn clear_services(&mut self) { - self.services.clear(); - } - - /// sets a new `key_pair` of type `PublicKey` into the `DIDDocument`. - pub fn update_public_key(&mut self, key_pair: PublicKey) { - let key_pair = IdCompare::new(key_pair); - - self.public_keys.insert(key_pair); - } - - /// remove all of the public keys from the `DIDDocument`. - pub fn clear_public_keys(&mut self) { - self.public_keys.clear(); - } - - /// sets in a new `auth` of type `Authentication` into the `DIDDocument`. - pub fn update_auth(&mut self, auth: Authentication) { - let auth = IdCompare::new(auth); - - let collection = add_unique_to_vec(auth, self.auth.clone()); - - self.auth = collection; - } - - /// remove all of the authentications from the `DIDDocument`. - pub fn clear_auth(&mut self) { - self.auth.clear(); - } - - /// sets in a new `assert` of type `Authentication` into the `DIDDocument`. - pub fn update_assert(&mut self, assert: Authentication) { - let assert = IdCompare::new(assert); - - let collection = add_unique_to_vec(assert, self.assert.clone()); - - self.assert = collection; - } - - /// remove all of the assertion methods from the `DIDDocument`. - pub fn clear_assert(&mut self) { - self.assert.clear(); - } - - /// sets in a new `verification` of type `Authentication` into the `DIDDocument`. - pub fn update_verification(&mut self, verification: Authentication) { - let verification = IdCompare::new(verification); - - let collection = add_unique_to_vec(verification, self.verification.clone()); - - self.verification = collection; - } - - /// remove all of the verification methods from the `DIDDocument`. - pub fn clear_verification(&mut self) { - self.verification.clear(); - } - - /// sets in a new `delegation` of type `Authentication` into the `DIDDocument`. - pub fn update_delegation(&mut self, delegation: Authentication) { - let delegation = IdCompare::new(delegation); - - let collection = add_unique_to_vec(delegation, self.delegation.clone()); - - self.delegation = collection; - } - - /// remove all of the capability delegations from the `DIDDocument`. - pub fn clear_delegation(&mut self) { - self.delegation.clear(); - } - - /// sets in a new `invocation` of type `Authentication` into the `DIDDocument`. - pub fn update_invocation(&mut self, invocation: Authentication) { - let invocation = IdCompare::new(invocation); - - let collection = add_unique_to_vec(invocation, self.invocation.clone()); - - self.invocation = collection; - } - - /// remove all of the capability invocations from the `DIDDocument`. - pub fn clear_invocation(&mut self) { - self.invocation.clear(); - } - - /// sets in a new `agreement` of type `Authentication` into the `DIDDocument`. - pub fn update_agreement(&mut self, agreement: Authentication) { - let agreement = IdCompare::new(agreement); - - let collection = add_unique_to_vec(agreement, self.agreement.clone()); - - self.agreement = collection; - } - - /// remove all of the key agreements from the `DIDDocument`. - pub fn clear_agreement(&mut self) { - self.agreement.clear(); - } - - /// get the ID from the Document as a DID. - pub fn derive_did(&self) -> crate::Result { - self.id.to_did() - } - - /// Updates the `updated` time for the `DIDDocument`. - pub fn update_time(&mut self) { - self.updated = Some(Timestamp::now().to_string()); - } - - /// Inserts `metadata` into the `DIDDocument` body. The metadata must be a HashMap where the keys - /// are json keys and values are the json values. - pub fn supply_metadata(self, metadata: HashMap) -> crate::Result { - Ok(DIDDocument { metadata, ..self }.init()) - } - - /// initialize the `created` and `updated` timestamps to publish the did document. Returns the did document with - /// these timestamps. - pub fn init_timestamps(self) -> crate::Result { - Ok(DIDDocument { - created: Some(Timestamp::now().to_string()), - updated: Some(Timestamp::now().to_string()), - ..self - } - .init()) - } - - pub fn get_diff_from_str(json: String) -> crate::Result { - Ok(serde_json::from_str(&json)?) - } -} - -/// converts a `DIDDocument` into a string using the `to_string()` method. -impl ToString for DIDDocument { - fn to_string(&self) -> String { - serde_json::to_string(&self).expect("Unable to serialize document") - } -} - -/// takes a &str and converts it into a `DIDDocument` given the proper format. -impl FromStr for DIDDocument { - type Err = crate::Error; - - fn from_str(s: &str) -> crate::Result { - let doc = serde_json::from_str(s)?; - Ok(doc) - } -} diff --git a/identity_core/src/error.rs b/identity_core/src/error.rs index fed67b1531..29f411fbb5 100644 --- a/identity_core/src/error.rs +++ b/identity_core/src/error.rs @@ -1,31 +1,51 @@ -use anyhow::Result as AnyhowResult; -use identity_common::Error as CommonError; -use pest::error::Error as PestError; -use thiserror::Error as DeriveError; - -use crate::did_parser::Rule; +/// The main crate result type derived from the `anyhow::Result` type. +pub type Result = anyhow::Result; /// The main crate Error type; uses `thiserror`. -#[derive(Debug, DeriveError)] +#[derive(Debug, thiserror::Error)] pub enum Error { /// A format error that takes a String. Indicates that the Format of the did is not correct. #[error("Format Error: {0}")] FormatError(String), /// Error from when pest can not properly parse a line. #[error("Parse Error: {0}")] - ParseError(#[from] PestError), - /// Error for when the key format is not supported. - #[error("Key Format Error: This Key encoding type is not supported")] - KeyFormatError, - /// Error for when the key type is not supported. - #[error("Key Type Error: This key type is not supported")] - KeyTypeError, - /// Json related error from `serde_json` - #[error("Json Error: {0}")] - SerdeError(#[from] serde_json::Error), - #[error(transparent)] - CommonError(#[from] CommonError), + ParseError(anyhow::Error), + #[error("Diff Error: {0}")] + DiffError(#[from] identity_diff::Error), + #[error("Crypto Error: {0}")] + CryptoError(#[from] identity_crypto::Error), + #[error("Failed to encode JSON: {0}")] + EncodeJSON(serde_json::Error), + #[error("Failed to decode JSON: {0}")] + DecodeJSON(serde_json::Error), + #[error("Invalid Timestamp: {0}")] + InvalidTimestamp(#[from] chrono::ParseError), + #[error("Failed to decode base16 data: {0}")] + DecodeBase16(#[from] hex::FromHexError), + #[error("Failed to decode base58 data: {0}")] + DecodeBase58(#[from] bs58::decode::Error), + #[error("Failed to decode base64 data: {0}")] + DecodeBase64(#[from] base64::DecodeError), + #[error("Invalid object id")] + InvalidObjectId, + #[error("Invalid object type")] + InvalidObjectType, + #[error("Invalid key type")] + InvalidKeyType, + #[error("Invalid Credential: {0}")] + InvalidCredential(String), + #[error("Invalid Presentation: {0}")] + InvalidPresentation(String), + #[error("Invalid Url: {0}")] + InvalidUrl(#[from] url::ParseError), + #[error("Invalid UTF-8: {0}")] + InvalidUtf8(#[from] core::str::Utf8Error), + #[error("DID Resolution Error: {0}")] + ResolutionError(anyhow::Error), + #[error("DID Dereference Error: {0}")] + DereferenceError(anyhow::Error), + #[error("Identity Reader Error: {0}")] + IdentityReaderError(anyhow::Error), + #[error("Identity Writer Error: {0}")] + IdentityWriterError(anyhow::Error), } - -/// The main crate result type derived from the `anyhow::Result` type. -pub type Result = AnyhowResult; diff --git a/identity_core/src/key/key_data.rs b/identity_core/src/key/key_data.rs new file mode 100644 index 0000000000..609578b342 --- /dev/null +++ b/identity_core/src/key/key_data.rs @@ -0,0 +1,15 @@ +use identity_diff::Diff; +use serde::{Deserialize, Serialize}; + +use crate::common::Object; + +/// Encoding method used for the specified public key. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Diff)] +#[serde(rename_all = "camelCase")] +pub enum KeyData { + EthereumAddress(String), + PublicKeyHex(String), + PublicKeyJwk(Object), // TODO: Replace this with libjose type + PublicKeyBase58(String), + PublicKeyPem(String), +} diff --git a/identity_core/src/key/key_index.rs b/identity_core/src/key/key_index.rs new file mode 100644 index 0000000000..ad88737791 --- /dev/null +++ b/identity_core/src/key/key_index.rs @@ -0,0 +1,35 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum KeyIndex<'i> { + Index(usize), + Ident(&'i str), +} + +impl<'i> From<&'i str> for KeyIndex<'i> { + fn from(other: &'i str) -> Self { + Self::Ident(other) + } +} + +impl From for KeyIndex<'_> { + fn from(other: usize) -> Self { + Self::Index(other) + } +} + +impl PartialEq for KeyIndex<'_> { + fn eq(&self, other: &usize) -> bool { + match self { + Self::Index(index) => index == other, + Self::Ident(_) => false, + } + } +} + +impl PartialEq<&'_ str> for KeyIndex<'_> { + fn eq(&self, other: &&'_ str) -> bool { + match self { + Self::Index(_) => false, + Self::Ident(ident) => ident == other, + } + } +} diff --git a/identity_core/src/key/key_relation.rs b/identity_core/src/key/key_relation.rs new file mode 100644 index 0000000000..c9d75abf49 --- /dev/null +++ b/identity_core/src/key/key_relation.rs @@ -0,0 +1,15 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum KeyRelation { + VerificationMethod, + Authentication, + AssertionMethod, + KeyAgreement, + CapabilityInvocation, + CapabilityDelegation, +} + +impl Default for KeyRelation { + fn default() -> Self { + Self::VerificationMethod + } +} diff --git a/identity_core/src/key/key_type.rs b/identity_core/src/key/key_type.rs new file mode 100644 index 0000000000..df5f1e7fa7 --- /dev/null +++ b/identity_core/src/key/key_type.rs @@ -0,0 +1,73 @@ +use core::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, + str::FromStr, +}; +use identity_diff::Diff; +use serde::{Deserialize, Serialize}; + +use crate::error::{Error, Result}; + +/// Possible key types within a DID document. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Diff)] +pub enum KeyType { + JsonWebKey2020, + EcdsaSecp256k1VerificationKey2019, + Ed25519VerificationKey2018, + GpgVerificationKey2020, + RsaVerificationKey2018, + X25519KeyAgreementKey2019, + SchnorrSecp256k1VerificationKey2019, + EcdsaSecp256k1RecoveryMethod2020, +} + +impl KeyType { + pub fn try_from_str(string: &str) -> Result { + match string { + "JsonWebKey2020" => Ok(Self::JsonWebKey2020), + "Secp256k1VerificationKey2018" => Ok(Self::EcdsaSecp256k1VerificationKey2019), + "Ed25519VerificationKey2018" => Ok(Self::Ed25519VerificationKey2018), + "GpgVerificationKey2020" => Ok(Self::GpgVerificationKey2020), + "RsaVerificationKey2018" => Ok(Self::RsaVerificationKey2018), + "X25519KeyAgreementKey2019" => Ok(Self::X25519KeyAgreementKey2019), + "SchnorrSecp256k1VerificationKey2019" => Ok(Self::SchnorrSecp256k1VerificationKey2019), + "EcdsaSecp256k1RecoveryMethod2020" => Ok(Self::EcdsaSecp256k1RecoveryMethod2020), + _ => Err(Error::InvalidKeyType), + } + } + + pub const fn as_str(&self) -> &'static str { + match self { + Self::JsonWebKey2020 => "JsonWebKey2020", + Self::EcdsaSecp256k1VerificationKey2019 => "Secp256k1VerificationKey2018", + Self::Ed25519VerificationKey2018 => "Ed25519VerificationKey2018", + Self::GpgVerificationKey2020 => "GpgVerificationKey2020", + Self::RsaVerificationKey2018 => "RsaVerificationKey2018", + Self::X25519KeyAgreementKey2019 => "X25519KeyAgreementKey2019", + Self::SchnorrSecp256k1VerificationKey2019 => "SchnorrSecp256k1VerificationKey2019", + Self::EcdsaSecp256k1RecoveryMethod2020 => "X25519KeyAgreementKey2019", + } + } +} + +impl Display for KeyType { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_str(self.as_str()) + } +} + +impl FromStr for KeyType { + type Err = Error; + + fn from_str(string: &str) -> Result { + Self::try_from_str(string) + } +} + +impl TryFrom<&'_ str> for KeyType { + type Error = Error; + + fn try_from(other: &'_ str) -> Result { + Self::try_from_str(other) + } +} diff --git a/identity_core/src/key/mod.rs b/identity_core/src/key/mod.rs new file mode 100644 index 0000000000..f8e70d3dc2 --- /dev/null +++ b/identity_core/src/key/mod.rs @@ -0,0 +1,11 @@ +mod key_data; +mod key_index; +mod key_relation; +mod key_type; +mod public_key; + +pub use key_data::*; +pub use key_index::*; +pub use key_relation::*; +pub use key_type::*; +pub use public_key::*; diff --git a/identity_core/src/key/public_key.rs b/identity_core/src/key/public_key.rs new file mode 100644 index 0000000000..92eaf77775 --- /dev/null +++ b/identity_core/src/key/public_key.rs @@ -0,0 +1,174 @@ +use derive_builder::Builder; +use identity_diff::{self as diff, Diff}; +use serde::{Deserialize, Serialize}; + +use crate::{ + did::DID, + key::{KeyData, KeyType}, + utils::HasId, +}; + +/// Public key struct. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Builder)] +#[builder(pattern = "owned")] +pub struct PublicKey { + #[builder(try_setter)] + id: DID, + #[builder(try_setter)] + controller: DID, + #[serde(rename = "type")] + #[builder(try_setter)] + key_type: KeyType, + #[serde(flatten)] + key_data: KeyData, +} + +impl PublicKey { + pub fn default_key_type() -> KeyType { + KeyType::Ed25519VerificationKey2018 + } + + pub fn default_key_data() -> KeyData { + KeyData::PublicKeyHex("".into()) + } + + pub fn id(&self) -> &DID { + &self.id + } + + pub fn controller(&self) -> &DID { + &self.controller + } + + pub fn key_type(&self) -> KeyType { + self.key_type + } + + pub fn key_data(&self) -> &KeyData { + &self.key_data + } +} + +impl Default for PublicKey { + fn default() -> Self { + Self { + id: Default::default(), + controller: Default::default(), + key_type: Self::default_key_type(), + key_data: Self::default_key_data(), + } + } +} + +impl HasId for PublicKey { + type Id = DID; + + fn id(&self) -> &Self::Id { + &self.id + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(from = "PublicKey", into = "PublicKey")] +pub struct DiffPublicKey { + #[serde(skip_serializing_if = "Option::is_none")] + id: Option<::Type>, + #[serde(skip_serializing_if = "Option::is_none")] + controller: Option<::Type>, + #[serde(skip_serializing_if = "Option::is_none")] + key_type: Option<::Type>, + #[serde(skip_serializing_if = "Option::is_none")] + key_data: Option<::Type>, +} + +impl Diff for PublicKey { + type Type = DiffPublicKey; + + fn merge(&self, diff: Self::Type) -> Result { + Ok(Self { + id: diff + .id + .map(|value| self.id.merge(value)) + .transpose()? + .unwrap_or_else(|| self.id.clone()), + controller: diff + .controller + .map(|value| self.controller.merge(value)) + .transpose()? + .unwrap_or_else(|| self.controller.clone()), + key_type: diff + .key_type + .map(|value| self.key_type.merge(value)) + .transpose()? + .unwrap_or_else(|| self.key_type), + key_data: diff + .key_data + .map(|value| self.key_data.merge(value)) + .transpose()? + .unwrap_or_else(|| self.key_data.clone()), + }) + } + + fn diff(&self, other: &Self) -> Result { + Ok(DiffPublicKey { + id: if self.id == other.id { + None + } else { + Some(self.id.diff(&other.id)?) + }, + controller: if self.controller == other.controller { + None + } else { + Some(self.controller.diff(&other.controller)?) + }, + key_type: if self.key_type == other.key_type { + None + } else { + Some(self.key_type.diff(&other.key_type)?) + }, + key_data: if self.key_data == other.key_data { + None + } else { + Some(self.key_data.diff(&other.key_data)?) + }, + }) + } + + fn from_diff(diff: Self::Type) -> Result { + Ok(Self { + id: diff.id.map(::from_diff).transpose()?.unwrap_or_default(), + controller: diff.controller.map(::from_diff).transpose()?.unwrap_or_default(), + key_type: diff + .key_type + .map(::from_diff) + .transpose()? + .unwrap_or_else(Self::default_key_type), + key_data: diff + .key_data + .map(::from_diff) + .transpose()? + .unwrap_or_else(Self::default_key_data), + }) + } + + fn into_diff(self) -> Result { + Ok(DiffPublicKey { + id: Some(self.id.into_diff()?), + controller: Some(self.controller.into_diff()?), + key_type: Some(self.key_type.into_diff()?), + key_data: Some(self.key_data.into_diff()?), + }) + } +} + +impl From for DiffPublicKey { + fn from(other: PublicKey) -> Self { + other.into_diff().expect("Unable to convert to diff") + } +} + +impl From for PublicKey { + fn from(other: DiffPublicKey) -> Self { + Self::from_diff(other).expect("Unable to convert from diff") + } +} diff --git a/identity_core/src/lib.rs b/identity_core/src/lib.rs index 2fb5074e9a..a9b10774b4 100644 --- a/identity_core/src/lib.rs +++ b/identity_core/src/lib.rs @@ -1,7 +1,14 @@ +#[macro_use] +pub mod common; + pub mod did; -pub mod did_parser; -pub mod document; -mod error; +pub mod error; +pub mod key; +pub mod resolver; pub mod utils; +pub mod vc; + +// Re-export the `identity_diff` crate as `diff` +pub use identity_diff as diff; pub use error::{Error, Result}; diff --git a/identity_core/src/resolver/dereference.rs b/identity_core/src/resolver/dereference.rs new file mode 100644 index 0000000000..4844c1e728 --- /dev/null +++ b/identity_core/src/resolver/dereference.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +use crate::resolver::{DocumentMetadata, ResolutionMetadata, Resource}; + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct Dereference { + #[serde(rename = "did-url-dereferencing-metadata")] + pub metadata: ResolutionMetadata, + #[serde(rename = "content-stream", skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(rename = "content-metadata", skip_serializing_if = "Option::is_none")] + pub content_metadata: Option, +} + +impl Dereference { + pub fn new() -> Self { + Self { + metadata: ResolutionMetadata::new(), + content: None, + content_metadata: None, + } + } +} diff --git a/identity_core/src/resolver/document_metadata.rs b/identity_core/src/resolver/document_metadata.rs new file mode 100644 index 0000000000..3c21d40991 --- /dev/null +++ b/identity_core/src/resolver/document_metadata.rs @@ -0,0 +1,29 @@ +use crate::common::{Object, Timestamp}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct DocumentMetadata { + /// The timestamp of the Create operation. + /// + /// [More Info](https://www.w3.org/TR/did-spec-registries/#created) + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, + /// The timestamp of the last Update operation. + /// + /// [More Info](https://www.w3.org/TR/did-spec-registries/#updated) + #[serde(skip_serializing_if = "Option::is_none")] + pub updated: Option, + /// Additional document metadata properties. + #[serde(flatten)] + pub properties: Object, +} + +impl DocumentMetadata { + pub fn new() -> Self { + Self { + created: None, + updated: None, + properties: Object::new(), + } + } +} diff --git a/identity_core/src/resolver/error_kind.rs b/identity_core/src/resolver/error_kind.rs new file mode 100644 index 0000000000..20b82409ff --- /dev/null +++ b/identity_core/src/resolver/error_kind.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub enum ErrorKind { + /// The DID supplied to the DID resolution function does not conform to + /// valid syntax. + #[serde(rename = "invalid-did")] + InvalidDID, + /// The DID resolver does not support the specified method. + #[serde(rename = "not-supported")] + NotSupported, + /// The DID resolver was unable to return the DID document resulting from + /// this resolution request. + #[serde(rename = "not-found")] + NotFound, +} diff --git a/identity_core/src/resolver/impls.rs b/identity_core/src/resolver/impls.rs new file mode 100644 index 0000000000..3fec604ba9 --- /dev/null +++ b/identity_core/src/resolver/impls.rs @@ -0,0 +1,330 @@ +use anyhow::anyhow; +use percent_encoding::percent_decode_str; +use std::{collections::BTreeMap, time::Instant}; + +use crate::{ + common::Url, + did::{DIDDocument as Document, Param, DID}, + error::{Error, Result}, + resolver::{ + Dereference, DocumentMetadata, ErrorKind, InputMetadata, MetaDocument, PrimaryResource, Resolution, + ResolverMethod, Resource, SecondaryResource, + }, + utils::HasId as _, +}; + +pub async fn resolve(did: &str, input: InputMetadata, method: R) -> Result +where + R: ResolverMethod, +{ + let mut context: ResolveContext = ResolveContext::new(); + + // 1. Validate that the input DID conforms to the did rule of the DID Syntax. + let did: DID = match did.parse() { + Ok(did) => did, + Err(_) => return Ok(context.finish_error(ErrorKind::InvalidDID)), + }; + + // 2. Determine if the input DID method is supported by the DID resolver + // that implements this algorithm. + if !method.is_supported(&did) { + return Ok(context.finish_error(ErrorKind::NotSupported)); + } + + // 3. Obtain the DID document for the input DID by executing the Read + // operation against the input DID's verifiable data registry. + let doc: MetaDocument = match method.read(&did, input).await? { + Some(doc) => doc, + None => return Ok(context.finish_error(ErrorKind::NotFound)), + }; + + // 4. Validate that the output DID document conforms to a conformant + // serialization of the DID document data model. + // if did.method() != doc.data.id.method() || did.method_id() != doc.data.id.method_id() { + // return Ok(context.finish_error(ErrorKind::InvalidDID)); + // } + + // TODO: Handle deactivated DIDs + // TODO: Handle signature verification + + context.set_document(doc.data); + context.set_metadata(doc.meta); + context.set_resolved(did); + + Ok(context.finish()) +} + +pub async fn dereference(did: &str, input: InputMetadata, method: R) -> Result +where + R: ResolverMethod, +{ + // 1. Obtain the DID document for the input DID by executing the DID + // resolution algorithm. + let resolution: Resolution = resolve(did, input, method).await?; + let mut context: DerefContext = DerefContext::new(); + + // If the resolution result contains an error, bail early. + if let Some(error) = resolution.metadata.error { + return Ok(context.finish_error(error)); + } + + // Extract the document and metadata - Both properties MUST exist as we + // checked for resolution errors above. + let (document, metadata): (Document, DocumentMetadata) = resolution + .document + .zip(resolution.document_metadata) + .ok_or_else(|| Error::DereferenceError(anyhow!("Missing Document/Metadata")))?; + + // Extract the parsed DID from the resolution output - It MUST exist as we + // checked for resolution errors above. + let did: DID = resolution + .metadata + .resolved + .ok_or_else(|| Error::DereferenceError(anyhow!("Missing Resolved DID")))?; + + // Add the resolution document metadata to the response. + context.set_metadata(metadata); + + // 2. Execute the algorithm for Dereferencing the Primary Resource. + let primary: PrimaryResource = match dereference_primary(document, did.clone())? { + Some(primary) => primary, + None => return Ok(context.finish_error(ErrorKind::NotFound)), + }; + + // 3. If the original input DID URL contained a DID fragment, execute the + // algorithm for Dereferencing the Secondary Resource. + if let Some(fragment) = did.fragment.as_deref() { + // + // Dereferencing the Secondary Resource + // + match primary { + // 1. If the result is a resolved DID document. + PrimaryResource::Document(inner) => { + // 1.1 From the resolved DID document, select the JSON object whose id + // property matches the input DID URL. + if let Some(resource) = dereference_document(inner, fragment)? { + // 1.2. Return the output resource. + context.set_content(resource); + } + } + // 2. Otherwise, if the result is an output service endpoint URL. + PrimaryResource::Service(mut inner) => { + // 2.1. Append the DID fragment to the output service endpoint URL. + inner.set_fragment(Some(fragment)); + + // 2.2. Return the output service endpoint URL. + context.set_content(PrimaryResource::Service(inner)); + } + } + } else { + context.set_content(primary); + } + + Ok(context.finish()) +} + +#[derive(Debug)] +struct ResolveContext(Resolution, Instant); + +impl ResolveContext { + fn new() -> Self { + Self(Resolution::new(), Instant::now()) + } + + fn set_document(&mut self, value: Document) { + self.0.document = Some(value); + } + + fn set_metadata(&mut self, value: DocumentMetadata) { + self.0.document_metadata = Some(value); + } + + fn set_resolved(&mut self, value: DID) { + self.0.metadata.resolved = Some(value); + } + + fn set_error(&mut self, value: ErrorKind) { + self.0.metadata.error = Some(value); + } + + fn finish_error(mut self, value: ErrorKind) -> Resolution { + self.set_error(value); + self.finish() + } + + fn finish(mut self) -> Resolution { + self.0.metadata.duration = self.1.elapsed(); + self.0 + } +} + +#[derive(Debug)] +struct DerefContext(Dereference, Instant); + +impl DerefContext { + fn new() -> Self { + Self(Dereference::new(), Instant::now()) + } + + fn set_content(&mut self, value: impl Into) { + self.0.content = Some(value.into()); + } + + fn set_metadata(&mut self, value: DocumentMetadata) { + self.0.content_metadata = Some(value); + } + + fn set_error(&mut self, value: ErrorKind) { + self.0.metadata.error = Some(value); + } + + fn finish_error(mut self, value: ErrorKind) -> Dereference { + self.set_error(value); + self.finish() + } + + fn finish(mut self) -> Dereference { + self.0.metadata.duration = self.1.elapsed(); + self.0 + } +} + +fn dereference_primary(document: Document, mut did: DID) -> Result> { + // Remove the DID fragment from the input DID URL. + did.fragment = None; + + // Parse and collect the query, for convenience. + let params: BTreeMap<&str, &str> = did.query.iter().flatten().map(|param| param.pair()).collect(); + + // 1. If the input DID URL contains the DID parameter service... + if let Some(target) = params.get("service").copied() { + // 1.1. From the resolved DID document, select the service endpoint whose + // id property contains a fragment which matches the value of the + // service DID parameter of the input DID URL. + document + .services + .iter() + .find(|service| matches!(service.id().fragment.as_deref(), Some(fragment) if fragment == target)) + .map(|service| service.endpoint().context()) + // 1.2. Execute the Service Endpoint Construction algorithm. + .map(|url| service_endpoint_ctor(did, url)) + .transpose()? + // 1.3. Return the output service endpoint URL. + .map(Into::into) + .map(Ok) + .transpose() + // 3. Otherwise, if the input DID URL contains no DID path and no DID query. + } else if did.path_segments.is_none() && did.query.is_none() { + // 3.1. Return the resolved DID document. + Ok(Some(document.into())) + } else { + todo!("Handle Method-Specific Dereference") + } +} + +fn dereference_document(document: Document, fragment: &str) -> Result> { + macro_rules! extract { + ($base:expr, $target:expr, $iter:expr) => { + for object in $iter { + let did: DID = DID::join_relative($base, object.id())?; + + if matches!(did.fragment.as_deref(), Some(fragment) if fragment == $target) { + return Ok(Some(object.into())); + } + } + }; + } + + extract!(&document.id, fragment, document.public_keys); + extract!(&document.id, fragment, document.verification); + extract!(&document.id, fragment, document.auth); + extract!(&document.id, fragment, document.assert); + extract!(&document.id, fragment, document.agreement); + extract!(&document.id, fragment, document.delegation); + extract!(&document.id, fragment, document.invocation); + extract!(&document.id, fragment, document.services); + + Ok(None) +} + +// Service Endpoint Construction +// +// [Ref](https://w3c-ccg.github.io/did-resolution/#service-endpoint-construction) +fn service_endpoint_ctor(did: DID, url: &Url) -> Result { + // The input DID URL and input service endpoint URL MUST NOT both have a + // query component. + if did.query.is_some() && url.query().is_some() { + return Err(Error::DereferenceError(anyhow!("Multiple DID Queries"))); + } + + // The input DID URL and input service endpoint URL MUST NOT both have a + // fragment component. + if did.fragment.is_some() && url.fragment().is_some() { + return Err(Error::DereferenceError(anyhow!("Multiple DID Fragments"))); + } + + // The input service endpoint URL MUST be an HTTP(S) URL. + if url.scheme() != "https" { + return Err(Error::DereferenceError(anyhow!("Invalid Service Protocol"))); + } + + // 1. Initialize a string output service endpoint URL to the value of + // the input service endpoint URL. + let mut output: Url = url.clone(); + + // 2. If the output service endpoint URL has a query component, remove it. + output.set_query(None); + + // 3. If the output service endpoint URL has a fragment component, remove it. + output.set_fragment(None); + + // Decode and join the `relative-ref` query param, if it exists. + let relative: Option<_> = did + .query + .as_deref() + .unwrap_or_default() + .iter() + .find(|param| param.key == "relative-ref") + .and_then(|param| param.value.as_deref()) + .filter(|value| !value.is_empty()) + .map(|value| percent_decode_str(value).decode_utf8()) + .transpose()?; + + if let Some(relative) = relative { + output = output.join(&relative)?; + } + + // 4. Append the path component of the input DID URL to the output + // service endpoint URL. + if let Some(segments) = did.path_segments.as_deref() { + output.path_segments_mut().unwrap().extend(segments); + } + + // 5. If the input service endpoint URL has a query component, append ? + // plus the query to the output service endpoint URL. + // 6. If the input DID URL has a query component, append ? plus the + // query to the output service endpoint URL. + match (did.query.as_deref(), url.query().map(|_| url.query_pairs())) { + (Some(params), None) => { + output.query_pairs_mut().extend_pairs(params.iter().map(Param::pair)); + } + (None, Some(query)) => { + output.query_pairs_mut().extend_pairs(query); + } + (Some(_), Some(_)) => unreachable!(), + (None, None) => {} + } + + // 7. If the input service endpoint URL has a fragment component, append + // # plus the fragment to the output service endpoint URL. + // 8. If the input DID URL has a fragment component, append # plus the + // fragment to the output service endpoint URL. + match (did.fragment.as_deref(), url.fragment()) { + (fragment @ Some(_), None) | (None, fragment @ Some(_)) => output.set_fragment(fragment), + (Some(_), Some(_)) => unreachable!(), + (None, None) => {} + } + + // 9. Return the output service endpoint URL. + Ok(output) +} diff --git a/identity_core/src/resolver/input_metadata.rs b/identity_core/src/resolver/input_metadata.rs new file mode 100644 index 0000000000..49cfdedd04 --- /dev/null +++ b/identity_core/src/resolver/input_metadata.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +use crate::common::Object; + +pub const MIME_ANY: &str = "*/*"; +pub const MIME_DID: &str = "application/did+json"; +pub const MIME_DID_LD: &str = "application/did+ld+json"; + +// TODO: Support versioning via `version-id`/`version-time` +// TODO: Support caching via `no-cache` +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct InputMetadata { + /// The MIME type of the preferred representation of the DID document. + /// + /// Note: This is only relevant when using stream-based resolution. + /// + /// [More Info](https://www.w3.org/TR/did-spec-registries/#accept) + #[serde(skip_serializing_if = "Option::is_none")] + pub accept: Option, + /// Additional input metadata properties. + #[serde(flatten)] + pub properties: Object, +} + +impl InputMetadata { + pub fn new() -> Self { + Self { + accept: None, + properties: Object::new(), + } + } +} diff --git a/identity_core/src/resolver/mod.rs b/identity_core/src/resolver/mod.rs new file mode 100644 index 0000000000..2a99c203ff --- /dev/null +++ b/identity_core/src/resolver/mod.rs @@ -0,0 +1,19 @@ +mod dereference; +mod document_metadata; +mod error_kind; +mod impls; +mod input_metadata; +mod resolution; +mod resolution_metadata; +mod resource; +mod traits; + +pub use dereference::*; +pub use document_metadata::*; +pub use error_kind::*; +pub use impls::*; +pub use input_metadata::*; +pub use resolution::*; +pub use resolution_metadata::*; +pub use resource::*; +pub use traits::*; diff --git a/identity_core/src/resolver/resolution.rs b/identity_core/src/resolver/resolution.rs new file mode 100644 index 0000000000..054200c70a --- /dev/null +++ b/identity_core/src/resolver/resolution.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + did::DIDDocument as Document, + resolver::{DocumentMetadata, ResolutionMetadata}, +}; + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct Resolution { + #[serde(rename = "did-resolution-metadata")] + pub metadata: ResolutionMetadata, + #[serde(rename = "did-document", skip_serializing_if = "Option::is_none")] + pub document: Option, + #[serde(rename = "did-document-metadata", skip_serializing_if = "Option::is_none")] + pub document_metadata: Option, +} + +impl Resolution { + pub fn new() -> Self { + Self { + metadata: ResolutionMetadata::new(), + document: None, + document_metadata: None, + } + } +} diff --git a/identity_core/src/resolver/resolution_metadata.rs b/identity_core/src/resolver/resolution_metadata.rs new file mode 100644 index 0000000000..8c6961fa89 --- /dev/null +++ b/identity_core/src/resolver/resolution_metadata.rs @@ -0,0 +1,40 @@ +use core::time::Duration; +use serde::{Deserialize, Serialize}; + +use crate::{common::Object, did::DID, resolver::ErrorKind}; + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct ResolutionMetadata { + /// The error code from the resolution process, if an error occurred. + /// + /// [More Info](https://www.w3.org/TR/did-spec-registries/#error) + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// The MIME type of the returned document stream. + /// + /// Note: This is only relevant when using stream-based resolution. + /// + /// [More Info](https://www.w3.org/TR/did-spec-registries/#content-type) + #[serde(skip_serializing_if = "Option::is_none")] + pub content_type: Option, + /// The elapsed time of the resolution operation. + pub duration: Duration, + /// The parsed DID that was used for resolution. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolved: Option, + /// Additional resolution metadata properties. + #[serde(flatten)] + pub properties: Object, +} + +impl ResolutionMetadata { + pub fn new() -> Self { + Self { + error: None, + content_type: None, + duration: Duration::from_secs(0), + resolved: None, + properties: Object::new(), + } + } +} diff --git a/identity_core/src/resolver/resource.rs b/identity_core/src/resolver/resource.rs new file mode 100644 index 0000000000..4d1cbae9c0 --- /dev/null +++ b/identity_core/src/resolver/resource.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + common::Url, + did::{Authentication, DIDDocument as Document, Service, DID}, + key::PublicKey, +}; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub enum Resource { + Primary(PrimaryResource), + Secondary(SecondaryResource), +} + +impl From for Resource { + fn from(other: PrimaryResource) -> Self { + Self::Primary(other) + } +} + +impl From for Resource { + fn from(other: SecondaryResource) -> Self { + Self::Secondary(other) + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum PrimaryResource { + Document(Document), + Service(Url), +} + +impl From for PrimaryResource { + fn from(other: Document) -> Self { + Self::Document(other) + } +} + +impl From for PrimaryResource { + fn from(other: Url) -> Self { + Self::Service(other) + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum SecondaryResource { + VerificationDID(DID), + VerificationKey(PublicKey), + Service(Service), +} + +impl From for SecondaryResource { + fn from(other: DID) -> Self { + Self::VerificationDID(other) + } +} + +impl From for SecondaryResource { + fn from(other: PublicKey) -> Self { + Self::VerificationKey(other) + } +} + +impl From for SecondaryResource { + fn from(other: Authentication) -> Self { + match other { + Authentication::DID(inner) => inner.into(), + Authentication::Key(inner) => inner.into(), + } + } +} + +impl From for SecondaryResource { + fn from(other: Service) -> Self { + Self::Service(other) + } +} diff --git a/identity_core/src/resolver/traits.rs b/identity_core/src/resolver/traits.rs new file mode 100644 index 0000000000..5f3d894ae9 --- /dev/null +++ b/identity_core/src/resolver/traits.rs @@ -0,0 +1,35 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::{ + did::{DIDDocument as Document, DID}, + error::Result, + resolver::{DocumentMetadata, InputMetadata}, +}; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct MetaDocument { + pub data: Document, + pub meta: DocumentMetadata, +} + +#[async_trait] +pub trait ResolverMethod { + fn is_supported(&self, did: &DID) -> bool; + + async fn read(&self, did: &DID, input: InputMetadata) -> Result>; +} + +#[async_trait] +impl ResolverMethod for &'_ T +where + T: ResolverMethod + Send + Sync, +{ + fn is_supported(&self, did: &DID) -> bool { + (**self).is_supported(did) + } + + async fn read(&self, did: &DID, input: InputMetadata) -> Result> { + (**self).read(did, input).await + } +} diff --git a/identity_core/src/utils.rs b/identity_core/src/utils.rs deleted file mode 100644 index 9c20f4f998..0000000000 --- a/identity_core/src/utils.rs +++ /dev/null @@ -1,61 +0,0 @@ -mod authentication; -mod context; -pub mod helpers; -mod keys; -mod service; -mod service_serialize; -mod subject; - -use identity_diff::Diff; -use serde::{Deserialize, Serialize}; -use std::hash::Hash; - -pub use authentication::Authentication; -pub use context::Context; -pub use keys::{KeyData, PublicKey, PublicKeyTypes}; -pub use service::{Service, ServiceEndpoint}; -pub use subject::Subject; - -pub trait Dedup { - fn clear_duplicates(&mut self); -} - -pub trait HasId { - type Id: Hash + PartialEq + Eq; - - fn id(&self) -> &Self::Id; -} - -#[derive(Hash, PartialEq, Eq, Debug, Serialize, Deserialize, Clone, PartialOrd, Ord, Diff, Default)] -pub struct IdCompare(pub T); - -impl IdCompare -where - T: HasId, -{ - pub fn new(item: T) -> Self { - Self(item) - } -} - -impl HasId for IdCompare -where - T: HasId, -{ - type Id = T::Id; - - fn id(&self) -> &Self::Id { - self.0.id() - } -} - -pub fn add_unique_to_vec(item: T, collection: Vec) -> Vec { - let mut collection: Vec = collection - .into_iter() - .filter(|it| it.id() != item.id()) - .collect::>(); - - collection.push(item); - - collection -} diff --git a/identity_core/src/utils/add_unique.rs b/identity_core/src/utils/add_unique.rs new file mode 100644 index 0000000000..fc593ce00c --- /dev/null +++ b/identity_core/src/utils/add_unique.rs @@ -0,0 +1,35 @@ +use crate::utils::HasId; + +pub trait AddUnique +where + T: HasId, +{ + fn add_unique(&mut self, item: T); + fn set_unique(&mut self, item: T); +} + +impl AddUnique for Vec +where + T: HasId, +{ + fn add_unique(&mut self, item: T) { + for it in self.iter() { + if it.id() == item.id() { + return; + } + } + + self.push(item); + } + + fn set_unique(&mut self, item: T) { + for it in self.iter_mut() { + if it.id() == item.id() { + *it = item; + return; + } + } + + self.push(item); + } +} diff --git a/identity_core/src/utils/authentication.rs b/identity_core/src/utils/authentication.rs deleted file mode 100644 index 11171b7291..0000000000 --- a/identity_core/src/utils/authentication.rs +++ /dev/null @@ -1,30 +0,0 @@ -use identity_diff::Diff; -use serde::{Deserialize, Serialize}; - -use crate::utils::{HasId, PublicKey, Subject}; - -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, PartialEq, Diff, Serialize, Deserialize)] -#[serde(untagged)] -#[diff(from_into)] -pub enum Authentication { - Method(Subject), - Key(PublicKey), -} - -impl Default for Authentication { - fn default() -> Self { - Self::Method(Subject::default()) - } -} - -impl HasId for Authentication { - type Id = Subject; - - fn id(&self) -> &Self::Id { - match self { - Authentication::Method(subject) => subject, - Authentication::Key(key) => &key.id, - } - } -} diff --git a/identity_core/src/utils/context.rs b/identity_core/src/utils/context.rs deleted file mode 100644 index 166167198b..0000000000 --- a/identity_core/src/utils/context.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::{hash::Hash, str::FromStr}; - -use identity_diff::Diff; -use serde::{ - ser::{Serialize, SerializeSeq, Serializer}, - Deserialize, -}; - -/// A context type. Contains a Vector of Strings which describe the DID context. -#[derive(Debug, PartialEq, Eq, Deserialize, Clone, Hash, Diff, PartialOrd, Ord)] -#[serde(transparent)] -pub struct Context(Vec); - -impl Context { - /// creates a new context. requires an `inner` value. - pub fn new(inner: Vec) -> Self { - Self(inner) - } - - /// gets the inner value of the context. - pub fn as_inner(&self) -> &Vec { - &self.0 - } - - /// checks if the context is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// add a context to an existing context. - pub fn add_context(&mut self, s: String) -> crate::Result { - self.0.push(s); - - Ok(self.clone()) - } -} - -/// Default context to "https://www.w3.org/ns/did/v1" as per the standard. -impl Default for Context { - fn default() -> Self { - Context(vec!["https://www.w3.org/ns/did/v1".into()]) - } -} - -impl FromStr for Context { - type Err = crate::Error; - - fn from_str(s: &str) -> crate::Result { - Ok(Context(vec![s.into()])) - } -} - -impl Serialize for Context { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self.0.len() { - 0 => serializer.serialize_none(), - 1 => serializer.serialize_str(&self.0[0]), - _ => { - let mut seq = serializer.serialize_seq(Some(self.0.len()))?; - for ch in &self.0 { - seq.serialize_element(&ch)?; - } - seq.end() - } - } - } -} - -impl From<&str> for Context { - fn from(s: &str) -> Context { - Context(vec![s.into()]) - } -} - -impl From for Context { - fn from(s: String) -> Context { - Context(vec![s]) - } -} diff --git a/identity_proof/src/utils/encoding.rs b/identity_core/src/utils/encoding.rs similarity index 71% rename from identity_proof/src/utils/encoding.rs rename to identity_core/src/utils/encoding.rs index 11c63a5384..4a9eddab76 100644 --- a/identity_proof/src/utils/encoding.rs +++ b/identity_core/src/utils/encoding.rs @@ -8,6 +8,14 @@ pub fn encode_b58(data: &(impl AsRef<[u8]> + ?Sized)) -> String { bs58::encode(data.as_ref()).into_string() } +pub fn decode_hex(data: &(impl AsRef + ?Sized)) -> Result> { + hex::decode(data.as_ref()).map_err(Error::DecodeBase16) +} + +pub fn encode_hex(data: &(impl AsRef<[u8]> + ?Sized)) -> String { + hex::encode(data.as_ref()) +} + pub fn decode_b64(data: &(impl AsRef + ?Sized)) -> Result> { base64::decode_config(data.as_ref(), base64::URL_SAFE_NO_PAD).map_err(Error::DecodeBase64) } diff --git a/identity_core/src/utils/has_id.rs b/identity_core/src/utils/has_id.rs new file mode 100644 index 0000000000..007383e4b6 --- /dev/null +++ b/identity_core/src/utils/has_id.rs @@ -0,0 +1,7 @@ +use core::hash::Hash; + +pub trait HasId { + type Id: Hash + PartialEq + Eq + PartialOrd + Ord; + + fn id(&self) -> &Self::Id; +} diff --git a/identity_core/src/utils/helpers.rs b/identity_core/src/utils/helpers.rs deleted file mode 100644 index 4cf2ff5ca7..0000000000 --- a/identity_core/src/utils/helpers.rs +++ /dev/null @@ -1,42 +0,0 @@ -use serde::{ - de::{self, SeqAccess, Visitor}, - Deserialize, Deserializer, -}; - -use std::{fmt, marker::PhantomData, str::FromStr}; - -/// deserializes the data into either a List or a String depending on how many elements are in the data. -pub fn string_or_list<'de, T, D>(deserializer: D) -> Result -where - T: Deserialize<'de> + FromStr, - D: Deserializer<'de>, -{ - deserializer.deserialize_any(StringOrList(PhantomData)) -} - -struct StringOrList(PhantomData T>); - -impl<'de, T> Visitor<'de> for StringOrList -where - T: Deserialize<'de> + FromStr, -{ - type Value = T; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("string or list") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - Ok(FromStr::from_str(value).unwrap()) - } - - fn visit_seq(self, seq: S) -> Result - where - S: SeqAccess<'de>, - { - Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq)) - } -} diff --git a/identity_core/src/utils/keys.rs b/identity_core/src/utils/keys.rs deleted file mode 100644 index 9219e38f6d..0000000000 --- a/identity_core/src/utils/keys.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{hash::Hash, str::FromStr}; - -use identity_diff::Diff; -use serde::{Deserialize, Serialize}; - -use crate::utils::{HasId, Subject}; - -/// Public Key type enum. Can also contain a custom key type specified by the CustomKey field. -#[derive(Debug, PartialEq, Clone, Diff, Deserialize, Serialize, Eq, Hash, Ord, PartialOrd)] -pub enum PublicKeyTypes { - Ed25519VerificationKey2018, - RsaVerificationKey2018, - EcdsaSecp256k1VerificationKey2019, - JsonWebKey2020, - GpgVerificationKey2020, - X25519KeyAgreementKey2019, - EcdsaSecp256k1RecoveryMethod2020, - SchnorrSecp256k1VerificationKey2019, - UnknownKey, -} - -/// Encoding method used for the specified public key. -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Diff, Eq, Hash, Ord, PartialOrd)] -pub enum KeyData { - #[serde(rename = "publicKeyUnknown")] - Unknown(String), - #[serde(rename = "publicKeyPem")] - Pem(String), - #[serde(rename = "publicKeyJwk")] - Jwk(String), - #[serde(rename = "publicKeyHex")] - Hex(String), - #[serde(rename = "publicKeyBase64")] - Base64(String), - #[serde(rename = "publicKeyBase58")] - Base58(String), - #[serde(rename = "publicKeyMultibase")] - Multibase(String), - #[serde(rename = "iotaAddress")] - IotaAddress(String), - #[serde(rename = "ethereumAddress")] - EthereumAddress(String), -} - -/// Public key struct that contains `id`, `key_type`, `controller`, `encoding_type`, `key_data` and `reference`. -/// `reference` defines whether or not the PublicKey is a reference. -#[derive(Debug, Clone, Default, PartialEq, Diff, Deserialize, Serialize, Eq, Hash, Ord, PartialOrd)] -#[diff(from_into)] -pub struct PublicKey { - pub id: Subject, - #[serde(rename = "type")] - pub key_type: PublicKeyTypes, - pub controller: Subject, - #[serde(flatten)] - pub key_data: KeyData, - #[serde(skip)] - pub reference: bool, -} - -impl PublicKey { - pub fn init(self) -> Self { - Self { - id: self.id, - key_type: self.key_type, - controller: self.controller, - key_data: self.key_data, - reference: self.reference, - } - } -} - -impl HasId for PublicKey { - type Id = Subject; - - fn id(&self) -> &Self::Id { - &self.id - } -} - -impl Default for PublicKeyTypes { - fn default() -> Self { - PublicKeyTypes::UnknownKey - } -} - -impl Default for KeyData { - fn default() -> Self { - KeyData::Unknown(String::from("")) - } -} - -impl FromStr for PublicKey { - type Err = crate::Error; - - fn from_str(s: &str) -> crate::Result { - Ok(serde_json::from_str(s)?) - } -} - -impl ToString for PublicKey { - fn to_string(&self) -> String { - serde_json::to_string(self).expect("Unable to serialize Public Key") - } -} - -impl FromStr for PublicKeyTypes { - type Err = crate::Error; - - fn from_str(s: &str) -> crate::Result { - match s { - "RsaVerificationKey2018" => Ok(Self::RsaVerificationKey2018), - "Ed25519VerificationKey2018" => Ok(Self::Ed25519VerificationKey2018), - "Secp256k1VerificationKey2018" => Ok(Self::EcdsaSecp256k1VerificationKey2019), - "JsonWebKey2020" => Ok(Self::JsonWebKey2020), - "GpgVerificationKey2020" => Ok(Self::GpgVerificationKey2020), - "X25519KeyAgreementKey2019" => Ok(Self::X25519KeyAgreementKey2019), - "EcdsaSecp256k1RecoveryMethod2020" => Ok(Self::EcdsaSecp256k1RecoveryMethod2020), - "SchnorrSecp256k1VerificationKey2019" => Ok(Self::SchnorrSecp256k1VerificationKey2019), - _ => Ok(Self::UnknownKey), - } - } -} - -impl ToString for PublicKeyTypes { - fn to_string(&self) -> String { - match self { - PublicKeyTypes::RsaVerificationKey2018 => "RsaVerificationKey2018".into(), - PublicKeyTypes::Ed25519VerificationKey2018 => "Ed25519VerificationKey2018".into(), - PublicKeyTypes::EcdsaSecp256k1VerificationKey2019 => "Secp256k1VerificationKey2018".into(), - PublicKeyTypes::JsonWebKey2020 => "JsonWebKey2020".into(), - PublicKeyTypes::GpgVerificationKey2020 => "GpgVerificationKey2020".into(), - PublicKeyTypes::X25519KeyAgreementKey2019 => "X25519KeyAgreementKey2019".into(), - PublicKeyTypes::EcdsaSecp256k1RecoveryMethod2020 => "X25519KeyAgreementKey2019".into(), - PublicKeyTypes::SchnorrSecp256k1VerificationKey2019 => "SchnorrSecp256k1VerificationKey2019".into(), - PublicKeyTypes::UnknownKey => "".into(), - } - } -} - -impl From<&str> for PublicKeyTypes { - fn from(s: &str) -> Self { - match s { - "RsaVerificationKey2018" => Self::RsaVerificationKey2018, - "Ed25519VerificationKey2018" => Self::Ed25519VerificationKey2018, - "Secp256k1VerificationKey2018" => Self::EcdsaSecp256k1VerificationKey2019, - "JsonWebKey2020" => Self::JsonWebKey2020, - "GpgVerificationKey2020" => Self::GpgVerificationKey2020, - "X25519KeyAgreementKey2019" => Self::X25519KeyAgreementKey2019, - "EcdsaSecp256k1RecoveryMethod2020" => Self::EcdsaSecp256k1RecoveryMethod2020, - "SchnorrSecp256k1VerificationKey2019" => Self::SchnorrSecp256k1VerificationKey2019, - _ => Self::UnknownKey, - } - } -} diff --git a/identity_core/src/utils/mod.rs b/identity_core/src/utils/mod.rs new file mode 100644 index 0000000000..3054bae213 --- /dev/null +++ b/identity_core/src/utils/mod.rs @@ -0,0 +1,7 @@ +mod add_unique; +mod encoding; +mod has_id; + +pub use add_unique::*; +pub use encoding::*; +pub use has_id::*; diff --git a/identity_core/src/utils/service.rs b/identity_core/src/utils/service.rs deleted file mode 100644 index af5b545c3e..0000000000 --- a/identity_core/src/utils/service.rs +++ /dev/null @@ -1,92 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use std::{hash::Hash, str::FromStr}; - -use crate::utils::{Context, HasId, Subject}; -use identity_diff::Diff; - -/// Describes a `Service` in a `DIDDocument` type. Contains an `id`, `service_type` and `endpoint`. The `endpoint` can -/// be represented as a `String` or a `ServiceEndpoint` in json. -#[derive(Debug, Eq, PartialEq, Deserialize, Serialize, Diff, Clone, Default, Hash, PartialOrd, Ord)] -#[diff(from_into)] -pub struct Service { - #[serde(default)] - pub id: Subject, - #[serde(rename = "type")] - pub service_type: String, - #[serde(rename = "serviceEndpoint")] - pub endpoint: ServiceEndpoint, -} - -/// Describes the `ServiceEndpoint` struct type. Contains a required `context` and two optional fields: `endpoint_type` -/// and `instances`. If neither `instances` nor `endpoint_type` is specified, the `ServiceEndpoint` is represented as a -/// String in json using the `context`. -#[derive(Debug, Eq, PartialEq, Clone, Diff, Default, Hash, PartialOrd, Ord)] -#[diff(from_into)] -pub struct ServiceEndpoint { - pub context: Context, - pub endpoint_type: Option, - pub instances: Option>, -} - -impl Service { - pub fn init(self) -> Self { - Self { - id: self.id, - service_type: self.service_type, - endpoint: self.endpoint, - } - } -} - -impl ServiceEndpoint { - pub fn init(self) -> Self { - Self { - context: self.context, - endpoint_type: self.endpoint_type, - instances: self.instances, - } - } -} - -impl HasId for Service { - type Id = Subject; - - fn id(&self) -> &Self::Id { - &self.id - } -} - -impl FromStr for Service { - type Err = crate::Error; - - fn from_str(s: &str) -> crate::Result { - Ok(serde_json::from_str(s)?) - } -} - -impl ToString for Service { - fn to_string(&self) -> String { - serde_json::to_string(self).expect("Unable to serialize the service") - } -} - -impl FromStr for ServiceEndpoint { - type Err = crate::Error; - - fn from_str(s: &str) -> crate::Result { - Ok(serde_json::from_str(s)?) - } -} - -impl ToString for ServiceEndpoint { - fn to_string(&self) -> String { - serde_json::to_string(self).expect("Unable to serialize the Service Endpoint struct") - } -} - -impl From<&str> for ServiceEndpoint { - fn from(s: &str) -> Self { - serde_json::from_str(s).expect("Unable to parse string") - } -} diff --git a/identity_core/src/utils/service_serialize.rs b/identity_core/src/utils/service_serialize.rs deleted file mode 100644 index 7e3db952af..0000000000 --- a/identity_core/src/utils/service_serialize.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::utils::{Context, ServiceEndpoint}; - -use serde::{ - de::{self, Deserialize, Deserializer, MapAccess, Visitor}, - ser::{Serialize, SerializeStruct, Serializer}, -}; - -use std::{ - fmt::{self, Formatter}, - str::FromStr, -}; - -/// The Json fields for the `ServiceEndpoint`. -enum Field { - Context, - Type, - Instances, -} - -/// A visitor for the service endpoint values. -struct ServiceEndpointVisitor; - -/// A visitor for the service endpoint keys. -struct FieldVisitor; - -/// Deserialize logic for the `ServiceEndpoint` type. -impl<'de> Deserialize<'de> for ServiceEndpoint { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(ServiceEndpointVisitor) - } -} - -/// Deserialize logic for the `Field` type. -impl<'de> Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(FieldVisitor) - } -} - -/// Visitor logic for the `ServiceEndpointVisitor` to deserialize the `ServiceEndpoint`. -impl<'de> Visitor<'de> for ServiceEndpointVisitor { - type Value = ServiceEndpoint; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - formatter.write_str("Expecting a string or a Service Endpoint Struct") - } - - /// If given a &str use this logic to create a `ServiceEndpoint`. - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - Ok(ServiceEndpoint { - context: Context::from_str(value).expect("Unable to deserialize the context"), - ..Default::default() - }) - } - - /// given a map, use this logic to create a `ServiceEndpoint`. - fn visit_map(self, mut map: M) -> Result - where - M: MapAccess<'de>, - { - let mut context: Option = None; - let mut endpoint_type: Option = None; - let mut instances: Option> = None; - - while let Some(key) = map.next_key()? { - match key { - Field::Context => { - if context.is_some() { - return Err(de::Error::duplicate_field("@context")); - } - context = Some(map.next_value()?); - } - Field::Type => { - if endpoint_type.is_some() { - return Err(de::Error::duplicate_field("type")); - } - endpoint_type = Some(map.next_value()?); - } - Field::Instances => { - if instances.is_some() { - return Err(de::Error::duplicate_field("instances")); - } - instances = Some(map.next_value()?); - } - } - } - - let context = context.ok_or_else(|| de::Error::missing_field("@context"))?; - - Ok(ServiceEndpoint { - context: Context::from_str(&context).expect("Unable to deserialize the context into a Service endpoint"), - endpoint_type, - instances, - }) - } -} - -/// Visitor logic for the `FieldVisitor` to deserialize the `Field` type. -impl<'de> Visitor<'de> for FieldVisitor { - type Value = Field; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - formatter.write_str("Expected `@context`, `type`, or `instances`") - } - - /// If given a &str use this logic to create a `Field`. - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - match value { - "@context" => Ok(Field::Context), - "type" => Ok(Field::Type), - "instances" => Ok(Field::Instances), - _ => Err(de::Error::unknown_field(value, &[])), - } - } -} - -/// Serialize the `ServiceEndpoint`. -impl Serialize for ServiceEndpoint { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - if self.instances == None && self.endpoint_type == None { - self.context.serialize(serializer) - } else { - let mut se = serializer.serialize_struct("", 3)?; - se.serialize_field("@context", &self.context)?; - se.serialize_field("type", &self.endpoint_type)?; - se.serialize_field("instances", &self.instances)?; - se.end() - } - } -} diff --git a/identity_core/src/utils/subject.rs b/identity_core/src/utils/subject.rs deleted file mode 100644 index 39555b82f9..0000000000 --- a/identity_core/src/utils/subject.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::did::DID; - -use identity_diff::Diff; -use serde::{Deserialize, Serialize}; - -use std::{hash::Hash, str::FromStr}; - -/// A wrapped `DID` type called a subject. -#[derive(Eq, PartialEq, Debug, Default, Clone, Serialize, Deserialize, Diff, Hash, PartialOrd, Ord)] -#[serde(transparent)] -#[diff(from_into)] -pub struct Subject(DID); - -impl Subject { - /// creates a new `Subject` given a `DID` string with proper format. - pub fn new(s: String) -> crate::Result { - let did = DID::parse_from_str(&s)?; - - Ok(Subject(did)) - } - - /// converts a `DID` into a `Subject`. - pub fn from_did(did: DID) -> crate::Result { - Ok(Subject(did)) - } - - /// retrieves the `DID` from the `Subject`. - pub fn to_did(&self) -> crate::Result { - Ok(self.0.clone()) - } -} - -impl FromStr for Subject { - type Err = crate::Error; - - fn from_str(s: &str) -> crate::Result { - Ok(Subject(DID::parse_from_str(s)?)) - } -} - -/// Allows type conversion from the `DID` type to the `Subject` type. -impl From for Subject { - fn from(did: DID) -> Self { - Subject::from_did(did).expect("unable to convert Did to Subject") - } -} - -impl From<&str> for Subject { - fn from(s: &str) -> Self { - Subject::from_str(s).expect("unable to convert Did to Subject") - } -} - -impl From for Subject { - fn from(s: String) -> Self { - Subject::from_str(&s).expect("Unable to convert to Subject") - } -} diff --git a/identity_core/src/vc/consts.rs b/identity_core/src/vc/consts.rs new file mode 100644 index 0000000000..f9d5cd1df1 --- /dev/null +++ b/identity_core/src/vc/consts.rs @@ -0,0 +1 @@ +pub const RESERVED_PROPERTIES: &[&str] = &["issued", "validFrom", "validUntil"]; diff --git a/identity_core/src/vc/credential.rs b/identity_core/src/vc/credential.rs new file mode 100644 index 0000000000..176ffa734c --- /dev/null +++ b/identity_core/src/vc/credential.rs @@ -0,0 +1,120 @@ +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Context, Object, OneOrMany, Timestamp, Url, Value}, + error::{Error, Result}, + vc::{ + validate_credential_structure, CredentialSchema, CredentialStatus, CredentialSubject, Evidence, Issuer, + RefreshService, TermsOfUse, VerifiableCredential, + }, +}; + +/// A `Credential` represents a set of claims describing an entity. +/// +/// `Credential`s can be combined with `Proof`s to create `VerifiableCredential`s. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Builder)] +#[builder(build_fn(name = "build_unchecked"), pattern = "owned")] +pub struct Credential { + /// A set of URIs or `Object`s describing the applicable JSON-LD contexts. + /// + /// NOTE: The first URI MUST be `https://www.w3.org/2018/credentials/v1` + #[serde(rename = "@context")] + #[builder(setter(into))] + pub context: OneOrMany, + /// A unique `URI` referencing the subject of the credential. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub id: Option, + /// One or more URIs defining the type of credential. + /// + /// NOTE: The VC spec defines this as a set of URIs BUT they are commonly + /// passed as non-`URI` strings and expected to be processed with JSON-LD. + /// We're using a `String` here since we don't currently use JSON-LD and + /// don't have any immediate plans to do so. + #[serde(rename = "type")] + #[builder(setter(into, strip_option))] + pub types: OneOrMany, + /// One or more `Object`s representing the `Credential` subject(s). + #[serde(rename = "credentialSubject")] + #[builder(default, setter(into, name = "subject"))] + pub credential_subject: OneOrMany, + /// A reference to the issuer of the `Credential`. + #[builder(setter(into))] + pub issuer: Issuer, + /// The date and time the `Credential` becomes valid. + #[serde(rename = "issuanceDate")] + #[builder(default, setter(into))] + pub issuance_date: Timestamp, + /// The date and time the `Credential` is no longer considered valid. + #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub expiration_date: Option, + /// TODO + #[serde(rename = "credentialStatus", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub credential_status: Option>, + /// TODO + #[serde(rename = "credentialSchema", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub credential_schema: Option>, + /// TODO + #[serde(rename = "refreshService", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub refresh_service: Option>, + /// The terms of use issued by the `Credential` issuer + #[serde(rename = "termsOfUse", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub terms_of_use: Option>, + /// TODO + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub evidence: Option>, + /// Indicates that the `Credential` must only be contained within a + /// `Presentation` with a proof issued from the `Credential` subject. + #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub non_transferable: Option, + /// Miscellaneous properties. + #[serde(flatten)] + #[builder(default, setter(into))] + pub properties: Object, +} + +impl Credential { + pub const BASE_CONTEXT: &'static str = "https://www.w3.org/2018/credentials/v1"; + + pub const BASE_TYPE: &'static str = "VerifiableCredential"; + + pub fn validate(&self) -> Result<()> { + validate_credential_structure(self) + } +} + +// ============================================================================= +// Credential Builder +// ============================================================================= + +impl CredentialBuilder { + pub fn new() -> Self { + let mut this: Self = Default::default(); + + this.context = Some(vec![Credential::BASE_CONTEXT.into()].into()); + this.types = Some(vec![Credential::BASE_TYPE.into()].into()); + this + } + + /// Consumes the `CredentialBuilder`, returning a valid `Credential` + pub fn build(self) -> Result { + let this: Credential = self.build_unchecked().map_err(Error::InvalidCredential)?; + + this.validate()?; + + Ok(this) + } + + /// Consumes the `CredentialBuilder`, returning a valid `VerifiableCredential` + pub fn build_verifiable(self, proof: impl Into>) -> Result { + self.build().map(|this| VerifiableCredential::new(this, proof)) + } +} diff --git a/identity_core/src/vc/mod.rs b/identity_core/src/vc/mod.rs new file mode 100644 index 0000000000..7bad52f4f9 --- /dev/null +++ b/identity_core/src/vc/mod.rs @@ -0,0 +1,15 @@ +mod consts; +mod credential; +mod presentation; +mod types; +mod validation; +mod verifiable_credential; +mod verifiable_presentation; + +pub use consts::*; +pub use credential::*; +pub use presentation::*; +pub use types::*; +pub use validation::*; +pub use verifiable_credential::*; +pub use verifiable_presentation::*; diff --git a/identity_core/src/vc/presentation.rs b/identity_core/src/vc/presentation.rs new file mode 100644 index 0000000000..ae3ce7ab60 --- /dev/null +++ b/identity_core/src/vc/presentation.rs @@ -0,0 +1,96 @@ +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Context, Object, OneOrMany, Url}, + error::{Error, Result}, + vc::{ + validate_presentation_structure, Credential, RefreshService, TermsOfUse, VerifiableCredential, + VerifiablePresentation, + }, +}; + +/// A `Presentation` represents a bundle of one or more `VerifiableCredential`s. +/// +/// `Presentation`s can be combined with `Proof`s to create `VerifiablePresentation`s. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Builder)] +#[builder(build_fn(name = "build_unchecked"), pattern = "owned")] +pub struct Presentation { + /// A set of URIs or `Object`s describing the applicable JSON-LD contexts. + /// + /// NOTE: The first URI MUST be `https://www.w3.org/2018/credentials/v1` + #[serde(rename = "@context")] + #[builder(setter(into))] + pub context: OneOrMany, + /// A unique `URI` referencing the subject of the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub id: Option, + /// One or more URIs defining the type of presentation. + /// + /// NOTE: The VC spec defines this as a set of URIs BUT they are commonly + /// passed as non-`URI` strings and expected to be processed with JSON-LD. + /// We're using a `String` here since we don't currently use JSON-LD and + /// don't have any immediate plans to do so. + #[serde(rename = "type")] + #[builder(setter(into, strip_option))] + pub types: OneOrMany, + /// TODO + #[serde(rename = "verifiableCredential")] + #[builder(default, setter(into, name = "credential"))] + pub verifiable_credential: OneOrMany, + /// The entity that generated the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub holder: Option, + /// TODO + #[serde(rename = "refreshService", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub refresh_service: Option>, + /// The terms of use issued by the presentation holder + #[serde(rename = "termsOfUse", skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub terms_of_use: Option>, + /// Miscellaneous properties. + #[serde(flatten)] + #[builder(default, setter(into))] + pub properties: Object, +} + +impl Presentation { + pub const BASE_CONTEXT: &'static str = Credential::BASE_CONTEXT; + + pub const BASE_TYPE: &'static str = "VerifiablePresentation"; + + pub fn validate(&self) -> Result<()> { + validate_presentation_structure(self) + } +} + +// ============================================================================= +// Presentation Builder +// ============================================================================= + +impl PresentationBuilder { + pub fn new() -> Self { + let mut this: Self = Default::default(); + + this.context = Some(vec![Presentation::BASE_CONTEXT.into()].into()); + this.types = Some(vec![Presentation::BASE_TYPE.into()].into()); + this + } + + /// Consumes the `PresentationBuilder_`, returning a valid `Presentation` + pub fn build(self) -> Result { + let this: Presentation = self.build_unchecked().map_err(Error::InvalidPresentation)?; + + this.validate()?; + + Ok(this) + } + + /// Consumes the `PresentationBuilder_`, returning a valid `VerifiablePresentation` + pub fn build_verifiable(self, proof: impl Into>) -> Result { + self.build().map(|this| VerifiablePresentation::new(this, proof)) + } +} diff --git a/identity_core/src/vc/types/credential_schema.rs b/identity_core/src/vc/types/credential_schema.rs new file mode 100644 index 0000000000..28f11e836c --- /dev/null +++ b/identity_core/src/vc/types/credential_schema.rs @@ -0,0 +1,52 @@ +use core::convert::TryFrom; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Object, Url}, + error::Error, +}; + +/// Information used to validate the structure of a `Credential`. +/// +/// [More Info](https://www.w3.org/TR/vc-data-model/#data-schemas) +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Builder)] +pub struct CredentialSchema { + #[builder(setter(into))] + pub id: Url, + #[serde(rename = "type")] + #[builder(setter(into))] + pub type_: String, + #[serde(flatten)] + #[builder(default, setter(into))] + pub properties: Object, +} + +impl TryFrom for CredentialSchema { + type Error = Error; + + fn try_from(mut other: Object) -> Result { + Ok(Self { + id: other.try_take_object_id()?.into(), + type_: other.try_take_object_type()?, + properties: other, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic = "`id` must be initialized"] + fn test_builder_missing_id() { + CredentialSchemaBuilder::default().type_("my-type").build().unwrap(); + } + + #[test] + #[should_panic = "`type_` must be initialized"] + fn test_builder_missing_type() { + CredentialSchemaBuilder::default().id("did:test").build().unwrap(); + } +} diff --git a/identity_core/src/vc/types/credential_status.rs b/identity_core/src/vc/types/credential_status.rs new file mode 100644 index 0000000000..58686e7b31 --- /dev/null +++ b/identity_core/src/vc/types/credential_status.rs @@ -0,0 +1,55 @@ +use core::convert::TryFrom; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Object, OneOrMany, Url}, + error::Error, +}; + +/// Information used to determine the current status of a `Credential`. +/// +/// [More Info](https://www.w3.org/TR/vc-data-model/#status) +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Builder)] +pub struct CredentialStatus { + #[builder(setter(into))] + pub id: Url, + #[serde(rename = "type")] + #[builder(setter(into))] + pub types: OneOrMany, + #[serde(flatten)] + #[builder(default, setter(into))] + pub properties: Object, +} + +impl TryFrom for CredentialStatus { + type Error = Error; + + fn try_from(mut other: Object) -> Result { + Ok(Self { + id: other.try_take_object_id()?.into(), + types: other.try_take_object_types()?, + properties: other, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic = "`id` must be initialized"] + fn test_builder_missing_id() { + CredentialStatusBuilder::default() + .types("my-type".to_string()) + .build() + .unwrap(); + } + + #[test] + #[should_panic = "`types` must be initialized"] + fn test_builder_missing_types() { + CredentialStatusBuilder::default().id("did:test").build().unwrap(); + } +} diff --git a/identity_vc/src/common/credential/credential_subject.rs b/identity_core/src/vc/types/credential_subject.rs similarity index 50% rename from identity_vc/src/common/credential/credential_subject.rs rename to identity_core/src/vc/types/credential_subject.rs index faa0d85491..3957d7ec6e 100644 --- a/identity_vc/src/common/credential/credential_subject.rs +++ b/identity_core/src/vc/types/credential_subject.rs @@ -1,17 +1,22 @@ -use identity_common::{Object, Uri}; +use core::convert::TryFrom; +use derive_builder::Builder; use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; -use crate::{common::take_object_id, error::Error}; +use crate::{ + common::{Object, Url}, + error::Error, +}; /// An entity who is the target of a set of claims. /// -/// Ref: https://www.w3.org/TR/vc-data-model/#credential-subject -#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +/// [More Info](https://www.w3.org/TR/vc-data-model/#credential-subject) +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Builder)] pub struct CredentialSubject { #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, + #[builder(default, setter(into, strip_option))] + pub id: Option, #[serde(flatten)] + #[builder(default, setter(into))] pub properties: Object, } @@ -19,11 +24,9 @@ impl TryFrom for CredentialSubject { type Error = Error; fn try_from(mut other: Object) -> Result { - let mut this: Self = Default::default(); - - this.id = take_object_id(&mut other).map(Into::into); - this.properties = other; - - Ok(this) + Ok(Self { + id: other.take_object_id().map(Into::into), + properties: other, + }) } } diff --git a/identity_core/src/vc/types/evidence.rs b/identity_core/src/vc/types/evidence.rs new file mode 100644 index 0000000000..cc85be427c --- /dev/null +++ b/identity_core/src/vc/types/evidence.rs @@ -0,0 +1,47 @@ +use core::convert::TryFrom; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Object, OneOrMany}, + error::Error, +}; + +/// Information used to increase confidence in the claims of a `Credential` +/// +/// [More Info](https://www.w3.org/TR/vc-data-model/#evidence) +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Builder)] +pub struct Evidence { + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub id: Option, + #[serde(rename = "type")] + #[builder(setter(into))] + pub types: OneOrMany, + #[serde(flatten)] + #[builder(default, setter(into))] + pub properties: Object, +} + +impl TryFrom for Evidence { + type Error = Error; + + fn try_from(mut other: Object) -> Result { + Ok(Self { + id: other.take_object_id(), + types: other.try_take_object_types()?, + properties: other, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic = "`types` must be initialized"] + fn test_builder_missing_types() { + EvidenceBuilder::default().id("did:test").build().unwrap(); + } +} diff --git a/identity_vc/src/common/issuer.rs b/identity_core/src/vc/types/issuer.rs similarity index 54% rename from identity_vc/src/common/issuer.rs rename to identity_core/src/vc/types/issuer.rs index 4532e16dc8..f7638f2340 100644 --- a/identity_vc/src/common/issuer.rs +++ b/identity_core/src/vc/types/issuer.rs @@ -1,24 +1,25 @@ -use identity_common::{Object, Uri}; use serde::{Deserialize, Serialize}; -/// TODO: -/// - Deserialize single Uri into object-style layout -/// - Replace Enum with plain struct +use crate::common::{Object, Url}; + +/// An identifier representing the issuer of a `Credential`. +/// +/// [More Info](https://www.w3.org/TR/vc-data-model/#issuer) #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[serde(untagged)] pub enum Issuer { - Uri(Uri), + Url(Url), Obj { - id: Uri, + id: Url, #[serde(flatten)] object: Object, }, } impl Issuer { - pub fn uri(&self) -> &Uri { + pub fn url(&self) -> &Url { match self { - Self::Uri(uri) => uri, + Self::Url(url) => url, Self::Obj { id, .. } => id, } } @@ -26,9 +27,9 @@ impl Issuer { impl From for Issuer where - T: Into, + T: Into, { fn from(other: T) -> Self { - Self::Uri(other.into()) + Self::Url(other.into()) } } diff --git a/identity_core/src/vc/types/mod.rs b/identity_core/src/vc/types/mod.rs new file mode 100644 index 0000000000..335cba8967 --- /dev/null +++ b/identity_core/src/vc/types/mod.rs @@ -0,0 +1,15 @@ +mod credential_schema; +mod credential_status; +mod credential_subject; +mod evidence; +mod issuer; +mod refresh_service; +mod terms_of_use; + +pub use credential_schema::*; +pub use credential_status::*; +pub use credential_subject::*; +pub use evidence::*; +pub use issuer::*; +pub use refresh_service::*; +pub use terms_of_use::*; diff --git a/identity_core/src/vc/types/refresh_service.rs b/identity_core/src/vc/types/refresh_service.rs new file mode 100644 index 0000000000..4fa4c13d3f --- /dev/null +++ b/identity_core/src/vc/types/refresh_service.rs @@ -0,0 +1,55 @@ +use core::convert::TryFrom; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Object, OneOrMany, Url}, + error::Error, +}; + +/// Information used to refresh or assert the status of a `Credential`. +/// +/// [More Info](https://www.w3.org/TR/vc-data-model/#refreshing) +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Builder)] +pub struct RefreshService { + #[builder(setter(into))] + pub id: Url, + #[serde(rename = "type")] + #[builder(setter(into))] + pub types: OneOrMany, + #[serde(flatten)] + #[builder(default, setter(into))] + pub properties: Object, +} + +impl TryFrom for RefreshService { + type Error = Error; + + fn try_from(mut other: Object) -> Result { + Ok(Self { + id: other.try_take_object_id()?.into(), + types: other.try_take_object_types()?, + properties: other, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic = "`id` must be initialized"] + fn test_builder_missing_id() { + RefreshServiceBuilder::default() + .types("my-type".to_string()) + .build() + .unwrap(); + } + + #[test] + #[should_panic = "`types` must be initialized"] + fn test_builder_missing_types() { + RefreshServiceBuilder::default().id("did:test").build().unwrap(); + } +} diff --git a/identity_core/src/vc/types/terms_of_use.rs b/identity_core/src/vc/types/terms_of_use.rs new file mode 100644 index 0000000000..56aec6f214 --- /dev/null +++ b/identity_core/src/vc/types/terms_of_use.rs @@ -0,0 +1,48 @@ +use core::convert::TryFrom; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Object, OneOrMany, Url}, + error::Error, +}; + +/// Information used to express obligations, prohibitions, and permissions about +/// a `Credential` or `Presentation`. +/// +/// [More Info](https://www.w3.org/TR/vc-data-model/#terms-of-use) +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Builder)] +pub struct TermsOfUse { + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] + pub id: Option, + #[serde(rename = "type")] + #[builder(setter(into))] + pub types: OneOrMany, + #[serde(flatten)] + #[builder(default, setter(into))] + pub properties: Object, +} + +impl TryFrom for TermsOfUse { + type Error = Error; + + fn try_from(mut other: Object) -> Result { + Ok(Self { + id: other.take_object_id().map(Into::into), + types: other.try_take_object_types()?, + properties: other, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic = "`types` must be initialized"] + fn test_builder_missing_types() { + TermsOfUseBuilder::default().id("did:test").build().unwrap(); + } +} diff --git a/identity_core/src/vc/validation.rs b/identity_core/src/vc/validation.rs new file mode 100644 index 0000000000..5019c92d9d --- /dev/null +++ b/identity_core/src/vc/validation.rs @@ -0,0 +1,54 @@ +use crate::{ + common::Context, + error::{Error, Result}, + vc::{Credential, Presentation}, +}; + +pub fn validate_credential_structure(credential: &Credential) -> Result<()> { + // Ensure the base context is present and in the correct location + match credential.context.get(0) { + Some(Context::Url(url)) if url == Credential::BASE_CONTEXT => {} + Some(_) => return Err(Error::InvalidCredential("Invalid Base Context".into())), + None => return Err(Error::InvalidCredential("Missing Base Context".into())), + } + + // The set of types MUST contain the base type + if !credential.types.contains(&Credential::BASE_TYPE.into()) { + return Err(Error::InvalidCredential("Missing Base Type".into())); + } + + // Credentials MUST have at least one subject + if credential.credential_subject.is_empty() { + return Err(Error::InvalidCredential("Missing Subject".into())); + } + + // Each subject is defined as one or more properties - no empty objects + for subject in credential.credential_subject.iter() { + if subject.id.is_none() && subject.properties.is_empty() { + return Err(Error::InvalidCredential("Invalid Subject".into())); + } + } + + Ok(()) +} + +pub fn validate_presentation_structure(presentation: &Presentation) -> Result<()> { + // Ensure the base context is present and in the correct location + match presentation.context.get(0) { + Some(Context::Url(url)) if url == Presentation::BASE_CONTEXT => {} + Some(_) => return Err(Error::InvalidPresentation("Invalid Base Context".into())), + None => return Err(Error::InvalidPresentation("Missing Base Context".into())), + } + + // The set of types MUST contain the base type + if !presentation.types.contains(&Presentation::BASE_TYPE.into()) { + return Err(Error::InvalidPresentation("Missing Base Type".into())); + } + + // Validate all verifiable credentials + for credential in presentation.verifiable_credential.iter() { + credential.validate()?; + } + + Ok(()) +} diff --git a/identity_vc/src/verifiable/credential.rs b/identity_core/src/vc/verifiable_credential.rs similarity index 90% rename from identity_vc/src/verifiable/credential.rs rename to identity_core/src/vc/verifiable_credential.rs index 28f328774f..2d92f673c3 100644 --- a/identity_vc/src/verifiable/credential.rs +++ b/identity_core/src/vc/verifiable_credential.rs @@ -1,8 +1,10 @@ -use identity_common::{Object, OneOrMany}; +use core::ops::Deref; use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use crate::credential::Credential; +use crate::{ + common::{Object, OneOrMany}, + vc::Credential, +}; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct VerifiableCredential { diff --git a/identity_vc/src/verifiable/presentation.rs b/identity_core/src/vc/verifiable_presentation.rs similarity index 90% rename from identity_vc/src/verifiable/presentation.rs rename to identity_core/src/vc/verifiable_presentation.rs index ce4b3a74c8..36a6571233 100644 --- a/identity_vc/src/verifiable/presentation.rs +++ b/identity_core/src/vc/verifiable_presentation.rs @@ -1,8 +1,10 @@ -use identity_common::{Object, OneOrMany}; +use core::ops::Deref; use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use crate::presentation::Presentation; +use crate::{ + common::{Object, OneOrMany}, + vc::Presentation, +}; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct VerifiablePresentation { diff --git a/identity_core/tests/authentication.rs b/identity_core/tests/authentication.rs index e6a8de619f..a8ff63e14c 100644 --- a/identity_core/tests/authentication.rs +++ b/identity_core/tests/authentication.rs @@ -1,13 +1,11 @@ use identity_core::{ - document::DIDDocument, - utils::{Authentication, Context, KeyData, PublicKey, Subject}, + did::{Authentication, DIDDocument, DIDDocumentBuilder, DID}, + key::{KeyData, KeyType, PublicKeyBuilder}, }; - -use std::str::FromStr; - use json::JsonValue; +use std::str::FromStr; -const JSON_STR: &str = include_str!("auth.json"); +const JSON_STR: &str = include_str!("fixtures/did/auth.json"); fn setup_json(index: usize) -> String { let json_str: JsonValue = json::parse(JSON_STR).unwrap(); @@ -21,58 +19,51 @@ fn test_auth() { let doc_1 = DIDDocument::from_str(&json_str).unwrap(); - let mut doc_2 = DIDDocument { - context: Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - }; - - let key_data_1 = KeyData::Pem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); - let key_data_2 = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - let auth_key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let key1 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: key_data_1, - ..Default::default() - } - .init(); - - let key2 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:pqrstuvwxyz0987654321".into(), - key_data: key_data_2, - ..Default::default() - } - .init(); - - let auth_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: auth_key_data, - ..Default::default() - }; + let mut doc_2 = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into(), DID::SECURITY_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + + let key_data_1 = KeyData::PublicKeyPem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); + let key_data_2 = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + let auth_key_data = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + + let key1 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(key_data_1) + .build() + .unwrap(); + + let key2 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:pqrstuvwxyz0987654321".parse().unwrap()) + .key_data(key_data_2) + .build() + .unwrap(); + + let auth_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(auth_key_data) + .build() + .unwrap(); doc_2.update_public_key(key1); doc_2.update_public_key(key2); - let auth1 = Authentication::Method("did:iota:123456789abcdefghi#keys-1".into()); - let auth2 = Authentication::Method("did:iota:123456789abcdefghi#biometric-1".into()); + let auth1 = Authentication::DID("did:iota:123456789abcdefghi#keys-1".parse().unwrap()); + let auth2 = Authentication::DID("did:iota:123456789abcdefghi#biometric-1".parse().unwrap()); let auth3 = Authentication::Key(auth_key); doc_2.update_auth(auth1); doc_2.update_auth(auth2); doc_2.update_auth(auth3); - let doc_2 = doc_2.init(); - assert_eq!(doc_1, doc_2); } @@ -82,58 +73,51 @@ fn test_assertion() { let doc_1 = DIDDocument::from_str(&json_str).unwrap(); - let mut doc_2 = DIDDocument { - context: Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - }; - - let key_data_1 = KeyData::Pem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); - let key_data_2 = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - let auth_key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let key1 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: key_data_1, - ..Default::default() - } - .init(); - - let key2 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:pqrstuvwxyz0987654321".into(), - key_data: key_data_2, - ..Default::default() - } - .init(); - - let auth_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: auth_key_data, - ..Default::default() - }; + let mut doc_2 = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into(), DID::SECURITY_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + + let key_data_1 = KeyData::PublicKeyPem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); + let key_data_2 = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + let auth_key_data = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + + let key1 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(key_data_1) + .build() + .unwrap(); + + let key2 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:pqrstuvwxyz0987654321".parse().unwrap()) + .key_data(key_data_2) + .build() + .unwrap(); + + let auth_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(auth_key_data) + .build() + .unwrap(); doc_2.update_public_key(key1); doc_2.update_public_key(key2); - let auth1 = Authentication::Method("did:iota:123456789abcdefghi#keys-1".into()); - let auth2 = Authentication::Method("did:iota:123456789abcdefghi#biometric-1".into()); + let auth1 = Authentication::DID("did:iota:123456789abcdefghi#keys-1".parse().unwrap()); + let auth2 = Authentication::DID("did:iota:123456789abcdefghi#biometric-1".parse().unwrap()); let auth3 = Authentication::Key(auth_key); doc_2.update_assert(auth1); doc_2.update_assert(auth2); doc_2.update_assert(auth3); - let doc_2 = doc_2.init(); - assert_eq!(doc_1, doc_2); } @@ -143,58 +127,51 @@ fn test_verification() { let doc_1 = DIDDocument::from_str(&json_str).unwrap(); - let mut doc_2 = DIDDocument { - context: Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - }; - - let key_data_1 = KeyData::Pem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); - let key_data_2 = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - let auth_key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let key1 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: key_data_1, - ..Default::default() - } - .init(); - - let key2 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:pqrstuvwxyz0987654321".into(), - key_data: key_data_2, - ..Default::default() - } - .init(); - - let auth_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: auth_key_data, - ..Default::default() - }; + let mut doc_2 = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into(), DID::SECURITY_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + + let key_data_1 = KeyData::PublicKeyPem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); + let key_data_2 = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + let auth_key_data = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + + let key1 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(key_data_1) + .build() + .unwrap(); + + let key2 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:pqrstuvwxyz0987654321".parse().unwrap()) + .key_data(key_data_2) + .build() + .unwrap(); + + let auth_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(auth_key_data) + .build() + .unwrap(); doc_2.update_public_key(key1); doc_2.update_public_key(key2); - let auth1 = Authentication::Method("did:iota:123456789abcdefghi#keys-1".into()); - let auth2 = Authentication::Method("did:iota:123456789abcdefghi#biometric-1".into()); + let auth1 = Authentication::DID("did:iota:123456789abcdefghi#keys-1".parse().unwrap()); + let auth2 = Authentication::DID("did:iota:123456789abcdefghi#biometric-1".parse().unwrap()); let auth3 = Authentication::Key(auth_key); doc_2.update_verification(auth1); doc_2.update_verification(auth2); doc_2.update_verification(auth3); - let doc_2 = doc_2.init(); - assert_eq!(doc_1, doc_2); } @@ -204,58 +181,51 @@ fn test_delegation() { let doc_1 = DIDDocument::from_str(&json_str).unwrap(); - let mut doc_2 = DIDDocument { - context: Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - }; - - let key_data_1 = KeyData::Pem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); - let key_data_2 = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - let auth_key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let key1 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: key_data_1, - ..Default::default() - } - .init(); - - let key2 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:pqrstuvwxyz0987654321".into(), - key_data: key_data_2, - ..Default::default() - } - .init(); - - let auth_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: auth_key_data, - ..Default::default() - }; + let mut doc_2 = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into(), DID::SECURITY_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + + let key_data_1 = KeyData::PublicKeyPem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); + let key_data_2 = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + let auth_key_data = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + + let key1 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(key_data_1) + .build() + .unwrap(); + + let key2 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:pqrstuvwxyz0987654321".parse().unwrap()) + .key_data(key_data_2) + .build() + .unwrap(); + + let auth_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(auth_key_data) + .build() + .unwrap(); doc_2.update_public_key(key1); doc_2.update_public_key(key2); - let auth1 = Authentication::Method("did:iota:123456789abcdefghi#keys-1".into()); - let auth2 = Authentication::Method("did:iota:123456789abcdefghi#biometric-1".into()); + let auth1 = Authentication::DID("did:iota:123456789abcdefghi#keys-1".parse().unwrap()); + let auth2 = Authentication::DID("did:iota:123456789abcdefghi#biometric-1".parse().unwrap()); let auth3 = Authentication::Key(auth_key); doc_2.update_delegation(auth1); doc_2.update_delegation(auth2); doc_2.update_delegation(auth3); - let doc_2 = doc_2.init(); - assert_eq!(doc_1, doc_2); } @@ -265,58 +235,51 @@ fn test_invocation() { let doc_1 = DIDDocument::from_str(&json_str).unwrap(); - let mut doc_2 = DIDDocument { - context: Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - }; - - let key_data_1 = KeyData::Pem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); - let key_data_2 = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - let auth_key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let key1 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: key_data_1, - ..Default::default() - } - .init(); - - let key2 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:pqrstuvwxyz0987654321".into(), - key_data: key_data_2, - ..Default::default() - } - .init(); - - let auth_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: auth_key_data, - ..Default::default() - }; + let mut doc_2 = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into(), DID::SECURITY_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + + let key_data_1 = KeyData::PublicKeyPem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); + let key_data_2 = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + let auth_key_data = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + + let key1 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(key_data_1) + .build() + .unwrap(); + + let key2 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:pqrstuvwxyz0987654321".parse().unwrap()) + .key_data(key_data_2) + .build() + .unwrap(); + + let auth_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(auth_key_data) + .build() + .unwrap(); doc_2.update_public_key(key1); doc_2.update_public_key(key2); - let auth1 = Authentication::Method("did:iota:123456789abcdefghi#keys-1".into()); - let auth2 = Authentication::Method("did:iota:123456789abcdefghi#biometric-1".into()); + let auth1 = Authentication::DID("did:iota:123456789abcdefghi#keys-1".parse().unwrap()); + let auth2 = Authentication::DID("did:iota:123456789abcdefghi#biometric-1".parse().unwrap()); let auth3 = Authentication::Key(auth_key); doc_2.update_invocation(auth1); doc_2.update_invocation(auth2); doc_2.update_invocation(auth3); - let doc_2 = doc_2.init(); - assert_eq!(doc_1, doc_2); } @@ -326,57 +289,50 @@ fn test_agreement() { let doc_1 = DIDDocument::from_str(&json_str).unwrap(); - let mut doc_2 = DIDDocument { - context: Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - }; - - let key_data_1 = KeyData::Pem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); - let key_data_2 = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - let auth_key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let key1 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: key_data_1, - ..Default::default() - } - .init(); - - let key2 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:pqrstuvwxyz0987654321".into(), - key_data: key_data_2, - ..Default::default() - } - .init(); - - let auth_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-2".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: auth_key_data, - ..Default::default() - }; + let mut doc_2 = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into(), DID::SECURITY_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + + let key_data_1 = KeyData::PublicKeyPem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); + let key_data_2 = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + let auth_key_data = KeyData::PublicKeyBase58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + + let key1 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(key_data_1) + .build() + .unwrap(); + + let key2 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:pqrstuvwxyz0987654321".parse().unwrap()) + .key_data(key_data_2) + .build() + .unwrap(); + + let auth_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(auth_key_data) + .build() + .unwrap(); doc_2.update_public_key(key1); doc_2.update_public_key(key2); - let auth1 = Authentication::Method("did:iota:123456789abcdefghi#keys-1".into()); - let auth2 = Authentication::Method("did:iota:123456789abcdefghi#biometric-1".into()); + let auth1 = Authentication::DID("did:iota:123456789abcdefghi#keys-1".parse().unwrap()); + let auth2 = Authentication::DID("did:iota:123456789abcdefghi#biometric-1".parse().unwrap()); let auth3 = Authentication::Key(auth_key); doc_2.update_agreement(auth1); doc_2.update_agreement(auth2); doc_2.update_agreement(auth3); - let doc_2 = doc_2.init(); - assert_eq!(doc_1, doc_2); } diff --git a/identity_common/tests/common.rs b/identity_core/tests/common.rs similarity index 95% rename from identity_common/tests/common.rs rename to identity_core/tests/common.rs index deb352b27e..0a86da5c27 100644 --- a/identity_common/tests/common.rs +++ b/identity_core/tests/common.rs @@ -1,4 +1,4 @@ -use identity_common::{line_error, object, Timestamp}; +use identity_core::{common::Timestamp, line_error, object}; use std::convert::TryFrom; diff --git a/identity_core/tests/credential.rs b/identity_core/tests/credential.rs new file mode 100644 index 0000000000..0756f3b581 --- /dev/null +++ b/identity_core/tests/credential.rs @@ -0,0 +1,91 @@ +use identity_core::{ + common::{Context, Timestamp}, + object, + vc::*, +}; + +#[test] +fn test_credential_builder_valid() { + let issuance: Timestamp = "2010-01-01T00:00:00Z".parse().unwrap(); + + let subjects = vec![ + CredentialSubjectBuilder::default() + .id("did:iota:alice") + .properties(object!(spouse: "did:iota:bob")) + .build() + .unwrap(), + CredentialSubjectBuilder::default() + .id("did:iota:bob") + .properties(object!(spouse: "did:iota:alice")) + .build() + .unwrap(), + ]; + + let credential = CredentialBuilder::new() + .issuer("did:example:issuer") + .context(vec![ + Context::from(Credential::BASE_CONTEXT), + Context::from("https://www.w3.org/2018/credentials/examples/v1"), + Context::from(object!(id: "did:context:1234", type: "CustomContext2020")), + ]) + .id("did:example:123") + .types(vec![Credential::BASE_TYPE.into(), "RelationshipCredential".into()]) + .subject(subjects) + .issuance_date(issuance) + .build() + .unwrap(); + + assert_eq!(credential.context.len(), 3); + assert!(matches!(credential.context.get(0).unwrap(), Context::Url(ref url) if url == Credential::BASE_CONTEXT)); + assert!( + matches!(credential.context.get(1).unwrap(), Context::Url(ref url) if url == "https://www.w3.org/2018/credentials/examples/v1") + ); + + assert_eq!(credential.id, Some("did:example:123".into())); + + assert_eq!(credential.types.len(), 2); + assert_eq!(credential.types.get(0).unwrap(), Credential::BASE_TYPE); + assert_eq!(credential.types.get(1).unwrap(), "RelationshipCredential"); + + assert_eq!(credential.credential_subject.len(), 2); + assert_eq!( + credential.credential_subject.get(0).unwrap().id, + Some("did:iota:alice".into()) + ); + assert_eq!( + credential.credential_subject.get(1).unwrap().id, + Some("did:iota:bob".into()) + ); + + assert_eq!(credential.issuer.url(), "did:example:issuer"); + + assert_eq!(credential.issuance_date, issuance); +} + +#[test] +#[should_panic = "Missing Subject"] +fn test_builder_missing_subjects() { + CredentialBuilder::new() + .issuer("did:issuer") + .build() + .unwrap_or_else(|error| panic!("{}", error)); +} + +#[test] +#[should_panic = "`issuer` must be initialized"] +fn test_builder_missing_issuer() { + CredentialBuilder::new() + .subject(CredentialSubjectBuilder::default().id("did:sub").build().unwrap()) + .build() + .unwrap_or_else(|error| panic!("{}", error)); +} + +#[test] +#[should_panic = "InvalidUrl"] +fn test_builder_invalid_issuer() { + CredentialBuilder::new() + .subject(CredentialSubjectBuilder::default().id("did:sub").build().unwrap()) + .issuer("foo") + .build() + .unwrap_or_else(|error| panic!("{}", error)); +} diff --git a/identity_core/tests/document.rs b/identity_core/tests/document.rs index 864dbbe7a3..73dbaeb2e5 100644 --- a/identity_core/tests/document.rs +++ b/identity_core/tests/document.rs @@ -1,14 +1,13 @@ use identity_core::{ - did::DID, - document::DIDDocument, - utils::{Authentication, Context, IdCompare, KeyData, PublicKey, Service, ServiceEndpoint, Subject}, + did::{Authentication, DIDDocument, DIDDocumentBuilder, ServiceBuilder, ServiceEndpoint, DID}, + key::{KeyData, KeyType, PublicKeyBuilder}, }; use std::str::FromStr; use identity_diff::Diff; -const JSON_STR: &str = include_str!("document.json"); +const JSON_STR: &str = include_str!("fixtures/did/document.json"); fn setup_json(key: &str) -> String { let json_str = json::parse(JSON_STR).unwrap(); @@ -26,10 +25,7 @@ fn test_parse_document() { assert!(doc.is_ok()); let doc = doc.unwrap(); - let ctx = Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]); + let ctx = vec![DID::BASE_CONTEXT.into(), DID::SECURITY_CONTEXT.into()].into(); let did = DID { method_name: "iota".into(), @@ -40,73 +36,63 @@ fn test_parse_document() { .unwrap(); assert_eq!(doc.context, ctx); - assert_eq!(doc.id, did.into()); + assert_eq!(doc.id, did); } /// test doc creation via the `DIDDocument::new` method. #[test] fn test_doc_creation() { - let mut did_doc = DIDDocument { - context: Context::from("https://w3id.org/did/v1"), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - } - .init(); - let endpoint = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); - - let service = Service { - id: "did:into:123#edv".into(), - service_type: "EncryptedDataVault".into(), - endpoint, - } - .init(); - - let endpoint2 = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); - - let service2 = Service { - id: "did:into:123#edv".into(), - service_type: "IdentityHub".into(), - endpoint: endpoint2, - } - .init(); + let mut did_doc = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + + let endpoint = ServiceEndpoint::Url("https://edv.example.com/".parse().unwrap()); + + let service = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("EncryptedDataVault") + .endpoint(endpoint) + .build() + .unwrap(); + + let endpoint2 = ServiceEndpoint::Url("https://edv.example.com/".parse().unwrap()); + + let service2 = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("IdentityHub") + .endpoint(endpoint2) + .build() + .unwrap(); did_doc.update_service(service.clone()); did_doc.update_service(service2.clone()); - let key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let public_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data, - ..Default::default() - } - .init(); + let public_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(KeyData::PublicKeyBase58( + "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into(), + )) + .build() + .unwrap(); did_doc.update_public_key(public_key.clone()); - let mut did_doc_2 = DIDDocument { - context: Context::from("https://w3id.org/did/v1"), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - } - .init(); + let mut did_doc_2 = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); did_doc_2.update_public_key(public_key); did_doc_2.update_service(service); did_doc_2.update_service(service2); - let did_doc = did_doc.init_timestamps().unwrap(); + did_doc.init_timestamps(); // did_doc has timestamps while did_doc_2 does not. assert_ne!(did_doc, did_doc_2); @@ -116,45 +102,39 @@ fn test_doc_creation() { #[test] fn test_doc_diff() { // old doc - let old = DIDDocument { - context: Context::from("https://w3id.org/did/v1"), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - } - .init(); + let old = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + // new doc. - let mut new = DIDDocument { - context: Context::from("https://w3id.org/did/v1"), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - } - .init(); + let mut new = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); - let endpoint = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); + let endpoint = ServiceEndpoint::Url("https://edv.example.com/".parse().unwrap()); - let service = Service { - id: "did:into:123#edv".into(), - service_type: "EncryptedDataVault".into(), - endpoint, - } - .init(); + let service = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("EncryptedDataVault") + .endpoint(endpoint) + .build() + .unwrap(); new.update_service(service); - let key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let public_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data, - ..Default::default() - } - .init(); + let public_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(KeyData::PublicKeyBase58( + "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into(), + )) + .build() + .unwrap(); new.update_public_key(public_key); @@ -196,42 +176,36 @@ fn test_diff_merge_from_string() { let diff_str = setup_json("diff"); // create a doc. - let mut doc = DIDDocument { - context: Context::from("https://w3id.org/did/v1"), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - } - .init(); + let mut doc = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); + // create an endpoint. - let endpoint = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); + let endpoint = ServiceEndpoint::Url("https://edv.example.com/".parse().unwrap()); // create a IdCompare - let service = Service { - id: "did:into:123#edv".into(), - service_type: "EncryptedDataVault".into(), - endpoint, - } - .init(); + let service = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("EncryptedDataVault") + .endpoint(endpoint) + .build() + .unwrap(); // update the service. doc.update_service(service); - // create some key data. - let key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - // create a public key, IdCompare. - let public_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data, - ..Default::default() - } - .init(); + let public_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(KeyData::PublicKeyBase58( + "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into(), + )) + .build() + .unwrap(); // add the public key to the did doc. doc.update_public_key(public_key); @@ -251,8 +225,6 @@ fn test_diff_merge_from_string() { #[test] fn test_doc_metadata() { - use std::collections::HashMap; - // get the json string for a did doc. let json_str = setup_json("doc"); // get the json string for the metadata. @@ -263,14 +235,10 @@ fn test_doc_metadata() { assert!(doc.is_ok()); - let doc = doc.unwrap(); - // create a new hashmap and insert the metadata. - let mut metadata = HashMap::new(); - metadata.insert("some".into(), "metadata".into()); - metadata.insert("some_more".into(), "metadata_stuff".into()); - - // add the metadata to the original doc. - let doc = doc.supply_metadata(metadata).unwrap(); + let mut doc = doc.unwrap(); + // insert the metadata. + doc.set_metadata("some", "metadata"); + doc.set_metadata("some_more", "metadata_stuff"); // get the metadata doc string and create a new did doc from it. let res_doc = DIDDocument::from_str(&result_str).unwrap(); @@ -283,63 +251,54 @@ fn test_doc_metadata() { fn test_realistic_diff() { let json_str = setup_json("diff2"); - let mut did_doc = DIDDocument { - context: Context::from("https://w3id.org/did/v1"), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - } - .init(); - let endpoint = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); + let mut did_doc = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); - let service = Service { - id: "did:into:123#edv".into(), - service_type: "EncryptedDataVault".into(), - endpoint, - } - .init(); + let endpoint = ServiceEndpoint::Url("https://edv.example.com/".parse().unwrap()); - let endpoint2 = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); + let service = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("EncryptedDataVault") + .endpoint(endpoint) + .build() + .unwrap(); - let service2 = Service { - id: "did:into:123#edv".into(), - service_type: "IdentityHub".into(), - endpoint: endpoint2, - } - .init(); + let endpoint2 = ServiceEndpoint::Url("https://edv.example.com/".parse().unwrap()); + + let service2 = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("IdentityHub") + .endpoint(endpoint2) + .build() + .unwrap(); did_doc.update_service(service); did_doc.update_service(service2); - let key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - - let public_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data, - ..Default::default() - } - .init(); + let public_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(KeyData::PublicKeyBase58( + "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into(), + )) + .build() + .unwrap(); did_doc.update_public_key(public_key.clone()); - let key_data_1 = KeyData::Pem("-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into()); - let key1 = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data: key_data_1, - ..Default::default() - } - .init(); + let key1 = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-2".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(KeyData::PublicKeyPem( + "-----BEGIN PUBLIC KEY...END PUBLIC KEY-----".into(), + )) + .build() + .unwrap(); let mut did_doc_2 = did_doc.clone(); @@ -362,44 +321,37 @@ fn test_realistic_diff() { // test that items in the did doc are unique by their subject/id. #[test] fn test_id_compare() { - // key data. - let key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); - // service endpoint. - let endpoint = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); + let endpoint = ServiceEndpoint::Url("https://edv.example.com/".parse().unwrap()); // create a public key. - let public_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "RsaVerificationKey2018".into(), - controller: "did:iota:123456789abcdefghi".into(), - key_data, - ..Default::default() - } - .init(); + let public_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::RsaVerificationKey2018) + .controller("did:iota:123456789abcdefghi".parse().unwrap()) + .key_data(KeyData::PublicKeyBase58( + "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into(), + )) + .build() + .unwrap(); // create the authentication key. let auth = Authentication::Key(public_key.clone()); // create a IdCompare - let service = Service { - id: "did:into:123#edv".into(), - service_type: "EncryptedDataVault".into(), - endpoint, - } - .init(); + let service = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("EncryptedDataVault") + .endpoint(endpoint) + .build() + .unwrap(); // generate a did doc. - let mut did_doc = DIDDocument { - context: Context::from("https://w3id.org/did/v1"), - id: Subject::from("did:iota:123456789abcdefghi"), - ..Default::default() - } - .init(); + let mut did_doc = DIDDocumentBuilder::default() + .context(vec![DID::BASE_CONTEXT.into()]) + .id("did:iota:123456789abcdefghi".parse().unwrap()) + .build() + .unwrap(); // insert the service twice. did_doc.update_service(service.clone()); @@ -417,7 +369,7 @@ fn test_id_compare() { let expected_length = 1; // failed structure of the agreement field. - let failed_auth = vec![IdCompare::new(auth.clone()), IdCompare::new(auth)]; + let failed_auth = vec![auth.clone(), auth]; assert_eq!(expected_length, did_doc.services.len()); assert_eq!(expected_length, did_doc.public_keys.len()); diff --git a/identity_core/tests/auth.json b/identity_core/tests/fixtures/did/auth.json similarity index 96% rename from identity_core/tests/auth.json rename to identity_core/tests/fixtures/did/auth.json index 04c3aca301..f9f64005ef 100644 --- a/identity_core/tests/auth.json +++ b/identity_core/tests/fixtures/did/auth.json @@ -1,7 +1,7 @@ [ { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", @@ -32,7 +32,7 @@ }, { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", @@ -63,7 +63,7 @@ }, { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", @@ -94,7 +94,7 @@ }, { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", @@ -125,7 +125,7 @@ }, { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", @@ -156,7 +156,7 @@ }, { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", @@ -185,4 +185,4 @@ } ] } -] \ No newline at end of file +] diff --git a/identity_core/tests/document.json b/identity_core/tests/fixtures/did/document.json similarity index 88% rename from identity_core/tests/document.json rename to identity_core/tests/fixtures/did/document.json index 2c9a102970..35413c7511 100644 --- a/identity_core/tests/document.json +++ b/identity_core/tests/fixtures/did/document.json @@ -1,11 +1,11 @@ { "doc": { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", - "updated": "2020-08-31T05:46:33.559590+00:00Z", + "updated": "2020-08-31T05:46:33.55Z", "publicKey": [ { "id": "did:iota:123456789abcdefghi#keys-1", @@ -29,11 +29,11 @@ }, "metadata": { "@context": [ - "https://w3id.org/did/v1", + "https://www.w3.org/ns/did/v1", "https://w3id.org/security/v1" ], "id": "did:iota:123456789abcdefghi", - "updated": "2020-08-31T05:46:33.559590+00:00Z", + "updated": "2020-08-31T05:46:33.55Z", "publicKey": [ { "id": "did:iota:123456789abcdefghi#keys-1", @@ -58,12 +58,9 @@ "some": "metadata" }, "diff": { - "context": [ - { - "index": 0, - "item": "https://w3id.org/did/v1" - } - ], + "context": { + "Url": "https://www.w3.org/ns/did/v1" + }, "id": "did:iota:123456789abcdefghi", "public_keys": [ { @@ -84,11 +81,11 @@ "diff2": { "public_keys": [ { - "id": "did:iota:123456789abcdefghi#keys-1", - "type": "RsaVerificationKey2018", + "id": "did:iota:123456789abcdefghi#keys-2", "controller": "did:iota:123456789abcdefghi", + "type": "RsaVerificationKey2018", "publicKeyPem": "-----BEGIN PUBLIC KEY...END PUBLIC KEY-----" } ] } -} \ No newline at end of file +} diff --git a/identity_core/tests/utils.json b/identity_core/tests/fixtures/did/utils.json similarity index 99% rename from identity_core/tests/utils.json rename to identity_core/tests/fixtures/did/utils.json index 8236d7592c..22f7df3c04 100644 --- a/identity_core/tests/utils.json +++ b/identity_core/tests/fixtures/did/utils.json @@ -23,4 +23,4 @@ "type": "EncryptedDataVault", "serviceEndpoint": "https://edv.example.com/" } -} \ No newline at end of file +} diff --git a/identity_vc/tests/input/example-01.json b/identity_core/tests/fixtures/vc/example-01.json similarity index 100% rename from identity_vc/tests/input/example-01.json rename to identity_core/tests/fixtures/vc/example-01.json diff --git a/identity_vc/tests/input/example-02.json b/identity_core/tests/fixtures/vc/example-02.json similarity index 100% rename from identity_vc/tests/input/example-02.json rename to identity_core/tests/fixtures/vc/example-02.json diff --git a/identity_vc/tests/input/example-03.json b/identity_core/tests/fixtures/vc/example-03.json similarity index 100% rename from identity_vc/tests/input/example-03.json rename to identity_core/tests/fixtures/vc/example-03.json diff --git a/identity_vc/tests/input/example-04.json b/identity_core/tests/fixtures/vc/example-04.json similarity index 100% rename from identity_vc/tests/input/example-04.json rename to identity_core/tests/fixtures/vc/example-04.json diff --git a/identity_vc/tests/input/example-05.json b/identity_core/tests/fixtures/vc/example-05.json similarity index 100% rename from identity_vc/tests/input/example-05.json rename to identity_core/tests/fixtures/vc/example-05.json diff --git a/identity_vc/tests/input/example-06.json b/identity_core/tests/fixtures/vc/example-06.json similarity index 100% rename from identity_vc/tests/input/example-06.json rename to identity_core/tests/fixtures/vc/example-06.json diff --git a/identity_vc/tests/input/example-07.json b/identity_core/tests/fixtures/vc/example-07.json similarity index 100% rename from identity_vc/tests/input/example-07.json rename to identity_core/tests/fixtures/vc/example-07.json diff --git a/identity_vc/tests/input/example-08.json b/identity_core/tests/fixtures/vc/example-08.json similarity index 100% rename from identity_vc/tests/input/example-08.json rename to identity_core/tests/fixtures/vc/example-08.json diff --git a/identity_vc/tests/input/example-09.json b/identity_core/tests/fixtures/vc/example-09.json similarity index 100% rename from identity_vc/tests/input/example-09.json rename to identity_core/tests/fixtures/vc/example-09.json diff --git a/identity_vc/tests/input/example-10.json b/identity_core/tests/fixtures/vc/example-10.json similarity index 100% rename from identity_vc/tests/input/example-10.json rename to identity_core/tests/fixtures/vc/example-10.json diff --git a/identity_vc/tests/input/example-11.json b/identity_core/tests/fixtures/vc/example-11.json similarity index 100% rename from identity_vc/tests/input/example-11.json rename to identity_core/tests/fixtures/vc/example-11.json diff --git a/identity_vc/tests/input/example-12.json b/identity_core/tests/fixtures/vc/example-12.json similarity index 100% rename from identity_vc/tests/input/example-12.json rename to identity_core/tests/fixtures/vc/example-12.json diff --git a/identity_vc/tests/input/example-13.json b/identity_core/tests/fixtures/vc/example-13.json similarity index 100% rename from identity_vc/tests/input/example-13.json rename to identity_core/tests/fixtures/vc/example-13.json diff --git a/identity_core/tests/presentation.rs b/identity_core/tests/presentation.rs new file mode 100644 index 0000000000..943c9c501e --- /dev/null +++ b/identity_core/tests/presentation.rs @@ -0,0 +1,95 @@ +use identity_core::{ + common::{Context, Object, Timestamp}, + vc::*, +}; + +#[test] +fn test_presentation_builder_valid() { + let issuance: Timestamp = "2010-01-01T00:00:00Z".parse().unwrap(); + + let subject = CredentialSubjectBuilder::default() + .id("did:iota:alice") + .build() + .unwrap(); + + let credential = CredentialBuilder::new() + .issuer("did:example:issuer") + .context(vec![ + Context::from(Credential::BASE_CONTEXT), + Context::from("https://www.w3.org/2018/credentials/examples/v1"), + ]) + .types(vec![Credential::BASE_TYPE.into(), "PrescriptionCredential".into()]) + .subject(subject) + .issuance_date(issuance) + .build() + .unwrap(); + + let verifiable = VerifiableCredential::new(credential, Object::new()); + + let refresh_service = RefreshServiceBuilder::default() + .id("did:refresh-service") + .types("Refresh2020".to_string()) + .build() + .unwrap(); + + let terms = vec![ + TermsOfUseBuilder::default() + .types("Policy2019".to_string()) + .build() + .unwrap(), + TermsOfUseBuilder::default() + .types("Policy2020".to_string()) + .build() + .unwrap(), + ]; + + let presentation = PresentationBuilder::new() + .context(vec![ + Context::from(Presentation::BASE_CONTEXT), + Context::from("https://www.w3.org/2018/credentials/examples/v1"), + ]) + .id("did:example:id:123") + .types(vec![Presentation::BASE_TYPE.into(), "PrescriptionCredential".into()]) + .credential(verifiable.clone()) + .refresh_service(refresh_service) + .terms_of_use(terms) + .build() + .unwrap(); + + assert_eq!(presentation.context.len(), 2); + assert!(matches!(presentation.context.get(0).unwrap(), Context::Url(ref url) if url == Presentation::BASE_CONTEXT)); + assert!( + matches!(presentation.context.get(1).unwrap(), Context::Url(ref url) if url == "https://www.w3.org/2018/credentials/examples/v1") + ); + + assert_eq!(presentation.id, Some("did:example:id:123".into())); + + assert_eq!(presentation.types.len(), 2); + assert_eq!(presentation.types.get(0).unwrap(), Presentation::BASE_TYPE); + assert_eq!(presentation.types.get(1).unwrap(), "PrescriptionCredential"); + + assert_eq!(presentation.verifiable_credential.len(), 1); + assert_eq!(presentation.verifiable_credential.get(0).unwrap(), &verifiable); + + assert_eq!(presentation.refresh_service.unwrap().len(), 1); + assert_eq!(presentation.terms_of_use.unwrap().len(), 2); +} + +#[test] +#[should_panic = "InvalidUrl"] +fn test_builder_invalid_id_fmt() { + PresentationBuilder::new() + .id("foo") + .build() + .unwrap_or_else(|error| panic!("{}", error)); +} + +#[test] +#[should_panic = "InvalidUrl"] +fn test_builder_invalid_holder_fmt() { + PresentationBuilder::new() + .id("did:iota:123") + .holder("d00m") + .build() + .unwrap_or_else(|error| panic!("{}", error)); +} diff --git a/identity_core/tests/utils.rs b/identity_core/tests/utils.rs index acfaccd9d0..c334c3770d 100644 --- a/identity_core/tests/utils.rs +++ b/identity_core/tests/utils.rs @@ -1,11 +1,10 @@ use identity_core::{ - did::DID, - utils::{Context, KeyData, PublicKey, Service, ServiceEndpoint, Subject}, + common::{AsJson as _, Context, OneOrMany, Url}, + did::{Service, ServiceBuilder, ServiceEndpointBuilder, DID}, + key::{KeyData, KeyType, PublicKey, PublicKeyBuilder}, }; -use std::str::FromStr; - -const JSON_STR: &str = include_str!("utils.json"); +const JSON_STR: &str = include_str!("fixtures/did/utils.json"); fn setup_json(key: &str) -> String { let json_str = json::parse(JSON_STR).unwrap(); @@ -17,10 +16,7 @@ fn setup_json(key: &str) -> String { #[test] fn test_context() { let raw_str = r#"["https://w3id.org/did/v1","https://w3id.org/security/v1"]"#; - let ctx = Context::new(vec![ - "https://w3id.org/did/v1".into(), - "https://w3id.org/security/v1".into(), - ]); + let ctx: OneOrMany = vec!["https://w3id.org/did/v1".into(), "https://w3id.org/security/v1".into()].into(); let string = serde_json::to_string(&ctx).unwrap(); @@ -31,76 +27,31 @@ fn test_context() { #[test] fn test_subject_from_string() { let raw_str = r#""did:iota:123456789abcdefghi""#; - let subject = Subject::new("did:iota:123456789abcdefghi".into()).unwrap(); + let subject: DID = "did:iota:123456789abcdefghi".parse().unwrap(); let string = serde_json::to_string(&subject).unwrap(); assert_eq!(string, raw_str); } -/// Test building a subject from a DID structure. -#[test] -fn test_subject_from_did() { - let did = DID { - method_name: "iota".into(), - id_segments: vec!["123456".into(), "789011".into()], - query: Some(vec![("name".into(), Some("value".into())).into()]), - ..Default::default() - } - .init() - .unwrap(); - - let string = format!("\"{}\"", did.to_string()); - - let subject = Subject::from_did(did).unwrap(); - let res = serde_json::to_string(&subject).unwrap(); - - assert_eq!(res, string); -} - -/// Test Subject from a DID using the From Trait. -#[test] -fn test_subject_from() { - let did = DID { - method_name: "iota".into(), - id_segments: vec!["123456".into(), "789011".into()], - query: Some(vec![("name".into(), Some("value".into())).into()]), - ..Default::default() - } - .init() - .unwrap(); - - let string = format!("\"{}\"", did.to_string()); - - let subject: Subject = did.into(); - - let res = serde_json::to_string(&subject).unwrap(); - - assert_eq!(res, string); -} - /// Test public key structure from String and with PublicKey::new #[test] fn test_public_key() { let raw_str = setup_json("public"); - let pk_t = PublicKey::from_str(&raw_str).unwrap(); - - let key_data = KeyData::Base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into()); + let pk_t: PublicKey = serde_json::from_str(&raw_str).unwrap(); - let public_key = PublicKey { - id: "did:iota:123456789abcdefghi#keys-1".into(), - key_type: "Ed25519VerificationKey2018".into(), - controller: "did:iota:pqrstuvwxyz0987654321".into(), - key_data, - ..Default::default() - }; + let public_key = PublicKeyBuilder::default() + .id("did:iota:123456789abcdefghi#keys-1".parse().unwrap()) + .key_type(KeyType::Ed25519VerificationKey2018) + .controller("did:iota:pqrstuvwxyz0987654321".parse().unwrap()) + .key_data(KeyData::PublicKeyBase58( + "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".into(), + )) + .build() + .unwrap(); assert_eq!(public_key, pk_t); - - let res = serde_json::to_string(&public_key).unwrap(); - - assert_eq!(res, pk_t.to_string()); } /// Test service without ServiceEndpoint body. @@ -108,25 +59,20 @@ fn test_public_key() { fn test_service_with_no_endpoint_body() { let raw_str = setup_json("service"); - let endpoint = ServiceEndpoint { - context: "https://edv.example.com/".into(), - ..Default::default() - } - .init(); - - let service = Service { - id: "did:into:123#edv".into(), - service_type: "EncryptedDataVault".into(), - endpoint, - }; + let service = ServiceBuilder::default() + .id("did:into:123#edv".parse().unwrap()) + .service_type("EncryptedDataVault") + .endpoint(Url::parse("https://edv.example.com/").unwrap()) + .build() + .unwrap(); - let service_2: Service = Service::from_str(&raw_str).unwrap(); + let service_2: Service = Service::from_json(&raw_str).unwrap(); assert_eq!(service, service_2); let res = serde_json::to_string(&service).unwrap(); - assert_eq!(res, service_2.to_string()); + assert_eq!(res, service_2.to_json().unwrap()); } /// Test Service with a ServiceEndpoint Body. @@ -134,24 +80,25 @@ fn test_service_with_no_endpoint_body() { fn test_service_with_body() { let raw_str = setup_json("endpoint"); - let endpoint = ServiceEndpoint { - context: "https://schema.identity.foundation/hub".into(), - endpoint_type: Some("UserHubEndpoint".into()), - instances: Some(vec!["did:example:456".into(), "did:example:789".into()]), - } - .init(); + let endpoint = ServiceEndpointBuilder::default() + .context("https://schema.identity.foundation/hub".parse().unwrap()) + .endpoint_type("UserHubEndpoint") + .instances(vec!["did:example:456".into(), "did:example:789".into()]) + .build() + .unwrap(); - let service = Service { - id: "did:example:123456789abcdefghi#hub".into(), - service_type: "IdentityHub".into(), - endpoint, - }; + let service = ServiceBuilder::default() + .id("did:example:123456789abcdefghi#hub".parse().unwrap()) + .service_type("IdentityHub") + .endpoint(endpoint) + .build() + .unwrap(); - let service_2: Service = Service::from_str(&raw_str).unwrap(); + let service_2: Service = Service::from_json(&raw_str).unwrap(); assert_eq!(service, service_2); let res = serde_json::to_string(&service).unwrap(); - assert_eq!(res, service_2.to_string()); + assert_eq!(res, service_2.to_json().unwrap()); } diff --git a/identity_core/tests/vc_serde.rs b/identity_core/tests/vc_serde.rs new file mode 100644 index 0000000000..48523807a2 --- /dev/null +++ b/identity_core/tests/vc_serde.rs @@ -0,0 +1,38 @@ +use identity_core::vc::*; +use serde_json::from_str; + +fn try_credential(data: &(impl AsRef + ?Sized)) { + from_str::(data.as_ref()) + .unwrap() + .validate() + .unwrap() +} + +fn try_presentation(data: &(impl AsRef + ?Sized)) { + from_str::(data.as_ref()) + .unwrap() + .validate() + .unwrap() +} + +#[test] +fn test_parse_credential_examples() { + try_credential(include_str!("fixtures/vc/example-01.json")); + try_credential(include_str!("fixtures/vc/example-02.json")); + try_credential(include_str!("fixtures/vc/example-03.json")); + try_credential(include_str!("fixtures/vc/example-04.json")); + try_credential(include_str!("fixtures/vc/example-05.json")); + try_credential(include_str!("fixtures/vc/example-06.json")); + try_credential(include_str!("fixtures/vc/example-07.json")); + + try_credential(include_str!("fixtures/vc/example-09.json")); + try_credential(include_str!("fixtures/vc/example-10.json")); + try_credential(include_str!("fixtures/vc/example-11.json")); + try_credential(include_str!("fixtures/vc/example-12.json")); + try_credential(include_str!("fixtures/vc/example-13.json")); +} + +#[test] +fn test_parse_presentation_examples() { + try_presentation(include_str!("fixtures/vc/example-08.json")); +} diff --git a/identity_crypto/Cargo.toml b/identity_crypto/Cargo.toml index d6c0d59007..9e0c5b260d 100644 --- a/identity_crypto/Cargo.toml +++ b/identity_crypto/Cargo.toml @@ -15,7 +15,6 @@ homepage = "https://www.iota.org" [dependencies] anyhow = "1.0" hex = "0.4" -identity_common = { path = "../identity_common" } sha2 = "0.9" sha3 = "0.9" thiserror = "1.0" diff --git a/identity_crypto/src/error.rs b/identity_crypto/src/error.rs index 44c3c702f0..c32c6ca7a1 100644 --- a/identity_crypto/src/error.rs +++ b/identity_crypto/src/error.rs @@ -1,5 +1,3 @@ -use identity_common::impl_error_ctor; - #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Invalid key: `{0}`")] @@ -16,13 +14,4 @@ pub enum Error { Custom(#[from] anyhow::Error), } -impl Error { - impl_error_ctor!(key_error, KeyError, Into); - impl_error_ctor!(sign_error, SignError, Into); - impl_error_ctor!(verify_error, VerifyError, Into); - impl_error_ctor!(create_proof, CreateProof, Into); - impl_error_ctor!(verify_proof, VerifyProof, Into); - impl_error_ctor!(custom, Custom, Into); -} - pub type Result = anyhow::Result; diff --git a/identity_derive/src/impls/structs.rs b/identity_derive/src/impls/structs.rs index 2e8b2fc29c..c4b9bc4362 100644 --- a/identity_derive/src/impls/structs.rs +++ b/identity_derive/src/impls/structs.rs @@ -295,7 +295,7 @@ pub fn impl_from_into(input: &InputModel) -> TokenStream { impl <#(#param_decls),*> std::convert::From<#diff<#params>> for #name<#params> #clause { fn from(diff: #diff<#params>) -> Self { - Self::from_diff(diff).expect("Unable to conver from diff") + Self::from_diff(diff).expect("Unable to convert from diff") } } } diff --git a/identity_diff/src/hashmap.rs b/identity_diff/src/hashmap.rs index 2fd8e03917..cadb9db73d 100644 --- a/identity_diff/src/hashmap.rs +++ b/identity_diff/src/hashmap.rs @@ -19,43 +19,18 @@ pub enum InnerValue { Remove { key: K }, } -/// A `DiffHashMap` type which represents a Diffed `HashMap`. By default this value is transparent to `serde`. +/// A `DiffHashMap` type which represents a Diffed `HashMap`. By default this value is transparent to `serde`. #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(transparent)] pub struct DiffHashMap( #[serde(skip_serializing_if = "Option::is_none")] pub Option>>, ); -impl DiffHashMap -where - K: Clone + Debug + PartialEq + Ord + Hash + Diff + for<'de> Deserialize<'de> + Serialize, - V: Clone + Debug + PartialEq + Ord + Diff + for<'de> Deserialize<'de> + Serialize, -{ - /// Converts the `DiffHashMap` into an iterator where the Item is the `InnerValue` - pub fn into_iter<'a>(self) -> Box> + 'a> - where - Self: 'a, - { - match self.0 { - Some(diff) => Box::new(diff.into_iter()), - None => Box::new(empty()), - } - } - - /// Returns the length of the `DiffHashMap` - pub fn len(&self) -> usize { - match &self.0 { - Some(d) => d.len(), - None => 0, - } - } -} - /// Diff Implementation on a HashMap impl Diff for HashMap where - K: Clone + Debug + PartialEq + Ord + Hash + Diff + for<'de> Deserialize<'de> + Serialize + Default, - V: Clone + Debug + PartialEq + Ord + Diff + for<'de> Deserialize<'de> + Serialize + Default, + K: Clone + Debug + PartialEq + Eq + Hash + Diff + for<'de> Deserialize<'de> + Serialize + Default, + V: Clone + Debug + PartialEq + Diff + for<'de> Deserialize<'de> + Serialize + Default, { /// the Diff type of the HashMap type Type = DiffHashMap; @@ -98,7 +73,7 @@ where fn merge(&self, diff: Self::Type) -> crate::Result { let mut new = self.clone(); - for change in diff.into_iter() { + for change in diff.0.into_iter().flatten() { match change { InnerValue::Change { key, value } => { let fake: &mut V = &mut *new.get_mut(&key).expect("Failed to get value"); diff --git a/identity_diff/src/hashset.rs b/identity_diff/src/hashset.rs index b4c42fcd80..9d3da90274 100644 --- a/identity_diff/src/hashset.rs +++ b/identity_diff/src/hashset.rs @@ -19,7 +19,7 @@ pub enum InnerValue { impl Diff for HashSet where - T: Debug + Clone + PartialEq + Ord + Diff + Hash + for<'de> Deserialize<'de> + Serialize, + T: Debug + Clone + PartialEq + Eq + Diff + Hash + for<'de> Deserialize<'de> + Serialize, { type Type = DiffHashSet; @@ -93,35 +93,6 @@ where } } -impl DiffHashSet -where - T: Clone + Debug + PartialEq + Diff + for<'de> Deserialize<'de> + Serialize, -{ - pub fn iter<'v>(&'v self) -> Box> + 'v> { - match &self.0 { - Some(diffs) => Box::new(diffs.iter()), - None => Box::new(empty()), - } - } - - pub fn into_iter<'v>(self) -> Box> + 'v> - where - Self: 'v, - { - match self.0 { - Some(diffs) => Box::new(diffs.into_iter()), - None => Box::new(empty()), - } - } - - pub fn len(&self) -> usize { - match &self.0 { - Some(diffs) => diffs.len(), - None => 0, - } - } -} - impl Debug for DiffHashSet where T: Debug + Diff, diff --git a/identity_diff/src/lib.rs b/identity_diff/src/lib.rs index 95244614fa..e1482526f4 100644 --- a/identity_diff/src/lib.rs +++ b/identity_diff/src/lib.rs @@ -5,15 +5,15 @@ /// `bool`, and `char` types. Structs and Enums are supported via `identity_derive` and can be composed of any number /// of these types. mod error; -mod hashmap; -mod hashset; +pub mod hashmap; +pub mod hashset; mod macros; pub mod option; -mod string; +pub mod string; mod traits; #[cfg(feature = "serde_value")] mod value; -mod vec; +pub mod vec; pub use error::{Error, Result}; pub use traits::Diff; diff --git a/identity_diff/src/vec.rs b/identity_diff/src/vec.rs index 43206e71f0..1f36089c76 100644 --- a/identity_diff/src/vec.rs +++ b/identity_diff/src/vec.rs @@ -19,30 +19,10 @@ pub enum InnerVec { Add(::Type), } -impl DiffVec { - /// returns an iterator of `&InnerVec` from a `DiffVec` - pub fn iter<'d>(&'d self) -> Box> + 'd> { - Box::new(self.0.iter()) - } - - /// returns an iterator of `InnerVec` from a `DiffVec` - pub fn into_iter<'d>(self) -> Box> + 'd> - where - Self: 'd, - { - Box::new(self.0.into_iter()) - } - - /// Returns the length of the `DiffVec` type. - pub fn len(&self) -> usize { - self.0.len() - } -} - /// `Diff` trait implementation for `Vec` impl Diff for Vec where - T: Clone + Debug + PartialEq + Diff + Default + for<'de> Deserialize<'de> + Serialize, + T: Clone + Debug + PartialEq + Diff + for<'de> Deserialize<'de> + Serialize, { /// Corresponding Diff Type for `Vec` type Type = DiffVec; @@ -76,7 +56,7 @@ where fn merge(&self, diff: Self::Type) -> crate::Result { let mut vec: Self = self.clone(); - for change in diff.into_iter() { + for change in diff.0.into_iter() { match change { InnerVec::Add(d) => vec.push(::from_diff(d)?), InnerVec::Change { index, item } => vec[index] = self[index].merge(item)?, diff --git a/identity_integration/Cargo.toml b/identity_integration/Cargo.toml deleted file mode 100644 index 3e4541519b..0000000000 --- a/identity_integration/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "identity_integration" -version = "0.1.0" -authors = ["IOTA Identity"] -edition = "2018" -description = "A DID to ledger intergration library for IOTA" -readme = "../README.md" -repository = "https://github.com/iotaledger/identity.rs" -license = "Apache-2.0" -keywords = ["iota", "tangle", "identity"] -homepage = "https://www.iota.org" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/identity_integration/src/lib.rs b/identity_integration/src/lib.rs deleted file mode 100644 index 31e1bb209f..0000000000 --- a/identity_integration/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml new file mode 100644 index 0000000000..9f1996d2c4 --- /dev/null +++ b/identity_iota/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "identity_iota" +version = "0.1.0" +authors = ["IOTA Identity"] +edition = "2018" +description = "A DID to ledger intergration library for IOTA" +readme = "../README.md" +repository = "https://github.com/iotaledger/identity.rs" +license = "Apache-2.0" +keywords = ["iota", "tangle", "identity"] +homepage = "https://www.iota.org" + +[dependencies] +anyhow = { version = "1.0", default-features = false, features = ["std"] } +async-trait = { version = "0.1", default-features = false } +bs58 = { version = "0.3", default-features = false, features = ["alloc"] } +iota-constants = { version = "0.2", default-features = false } +iota-conversion = { version = "0.5", default-features = false } +# iota-core = { git = "https://github.com/iotaledger/iota.rs", branch = "dev" } +iota-core = { git = "https://github.com/Thoralf-M/iota.rs", branch = "works" } +multihash = { version = "0.11", default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } +thiserror = { version = "1.0", default-features = false } + +identity_core = { path = "../identity_core" } +identity_crypto = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/identity-signature-suites" } + +[dev-dependencies] +smol-potat = { version = "0.3.3" } +smol = { version = "0.1.18", features = ["tokio02"] } +hex = "0.4.2" +identity_diff = { path = "../identity_diff" } diff --git a/identity_iota/examples/mem_resolver.rs b/identity_iota/examples/mem_resolver.rs new file mode 100644 index 0000000000..60acd073b3 --- /dev/null +++ b/identity_iota/examples/mem_resolver.rs @@ -0,0 +1,157 @@ +use async_trait::async_trait; +use std::collections::BTreeMap; + +use identity_core::{ + self as core, + common::{AsJson as _, Object, OneOrMany, Timestamp, Url}, + did::{DIDDocument, DIDDocumentBuilder, ServiceBuilder, DID}, + key::{KeyData, KeyType, PublicKey, PublicKeyBuilder}, + resolver::{ + dereference, resolve, Dereference, DocumentMetadata, InputMetadata, MetaDocument, Resolution, ResolverMethod, + }, +}; +use identity_crypto::{Ed25519, KeyGen as _, KeyPair}; +use identity_iota::error::Result; +use multihash::{Blake2b256, MultihashGeneric}; + +#[smol_potat::main] +async fn main() -> Result<()> { + let mut resolver: MemResolver = MemResolver::new(); + + let keypair: KeyPair = Ed25519::generate(&Ed25519, Default::default())?; + let public: String = bs58::encode(keypair.public()).into_string(); + + let ident: MultihashGeneric<_> = Blake2b256::digest(public.as_bytes()); + let ident: String = bs58::encode(ident.digest()).into_string(); + + let did: DID = format!("did:iota:com:{}", ident).parse()?; + let key: DID = format!("did:iota:com:{}#key-1", ident).parse()?; + let srv: DID = format!("did:iota:com:{}#srv-1", ident).parse()?; + + let pkey: PublicKey = PublicKeyBuilder::default() + .id(key.clone()) + .controller(did.clone()) + .key_type(KeyType::Ed25519VerificationKey2018) + .key_data(KeyData::PublicKeyBase58(public)) + .build() + .unwrap(); + + let doc: DIDDocument = DIDDocumentBuilder::default() + .context(OneOrMany::One(DID::BASE_CONTEXT.into())) + .id(did.clone()) + .public_keys(vec![pkey.clone()]) + .auth(vec![key.clone().into()]) + .assert(vec![key.clone().into()]) + .verification(vec![key.clone().into()]) + .delegation(vec![key.clone().into()]) + .invocation(vec![key.clone().into()]) + .agreement(vec![key.clone().into()]) + .services(vec![ServiceBuilder::default() + .id(srv.clone()) + .service_type("MyService") + .endpoint(Url::parse("https://example.com")?) + .build() + .unwrap()]) + .build() + .unwrap(); + + println!("> Doc"); + println!("{}", doc.to_json_pretty()?); + + resolver.add(doc)?; + + let did: String = did.to_string(); + let key: String = key.to_string(); + let srv: String = format!("{}?service=srv-1", did); + + let data: Resolution = resolve(&did, InputMetadata::new(), &resolver).await?; + println!("> Resolution"); + println!("{}", data.to_json_pretty()?); + + let data: Resolution = resolve("not-a-did", InputMetadata::new(), &resolver).await?; + println!("> Resolution (invalid-did)"); + println!("{}", data.to_json_pretty()?); + + let data: Resolution = resolve("did:iota:1234", InputMetadata::new(), &resolver).await?; + println!("> Resolution (not-found)"); + println!("{}", data.to_json_pretty()?); + + let data: Dereference = dereference(&key, InputMetadata::new(), &resolver).await?; + println!("> Dereference (key)"); + println!("{}", data.to_json_pretty()?); + + let data: Dereference = dereference(&srv, InputMetadata::new(), &resolver).await?; + println!("> Dereference (service)"); + println!("{}", data.to_json_pretty()?); + + Ok(()) +} + +#[derive(Clone, Debug, Default)] +pub struct MemResolver { + store: Vec, + index: BTreeMap, +} + +impl MemResolver { + pub fn new() -> Self { + Self { + store: Vec::new(), + index: BTreeMap::new(), + } + } + + pub fn add(&mut self, document: DIDDocument) -> Result<()> { + if let Some(index) = self.index(&document.id) { + let current: &mut MetaDocument = self.store.get_mut(index).expect("infallible"); + + current.data = document; + current.meta.updated = Some(Timestamp::now()); + } else { + let did: DID = self.clean(&document.id); + let idx: usize = self.store.len(); + + self.store.push(MetaDocument { + data: document, + meta: DocumentMetadata { + created: Some(Timestamp::now()), + updated: Some(Timestamp::now()), + properties: Object::new(), + }, + }); + + self.index.insert(did, idx); + } + + Ok(()) + } + + pub fn get(&self, did: &DID) -> Option<&MetaDocument> { + self.index(did).and_then(|index| self.store.get(index)) + } + + pub fn index(&self, did: &DID) -> Option { + self.index.get(&self.clean(did)).copied() + } + + fn clean(&self, did: &DID) -> DID { + DID { + method_name: did.method_name.clone(), + id_segments: did.id_segments.clone(), + path_segments: None, + query: None, + fragment: None, + } + } +} + +#[async_trait] +impl ResolverMethod for MemResolver { + fn is_supported(&self, _: &DID) -> bool { + true + } + + async fn read(&self, did: &DID, _: InputMetadata) -> core::Result> { + Ok(self.get(did).cloned()) + } +} diff --git a/identity_iota/examples/publish.rs b/identity_iota/examples/publish.rs new file mode 100644 index 0000000000..a2b349e96e --- /dev/null +++ b/identity_iota/examples/publish.rs @@ -0,0 +1,38 @@ +//! Publish new did document and read it from the tangle +//! cargo run --example publish + +use identity_crypto::{Ed25519, KeyGen}; +use identity_iota::{ + did::TangleDocument as _, + error::Result, + helpers::create_document, + io::TangleWriter, + network::{Network, NodeList}, +}; +use iota_conversion::Trinary as _; + +#[smol_potat::main] +async fn main() -> Result<()> { + let nodes = vec!["http://localhost:14265", "https://nodes.comnet.thetangle.org:443"]; + let nodelist = NodeList::with_network_and_nodes(Network::Comnet, nodes); + + let tangle_writer = TangleWriter::new(&nodelist)?; + + // Create keypair + let keypair = Ed25519::generate(&Ed25519, Default::default())?; + let bs58_auth_key = bs58::encode(keypair.public()).into_string(); + + // Create, sign and publish DID document to the Tangle + let mut did_document = create_document(bs58_auth_key)?; + + did_document.sign_unchecked(keypair.secret())?; + + let tail_transaction = tangle_writer.write_json(did_document.did(), &did_document).await?; + + println!( + "DID document published: https://comnet.thetangle.org/transaction/{}", + tail_transaction.as_i8_slice().trytes().expect("Couldn't get Trytes") + ); + + Ok(()) +} diff --git a/identity_iota/examples/publish_read.rs b/identity_iota/examples/publish_read.rs new file mode 100644 index 0000000000..89f7d3e37c --- /dev/null +++ b/identity_iota/examples/publish_read.rs @@ -0,0 +1,88 @@ +//! Publish new did document and read it from the tangle +//! cargo run --example publish_read + +use anyhow::Result; +use identity_core::{did::DIDDocument, diff::Diff}; +use identity_crypto::{Ed25519, KeyGen, KeyGenerator}; +use identity_iota::{ + did::{DIDDiff, DIDProof, TangleDocument as _}, + helpers::create_document, + io::{TangleReader, TangleWriter}, + network::{Network, NodeList}, +}; +use iota_conversion::Trinary as _; + +#[smol_potat::main] +async fn main() -> Result<()> { + let nodes = vec!["http://localhost:14265", "https://nodes.comnet.thetangle.org:443"]; + let nodelist = NodeList::with_network_and_nodes(Network::Comnet, nodes); + + let tangle_writer = TangleWriter::new(&nodelist)?; + + // Create keypair + let keypair = Ed25519::generate(&Ed25519, KeyGenerator::default())?; + let bs58_auth_key = bs58::encode(keypair.public()).into_string(); + + // Create, sign and publish DID document to the Tangle + let mut did_document = create_document(bs58_auth_key)?; + + did_document.sign_unchecked(keypair.secret())?; + + let tail_transaction = tangle_writer.write_json(did_document.did(), &did_document).await?; + + println!( + "DID document published: https://comnet.thetangle.org/transaction/{}", + tail_transaction.as_i8_slice().trytes().expect("Couldn't get Trytes") + ); + + // Create, sign and publish diff to the Tangle + let signed_diff = create_diff(did_document.clone(), &keypair).await?; + let tail_transaction = tangle_writer.publish_json(&did_document.did(), &signed_diff).await?; + + println!( + "DID document DIDDiff published: https://comnet.thetangle.org/transaction/{}", + tail_transaction.as_i8_slice().trytes().expect("Couldn't get Trytes") + ); + + // Get document and diff from the tangle and validate the signatures + let did = did_document.did(); + let tangle_reader = TangleReader::new(&nodelist)?; + + let received_messages = tangle_reader.fetch(&did).await?; + println!("{:?}", received_messages); + + let docs = TangleReader::extract_documents(&did, &received_messages)?; + println!("extracted docs: {:?}", docs); + + let diffs = TangleReader::extract_diffs(&did, &received_messages)?; + println!("extracted diffs: {:?}", diffs); + + let sig = docs[0].data.verify_unchecked().is_ok(); + println!("Document has valid signature: {}", sig); + + let sig = docs[0].data.verify_diff_unchecked(&diffs[0].data).is_ok(); + println!("Diff has valid signature: {}", sig); + + Ok(()) +} + +async fn create_diff(did_document: DIDDocument, keypair: &identity_crypto::KeyPair) -> crate::Result { + // updated doc and publish diff + let mut new = did_document.clone(); + + new.set_metadata("new-value", true); + new.update_time(); + + // diff the two docs. + let diff = did_document.diff(&new)?; + + let mut diddiff = DIDDiff { + id: new.did().clone(), + diff: serde_json::to_string(&diff)?, + proof: DIDProof::new(new.did().clone()), // TODO: This is wrong - should be the key DID + }; + + did_document.sign_diff_unchecked(&mut diddiff, keypair.secret())?; + + Ok(diddiff) +} diff --git a/identity_iota/src/did/did.rs b/identity_iota/src/did/did.rs new file mode 100644 index 0000000000..5a78ec3ca2 --- /dev/null +++ b/identity_iota/src/did/did.rs @@ -0,0 +1,31 @@ +use bs58::encode; +use identity_core::did::DID; +use iota::transaction::bundled::Address; +use multihash::Blake2b256; + +use crate::{ + error::{Error, Result}, + utils::{create_address_from_trits, utf8_to_trytes}, +}; + +pub fn method_id(did: &DID) -> Result<&str> { + did.id_segments + .last() + .map(|string| string.as_str()) + .ok_or_else(|| Error::InvalidMethodId) +} + +/// Creates an 81 Trytes IOTA address from the DID +pub fn create_address_hash(did: &DID) -> Result { + let digest: &[u8] = &Blake2b256::digest(method_id(did)?.as_bytes()); + let encoded: String = encode(digest).into_string(); + let mut trytes: String = utf8_to_trytes(&encoded); + + trytes.truncate(iota_constants::HASH_TRYTES_SIZE); + + Ok(trytes) +} + +pub fn create_address(did: &DID) -> Result
{ + create_address_hash(did).and_then(create_address_from_trits) +} diff --git a/identity_iota/src/did/diff.rs b/identity_iota/src/did/diff.rs new file mode 100644 index 0000000000..b56afe3dcd --- /dev/null +++ b/identity_iota/src/did/diff.rs @@ -0,0 +1,11 @@ +use identity_core::did::DID; +use serde::{Deserialize, Serialize}; + +use crate::did::DIDProof; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub struct DIDDiff { + pub id: DID, + pub diff: String, // TODO: Replace with DiffDIDDocument + pub proof: DIDProof, +} diff --git a/identity_iota/src/did/document.rs b/identity_iota/src/did/document.rs new file mode 100644 index 0000000000..0be6fc069e --- /dev/null +++ b/identity_iota/src/did/document.rs @@ -0,0 +1,197 @@ +use identity_core::{ + common::{AsJson as _, Object, SerdeInto as _, Value}, + did::DIDDocument, + key::{KeyData, KeyRelation, KeyType, PublicKey}, + utils::{decode_b58, decode_hex, encode_b58}, +}; +use identity_crypto::{Ed25519, Secp256k1, SecretKey, Sign as _, Verify as _}; +use serde::{Deserialize, Serialize}; + +use crate::{ + did::{DIDDiff, DIDProof, TangleDocument}, + error::{Error, Result}, +}; + +impl TangleDocument for DIDDocument { + fn sign_diff_unchecked(&self, diff: &mut DIDDiff, secret: &SecretKey) -> Result<()> { + // Get the first authentication key from the document. + let key: &PublicKey = self + .resolve_key(0, KeyRelation::Authentication) + .ok_or(Error::InvalidAuthenticationKey)?; + + // Reset the proof object in the diff. + diff.proof = DIDProof::new(key.id().clone()); + + // Create a signature from the diff JSON. + let signature: String = sign_canonicalized(diff, key.key_type(), secret)?; + + // Update the diff proof with the encoded signature. + diff.proof.signature = signature; + + Ok(()) + } + + fn verify_diff_unchecked(&self, diff: &DIDDiff) -> Result<()> { + // TODO: Validate diff.id + // TODO: Validate diff.prevMsg + + // Get the first authentication key from the document. + let key: &PublicKey = self + .resolve_key(0, KeyRelation::Authentication) + .ok_or(Error::InvalidAuthenticationKey)?; + + verify_canonicalized(diff, key) + } + + fn sign_unchecked(&mut self, secret: &SecretKey) -> Result<()> { + // Get the first authentication key from the document. + let key: &PublicKey = self + .resolve_key(0, KeyRelation::Authentication) + .ok_or(Error::InvalidAuthenticationKey)?; + + let key_type: KeyType = key.key_type(); + let proof: DIDProof = DIDProof::new(key.id().clone()); + let proof: Object = proof.serde_into()?; + + // Reset the proof object in the document. + self.set_metadata("proof", proof); + + // Create a signature from the document JSON. + let signature: String = sign_canonicalized(self, key_type, secret)?; + + // Update the document proof with the encoded signature. + // + // Note: This access should not panic since we already set the "proof" object. + self.metadata_mut()["proof"]["signature"] = signature.into(); + + Ok(()) + } + + fn verify_unchecked(&self) -> Result<()> { + // Get the first authentication key from the document. + let key: &PublicKey = self + .resolve_key(0, KeyRelation::Authentication) + .ok_or(Error::InvalidAuthenticationKey)?; + + // TODO: Validate self.id == DID::parse(key.key_data()) + + verify_canonicalized(self, key) + } +} + +fn sign_canonicalized(data: &T, key_type: KeyType, secret: &SecretKey) -> Result +where + T: for<'de> Deserialize<'de> + Serialize, +{ + // Serialize as canonicalized JSON. + // TODO: Canonicalize + let data: Vec = data.to_json_vec()?; + + // Create a signature from the canonicalized JSON. + let signature: Vec = sign(&data, key_type, secret)?; + + Ok(encode_b58(&signature)) +} + +fn sign(data: &[u8], key_type: KeyType, secret: &SecretKey) -> Result> { + match key_type { + KeyType::JsonWebKey2020 => todo!("Not Supported: JsonWebKey2020"), + KeyType::EcdsaSecp256k1VerificationKey2019 => Secp256k1.sign(&data, secret).map_err(Into::into), + KeyType::Ed25519VerificationKey2018 => Ed25519.sign(&data, secret).map_err(Into::into), + KeyType::GpgVerificationKey2020 => todo!("Not Supported: GpgVerificationKey2020"), + KeyType::RsaVerificationKey2018 => todo!("Not Supported: RsaVerificationKey2018"), + KeyType::X25519KeyAgreementKey2019 => todo!("Not Supported: X25519KeyAgreementKey2019"), + KeyType::SchnorrSecp256k1VerificationKey2019 => todo!("Not Supported: SchnorrSecp256k1VerificationKey2019"), + KeyType::EcdsaSecp256k1RecoveryMethod2020 => todo!("Not Supported: EcdsaSecp256k1RecoveryMethod2020"), + } +} + +fn verify_canonicalized(data: &T, key: &PublicKey) -> Result<()> +where + T: for<'de> Deserialize<'de> + Serialize, +{ + // Convert the diff to a JSON object. + let mut data: Object = data.serde_into()?; + + // Remove the signature from the proof. + let signature: Vec = data + .get_mut("proof") + .ok_or(Error::InvalidProof)? + .as_object_mut() + .ok_or(Error::InvalidProof)? + .remove("signature") + .and_then(|value| match value { + Value::String(value) => decode_b58(&value).ok(), + _ => None, + }) + .ok_or(Error::InvalidProof)?; + + // Serialize as canonicalized JSON. + // TODO: Canonicalize + let data: Vec = data.to_json_vec()?; + + if verify(&data, &signature, key)? { + Ok(()) + } else { + Err(Error::InvalidProof) + } +} + +fn verify(data: &[u8], signature: &[u8], key: &PublicKey) -> Result { + match (key.key_type(), key.key_data()) { + (KeyType::JsonWebKey2020, KeyData::PublicKeyJwk(_)) => todo!("Not Supported: JsonWebKey2020/PublicKeyJwk"), + + (KeyType::EcdsaSecp256k1VerificationKey2019, KeyData::PublicKeyHex(inner)) => { + let key: Vec = decode_hex(inner)?; + let valid: bool = Secp256k1.verify(data, signature, &key.into())?; + + Ok(valid) + } + (KeyType::EcdsaSecp256k1VerificationKey2019, KeyData::PublicKeyJwk(_)) => { + todo!("Not Supported: EcdsaSecp256k1VerificationKey2019/PublicKeyJwk") + } + + (KeyType::Ed25519VerificationKey2018, KeyData::PublicKeyJwk(_)) => { + todo!("Not Supported: Ed25519VerificationKey2018/PublicKeyJwk") + } + (KeyType::Ed25519VerificationKey2018, KeyData::PublicKeyBase58(inner)) => { + let key: Vec = decode_b58(inner)?; + let valid: bool = Ed25519.verify(data, signature, &key.into())?; + + Ok(valid) + } + + // (KeyType::GpgVerificationKey2020, KeyData::PublicKeyGpg(_)) => {} + (KeyType::RsaVerificationKey2018, KeyData::PublicKeyJwk(_)) => { + todo!("Not Supported: RsaVerificationKey2018/PublicKeyJwk") + } + (KeyType::RsaVerificationKey2018, KeyData::PublicKeyPem(_)) => { + todo!("Not Supported: RsaVerificationKey2018/PublicKeyPem") + } + + (KeyType::X25519KeyAgreementKey2019, KeyData::PublicKeyJwk(_)) => { + todo!("Not Supported: X25519KeyAgreementKey2019/PublicKeyJwk") + } + (KeyType::X25519KeyAgreementKey2019, KeyData::PublicKeyBase58(_)) => { + todo!("Not Supported: X25519KeyAgreementKey2019/PublicKeyBase58") + } + + (KeyType::SchnorrSecp256k1VerificationKey2019, KeyData::PublicKeyJwk(_)) => { + todo!("Not Supported: SchnorrSecp256k1VerificationKey2019/PublicKeyJwk") + } + (KeyType::SchnorrSecp256k1VerificationKey2019, KeyData::PublicKeyBase58(_)) => { + todo!("Not Supported: SchnorrSecp256k1VerificationKey2019/PublicKeyBase58") + } + + (KeyType::EcdsaSecp256k1RecoveryMethod2020, KeyData::EthereumAddress(_)) => { + todo!("Not Supported: EcdsaSecp256k1RecoveryMethod2020/EthereumAddress") + } + (KeyType::EcdsaSecp256k1RecoveryMethod2020, KeyData::PublicKeyHex(_)) => { + todo!("Not Supported: EcdsaSecp256k1RecoveryMethod2020/PublicKeyHex") + } + (KeyType::EcdsaSecp256k1RecoveryMethod2020, KeyData::PublicKeyJwk(_)) => { + todo!("Not Supported: EcdsaSecp256k1RecoveryMethod2020/PublicKeyJwk") + } + (_, _) => todo!("Invalid KeyType/KeyData Configuration"), + } +} diff --git a/identity_iota/src/did/mod.rs b/identity_iota/src/did/mod.rs new file mode 100644 index 0000000000..4bf5f0349b --- /dev/null +++ b/identity_iota/src/did/mod.rs @@ -0,0 +1,12 @@ +#[allow(clippy::module_inception)] +mod did; +mod diff; +mod document; +mod proof; +mod traits; + +pub use did::*; +pub use diff::*; +pub use document::*; +pub use proof::*; +pub use traits::*; diff --git a/identity_iota/src/did/proof.rs b/identity_iota/src/did/proof.rs new file mode 100644 index 0000000000..05224787e1 --- /dev/null +++ b/identity_iota/src/did/proof.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +use identity_core::{common::Timestamp, did::DID}; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub struct DIDProof { + /// A DID URL referencing a DID document key used for signature creation. + pub id: DID, + /// A timestamp of when the DID proof was created. + pub created: Timestamp, + /// The signature value generated by the signature algorithm. + #[serde(skip_serializing_if = "String::is_empty")] + pub signature: String, +} + +impl DIDProof { + pub fn new(id: DID) -> Self { + Self { + id, + created: Timestamp::now(), + signature: String::new(), + } + } +} diff --git a/identity_iota/src/did/traits.rs b/identity_iota/src/did/traits.rs new file mode 100644 index 0000000000..c9d8982c45 --- /dev/null +++ b/identity_iota/src/did/traits.rs @@ -0,0 +1,13 @@ +use identity_crypto::SecretKey; + +use crate::{did::DIDDiff, error::Result}; + +pub trait TangleDocument { + fn sign_unchecked(&mut self, secret: &SecretKey) -> Result<()>; + + fn verify_unchecked(&self) -> Result<()>; + + fn sign_diff_unchecked(&self, diff: &mut DIDDiff, secret: &SecretKey) -> Result<()>; + + fn verify_diff_unchecked(&self, diff: &DIDDiff) -> Result<()>; +} diff --git a/identity_iota/src/error.rs b/identity_iota/src/error.rs new file mode 100644 index 0000000000..bafea025e7 --- /dev/null +++ b/identity_iota/src/error.rs @@ -0,0 +1,53 @@ +pub type Result = anyhow::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + CoreError(#[from] identity_core::Error), + #[error(transparent)] + CryptoError(#[from] identity_crypto::Error), + #[error(transparent)] + DiffError(#[from] identity_core::diff::Error), + #[error(transparent)] + ClientError(#[from] iota::client::error::Error), + #[error(transparent)] + TernaryError(#[from] iota::ternary::Error), + #[error("Invalid DID Method ID")] + InvalidMethodId, + #[error("Invalid DID Signature")] + InvalidSignature, + #[error("Invalid DID Authentication Key")] + InvalidAuthenticationKey, + #[error("Invalid DID Proof")] + InvalidProof, + #[error("Invalid Tryte Conversion")] + InvalidTryteConversion, + #[error("Invalid Transaction: {0}")] + InvalidTransaction(TransactionError), + #[error("Invalid Document: {0}")] + InvalidDocument(DocumentError), +} + +#[derive(Debug, thiserror::Error)] +pub enum TransactionError { + #[error("Invalid Bundle")] + InvalidBundle, + #[error("Missing Bundle")] + MissingBundle, + #[error("Missing Content")] + MissingContent, + #[error("Missing Trytes")] + MissingTrytes, + #[error("Unconfirmable Transaction")] + Unconfirmable, +} + +#[derive(Debug, thiserror::Error)] +pub enum DocumentError { + #[error("Missing Payload")] + MissingPayload, + #[error("Missing Timestamp (Updated)")] + MissingUpdated, + #[error("Invalid DID Network")] + NetworkMismatch, +} diff --git a/identity_iota/src/helpers.rs b/identity_iota/src/helpers.rs new file mode 100644 index 0000000000..1814bf722d --- /dev/null +++ b/identity_iota/src/helpers.rs @@ -0,0 +1,52 @@ +use identity_core::{ + common::OneOrMany, + did::{DIDDocument, DIDDocumentBuilder, DID}, + key::{KeyData, KeyType, PublicKey, PublicKeyBuilder}, +}; +use multihash::Blake2b256; + +use crate::error::Result; + +/// Creates a DID document with an auth key and a DID +pub fn create_document(auth_key: String) -> Result { + //create comnet id + let did: DID = create_method_id(&auth_key, Some("com"), None)?; + let key: DID = format!("{}#key-1", did).parse()?; + + let public_key: PublicKey = PublicKeyBuilder::default() + .id(key.clone()) + .controller(did.clone()) + .key_type(KeyType::Ed25519VerificationKey2018) + .key_data(KeyData::PublicKeyBase58(auth_key)) + .build() + .unwrap(); + + let doc: DIDDocument = DIDDocumentBuilder::default() + .context(OneOrMany::One(DID::BASE_CONTEXT.into())) + .id(did) + .public_keys(vec![public_key]) + .auth(vec![key.into()]) + .build() + .unwrap(); + + Ok(doc) +} + +pub fn create_method_id(pub_key: &str, network: Option<&str>, network_shard: Option) -> Result { + let hash = Blake2b256::digest(pub_key.as_bytes()); + let bs58key = bs58::encode(&hash.digest()).into_string(); + let network_string = match network { + Some(network_str) => match network_str { + "com" => "com:".to_string(), + "dev" => "dev:".to_string(), + _ => "".to_string(), + }, + _ => "".to_string(), // default: "main" also can be written as "" + }; + let shard_string = match &network_shard { + Some(shard) => format!("{}:", shard), + _ => String::new(), + }; + let id_string = format!("did:iota:{}{}{}", network_string, shard_string, bs58key); + Ok(DID::parse_from_str(id_string).unwrap()) +} diff --git a/identity_iota/src/io/mod.rs b/identity_iota/src/io/mod.rs new file mode 100644 index 0000000000..b06963d712 --- /dev/null +++ b/identity_iota/src/io/mod.rs @@ -0,0 +1,5 @@ +mod reader; +mod writer; + +pub use reader::*; +pub use writer::*; diff --git a/identity_iota/src/io/reader.rs b/identity_iota/src/io/reader.rs new file mode 100644 index 0000000000..8a2695f728 --- /dev/null +++ b/identity_iota/src/io/reader.rs @@ -0,0 +1,200 @@ +use identity_core::{ + common::{Object, Timestamp}, + did::{DIDDocument, DID}, + diff::Diff as _, +}; +use iota::{ + client::{FindTransactionsResponse, GetTrytesResponse}, + transaction::{ + bundled::{Address, BundledTransaction, BundledTransactionField as _}, + Vertex as _, + }, +}; +use serde_json::from_str; + +use std::collections::BTreeMap; + +use crate::{ + did::{create_address, method_id, DIDDiff}, + error::{DocumentError, Error, Result, TransactionError}, + network::{Network, NodeList}, + types::{TangleDiff, TangleDoc}, + utils::{encode_trits, trytes_to_utf8, txn_hash_trytes}, +}; + +type Bundles = BTreeMap>; +type Content = BTreeMap; + +#[derive(Debug)] +pub struct TangleReader { + client: iota::Client, + network: Network, +} + +impl TangleReader { + pub fn new(nodelist: &NodeList) -> Result { + let mut builder: iota::ClientBuilder = iota::ClientBuilder::new(); + + for node in nodelist.nodes() { + builder = builder.node(node)?; + } + + builder = builder.network(nodelist.network().into()); + + Ok(Self { + client: builder.build()?, + network: nodelist.network(), + }) + } + + pub async fn fetch(&self, did: &DID) -> Result { + let address: Address = create_address(&did)?; + + let response: FindTransactionsResponse = self.client.find_transactions().addresses(&[address]).send().await?; + + let content: GetTrytesResponse = self.client.get_trytes(&response.hashes).await?; + + if content.trytes.is_empty() { + return Err(Error::InvalidTransaction(TransactionError::MissingTrytes)); + } + + let bundles: Bundles = Self::index_bundles(content.trytes); + + if bundles.is_empty() { + return Err(Error::InvalidTransaction(TransactionError::MissingBundle)); + } + + let content: Content = Self::index_content(bundles)?; + + if content.is_empty() { + return Err(Error::InvalidTransaction(TransactionError::MissingContent)); + } + + Ok(content) + } + + pub async fn fetch_latest(&self, did: &DID) -> Result<(TangleDoc, Object)> { + let messages: Content = self.fetch(did).await?; + + let mut documents: Vec = Self::extract_documents(did, &messages)?; + + if documents.is_empty() { + return Err(Error::InvalidDocument(DocumentError::MissingPayload)); + } + + let diffs: Vec = Self::extract_diffs(did, &messages)?; + + // We checked above if the `documents` vec was empty - this should not fail. + let mut latest: TangleDoc = documents.remove(0); + let mut metadata: Object = Object::new(); + + for (index, diff) in diffs.into_iter().enumerate() { + let updated: &Timestamp = if let Some(updated) = latest.data.updated.as_ref() { + updated + } else { + return Err(Error::InvalidDocument(DocumentError::MissingUpdated)); + }; + + if diff.data.proof.created > *updated { + latest.data = latest.data.merge(DIDDocument::get_diff_from_str(&diff.data.diff)?)?; + metadata.insert(format!("hash:diff:{}", index), diff.hash.into()); + } + } + + metadata.insert("hash:doc".into(), latest.hash.as_str().into()); + + Ok((latest, metadata)) + } + + pub fn extract_documents<'a>(did: &DID, content: &'a Content) -> Result> { + let mid: &str = method_id(did)?; + + let mut documents: Vec = content + .iter() + .filter_map(|(hash, payload)| { + from_str::(payload) + .ok() + .filter(|document| matches!(method_id(document.did()), Ok(id) if id == mid)) + .map(|data| TangleDoc { + data, + hash: hash.into(), + }) + }) + .collect(); + + // Sort documents by updated timestamp in descending order + documents.sort_by(|a, b| b.data.updated.cmp(&a.data.updated)); + + Ok(documents) + } + + pub fn extract_diffs<'a>(did: &DID, content: &'a Content) -> Result> { + let mid: &str = method_id(did)?; + + let mut diffs: Vec = content + .iter() + .filter_map(|(hash, payload)| { + from_str::(payload) + .ok() + .filter(|diff| matches!(method_id(&diff.id), Ok(id) if id == mid)) + .map(|data| TangleDiff { + data, + hash: hash.into(), + }) + }) + .collect(); + + // Sort diffs by timestamp in ascending order + diffs.sort_by(|a, b| a.data.proof.created.cmp(&b.data.proof.created)); + + Ok(diffs) + } + + fn index_bundles(txns: Vec) -> Bundles { + let mut bundles: Bundles = BTreeMap::new(); + let mut overflow: BTreeMap = BTreeMap::new(); + + for txn in txns { + // Distinguish between tail transactions, because the content can be + // changed at reattachments. + if txn.is_tail() { + bundles.insert(txn_hash_trytes(&txn), vec![txn]); + } else { + overflow.insert(txn_hash_trytes(&txn), txn); + } + } + + for txns in bundles.values_mut() { + for index in 0..Self::bundle_idx(txns) { + let hash: String = encode_trits(txns[index].trunk().to_inner()); + + if let Some(trunk) = overflow.get(&hash) { + txns.push(trunk.clone()); + } else { + println!("[+] Missing Trunk Transaction: {}", hash); + } + } + } + + // Remove incomplete bundles + bundles + .into_iter() + .filter(|(_, txns)| txns.len() == Self::bundle_idx(&txns) + 1) + .collect() + } + + fn index_content(bundles: Bundles) -> Result { + bundles + .into_iter() + .map(|(hash, txns)| { + let trytes: String = txns.iter().map(|txn| encode_trits(txn.payload().to_inner())).collect(); + + trytes_to_utf8(&trytes).map(|trytes| (hash, trytes)) + }) + .collect() + } + + fn bundle_idx(txns: &[BundledTransaction]) -> usize { + txns.get(0).map(|txn| *txn.last_index().to_inner()).expect("infallible") + } +} diff --git a/identity_iota/src/io/writer.rs b/identity_iota/src/io/writer.rs new file mode 100644 index 0000000000..8e74f26111 --- /dev/null +++ b/identity_iota/src/io/writer.rs @@ -0,0 +1,172 @@ +use core::slice::from_ref; +use identity_core::did::{DIDDocument, DID}; +use iota::{ + client::{AttachToTangleResponse, GTTAResponse, Transfer}, + crypto::ternary::Hash, + transaction::bundled::{Bundle, BundledTransaction}, +}; +use serde::Serialize; +use serde_json::to_string; +use std::{thread, time::Duration}; + +use crate::{ + did::create_address, + error::{DocumentError, Error, Result, TransactionError}, + network::{Network, NodeList}, + utils::{create_address_from_trits, encode_trits, txn_hash}, +}; + +/// Tipselection depth +const TS_DEPTH: u8 = 2; + +/// Fixed-address used for faster transaction confirmation times +const PROMOTION: &str = "PROMOTEADDRESSPROMOTEADDRESSPROMOTEADDRESSPROMOTEADDRESSPROMOTEADDRESSPROMOTEADDR"; + +#[derive(Debug)] +pub struct Config { + confirm_delay: Duration, + promote_delay: Duration, + promote_limit: usize, +} + +impl Config { + const DEFAULT_CONFIRM_DELAY: Duration = Duration::from_secs(5); + const DEFAULT_PROMOTE_DELAY: Duration = Duration::from_secs(5); + const DEFAULT_PROMOTE_LIMIT: usize = 20; + + pub const fn new() -> Self { + Self { + confirm_delay: Self::DEFAULT_CONFIRM_DELAY, + promote_delay: Self::DEFAULT_PROMOTE_DELAY, + promote_limit: Self::DEFAULT_PROMOTE_LIMIT, + } + } +} + +#[derive(Debug)] +pub struct TangleWriter { + client: iota::Client, + config: Config, + network: Network, +} + +impl TangleWriter { + pub fn new(nodelist: &NodeList) -> Result { + let mut builder: iota::ClientBuilder = iota::ClientBuilder::new(); + + for node in nodelist.nodes() { + builder = builder.node(node)?; + } + + builder = builder.network(nodelist.network().into()); + + Ok(Self { + client: builder.build()?, + network: nodelist.network(), + config: Config::new(), + }) + } + + pub async fn write_document(&self, document: &DIDDocument) -> Result { + self.write_json(document.did(), document).await + } + + pub async fn write_json(&self, did: &DID, data: &T) -> Result + where + T: Serialize, + { + let txn: Hash = self.publish_json(did, data).await?; + + println!("[+] Transaction > {}", encode_trits(txn.as_trits())); + + let mut tries: usize = 0; + + thread::sleep(self.config.confirm_delay); + + while !self.is_confirmed(&txn).await? { + println!("[+] Promoting > {}", encode_trits(txn.as_trits())); + + tries += 1; + thread::sleep(self.config.promote_delay); + + if tries >= self.config.promote_limit { + return Err(Error::InvalidTransaction(TransactionError::Unconfirmable)); + } + + self.promote(&txn).await?; + } + + Ok(txn) + } + + pub async fn publish_json(&self, did: &DID, data: &T) -> Result + where + T: Serialize, + { + // Ensure the correct network is selected + if !self.network.matches_did(did) { + return Err(Error::InvalidDocument(DocumentError::NetworkMismatch)); + } + + // Build the transfer to push the serialized data to the tangle. + let transfer: Transfer = Transfer { + address: create_address(&did)?, + value: 0, + message: Some(to_string(&data).map_err(identity_core::Error::EncodeJSON)?), + tag: None, + }; + + // Dispatch the transfer to the network + let bundle: Vec = self.client.send(None).transfers(vec![transfer]).send().await?; + + // Extract the tail transaction from the response. + let tail: &BundledTransaction = bundle + .iter() + .find(|txn| txn.is_tail()) + .ok_or_else(|| Error::InvalidTransaction(TransactionError::InvalidBundle))?; + + Ok(txn_hash(tail)) + } + + async fn promote(&self, txn: &Hash) -> Result { + let transfer: Transfer = Transfer { + address: create_address_from_trits(PROMOTION)?, + value: 0, + message: None, + tag: None, + }; + + let transfers: Bundle = self + .client + .prepare_transfers(None) + .transfers(vec![transfer]) + .build() + .await?; + + let tips: GTTAResponse = self.client.get_transactions_to_approve().depth(TS_DEPTH).send().await?; + let tail: &BundledTransaction = transfers.tail(); + + let trytes: AttachToTangleResponse = self + .client + .attach_to_tangle() + .trunk_transaction(&txn) + .branch_transaction(&tips.branch_transaction) + .trytes(from_ref(tail)) + .send() + .await?; + + self.client.broadcast_transactions(&trytes.trytes).await?; + + Ok(encode_trits(tail.bundle().as_trits())) + } + + async fn is_confirmed(&self, txn: &Hash) -> Result { + self.client + .get_inclusion_states() + .transactions(from_ref(txn)) + .send() + .await + .map_err(Into::into) + .map(|states| states.states.as_slice() == [true]) + } +} diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs new file mode 100644 index 0000000000..be62e1a4f6 --- /dev/null +++ b/identity_iota/src/lib.rs @@ -0,0 +1,11 @@ +pub mod did; +pub mod error; +pub mod helpers; +pub mod io; +pub mod network; +pub mod resolver; +pub mod types; +pub mod utils; + +// Re-export the `identity_core` crate as `core` +pub use identity_core as core; diff --git a/identity_iota/src/network/mod.rs b/identity_iota/src/network/mod.rs new file mode 100644 index 0000000000..238572c611 --- /dev/null +++ b/identity_iota/src/network/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod network; +mod nodelist; + +pub use network::*; +pub use nodelist::*; diff --git a/identity_iota/src/network/network.rs b/identity_iota/src/network/network.rs new file mode 100644 index 0000000000..21571035a4 --- /dev/null +++ b/identity_iota/src/network/network.rs @@ -0,0 +1,45 @@ +use identity_core::did::DID; + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum Network { + Mainnet, + Devnet, + Comnet, +} + +impl Network { + pub fn matches_did(self, did: &DID) -> bool { + // TODO: Move this to a generic DID helper + let network: Option<&str> = did.id_segments.get(0).map(|string| string.as_str()); + + match (self, network) { + (Self::Devnet, Some(network)) => network == "dev", + (Self::Comnet, Some(network)) => network == "com", + (Self::Devnet, _) => false, + (Self::Comnet, _) => false, + (Self::Mainnet, _) => true, + } + } +} + +use iota::client::builder; + +impl From for Network { + fn from(other: builder::Network) -> Network { + match other { + builder::Network::Mainnet => Self::Mainnet, + builder::Network::Devnet => Self::Devnet, + builder::Network::Comnet => Self::Comnet, + } + } +} + +impl From for builder::Network { + fn from(other: Network) -> builder::Network { + match other { + Network::Mainnet => Self::Mainnet, + Network::Devnet => Self::Devnet, + Network::Comnet => Self::Comnet, + } + } +} diff --git a/identity_iota/src/network/nodelist.rs b/identity_iota/src/network/nodelist.rs new file mode 100644 index 0000000000..b4b41ef744 --- /dev/null +++ b/identity_iota/src/network/nodelist.rs @@ -0,0 +1,62 @@ +use core::iter::FromIterator as _; + +use crate::network::Network; + +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct NodeList { + network: Network, + nodes: Vec, +} + +impl NodeList { + pub const fn new() -> Self { + Self::with_network(Network::Devnet) + } + + pub const fn with_network(network: Network) -> Self { + Self { + network, + nodes: Vec::new(), + } + } + + pub fn with_nodes(nodes: impl IntoIterator>) -> Self { + Self { + network: Network::Devnet, + nodes: Vec::from_iter(nodes.into_iter().map(Into::into)), + } + } + + pub fn with_network_and_nodes(network: Network, nodes: impl IntoIterator>) -> Self { + Self { + network, + nodes: Vec::from_iter(nodes.into_iter().map(Into::into)), + } + } + + pub fn set_network(&mut self, network: Network) { + self.network = network; + } + + pub fn set_node(&mut self, node: impl Into) { + self.nodes.push(node.into()); + } + + pub fn nodes(&self) -> &[String] { + self.nodes.as_slice() + } + + pub fn network(&self) -> Network { + self.network + } +} + +impl From for NodeList +where + T: IntoIterator, + U: Into, +{ + fn from(other: T) -> Self { + Self::with_nodes(other) + } +} diff --git a/identity_iota/src/resolver.rs b/identity_iota/src/resolver.rs new file mode 100644 index 0000000000..ba723f45dd --- /dev/null +++ b/identity_iota/src/resolver.rs @@ -0,0 +1,66 @@ +use async_trait::async_trait; +use identity_core::{ + self as core, + did::DID, + resolver::{DocumentMetadata, InputMetadata, MetaDocument, ResolverMethod}, +}; + +use crate::{error::Result, io::TangleReader, network::NodeList, types::TangleDoc}; + +#[derive(Debug)] +pub struct TangleResolver { + nodes: NodeList, +} + +impl TangleResolver { + pub const fn new() -> Self { + Self { nodes: NodeList::new() } + } + + pub fn set_nodes(&mut self, nodes: impl Into) { + self.nodes = nodes.into(); + } + + pub fn nodes(&self) -> &NodeList { + &self.nodes + } + + pub fn nodes_mut(&mut self) -> &mut NodeList { + &mut self.nodes + } + + pub async fn document(&self, did: &DID) -> Result { + let reader: TangleReader = TangleReader::new(&self.nodes)?; + let document: TangleDoc = reader.fetch_latest(did).await.map(|(doc, _)| doc)?; + + let mut metadata: DocumentMetadata = DocumentMetadata::new(); + + metadata.created = document.data.created; + metadata.updated = document.data.updated; + + Ok(MetaDocument { + data: document.data, + meta: metadata, + }) + } +} + +#[async_trait] +impl ResolverMethod for TangleResolver { + fn is_supported(&self, did: &DID) -> bool { + // The DID method MUST be IOTA. + if did.method_name != "iota" { + return false; + } + + // The DID network MUST match the configured network. + self.nodes.network().matches_did(did) + } + + async fn read(&self, did: &DID, _input: InputMetadata) -> core::Result> { + self.document(did) + .await + .map_err(|error| core::Error::ResolutionError(error.into())) + .map(Some) + } +} diff --git a/identity_iota/src/types.rs b/identity_iota/src/types.rs new file mode 100644 index 0000000000..e3a9ae224b --- /dev/null +++ b/identity_iota/src/types.rs @@ -0,0 +1,16 @@ +use identity_core::did::DIDDocument; +use serde::{Deserialize, Serialize}; + +use crate::did::DIDDiff; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct TangleDoc { + pub hash: String, + pub data: DIDDocument, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub struct TangleDiff { + pub hash: String, + pub data: DIDDiff, +} diff --git a/identity_iota/src/utils.rs b/identity_iota/src/utils.rs new file mode 100644 index 0000000000..f1b7e32b93 --- /dev/null +++ b/identity_iota/src/utils.rs @@ -0,0 +1,51 @@ +use core::iter::once; +use iota::{ + crypto::ternary::{ + sponge::{CurlP81, Sponge as _}, + Hash, + }, + ternary::{raw::RawEncoding, Btrit, T1B1Buf, TritBuf, Trits, TryteBuf}, + transaction::bundled::{Address, BundledTransaction, BundledTransactionField as _}, +}; +use iota_conversion::trytes_converter; + +use crate::error::{Error, Result}; + +pub fn txn_hash(txn: &BundledTransaction) -> Hash { + let mut curl: CurlP81 = CurlP81::new(); + let mut tbuf: TritBuf = TritBuf::zeros(BundledTransaction::trit_len()); + + txn.into_trits_allocated(&mut tbuf); + + Hash::from_inner_unchecked(curl.digest(&tbuf).expect("infallible")) +} + +pub fn txn_hash_trytes(txn: &BundledTransaction) -> String { + encode_trits(txn_hash(txn).as_trits()) +} + +pub fn encode_trits(trits: &Trits) -> String +where + T: RawEncoding, +{ + trits.iter_trytes().map(char::from).collect() +} + +pub fn create_address_from_trits(trits: impl AsRef) -> Result
{ + let trits: TritBuf = TryteBuf::try_from_str(trits.as_ref())?.as_trits().encode(); + + Ok(Address::from_inner_unchecked(trits)) +} + +pub fn to_tryte(byte: u8) -> impl IntoIterator { + once(iota_constants::TRYTE_ALPHABET[(byte % 27) as usize]) + .chain(once(iota_constants::TRYTE_ALPHABET[(byte / 27) as usize])) +} + +pub fn utf8_to_trytes(input: impl AsRef<[u8]>) -> String { + input.as_ref().iter().copied().flat_map(to_tryte).collect() +} + +pub fn trytes_to_utf8(string: impl AsRef) -> Result { + trytes_converter::to_string(string.as_ref()).map_err(|_| Error::InvalidTryteConversion) +} diff --git a/identity_proof/Cargo.toml b/identity_proof/Cargo.toml index b4d01190b8..85cc323dbd 100644 --- a/identity_proof/Cargo.toml +++ b/identity_proof/Cargo.toml @@ -14,10 +14,8 @@ homepage = "https://www.iota.org" [dependencies] anyhow = "1.0" -base64 = "0.12" -bs58 = "0.3" canonical_json = { git = "https://github.com/l1h3r/canonicaljson-rs", branch = "force-map-order" } -identity_common = { path = "../identity_common" } +identity_core = { path = "../identity_core" } identity_crypto = { path = "../identity_crypto" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } diff --git a/identity_proof/src/canonicalize/impls/canonical_json.rs b/identity_proof/src/canonicalize/impls/canonical_json.rs index 0b1f6473ed..f33f62ff2e 100644 --- a/identity_proof/src/canonicalize/impls/canonical_json.rs +++ b/identity_proof/src/canonicalize/impls/canonical_json.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use canonical_json::to_string; -use identity_common::Object; +use identity_core::common::Object; use serde_json::to_value; use crate::{ diff --git a/identity_proof/src/canonicalize/impls/urdna_2015.rs b/identity_proof/src/canonicalize/impls/urdna_2015.rs index 0796108f5b..6bb2392482 100644 --- a/identity_proof/src/canonicalize/impls/urdna_2015.rs +++ b/identity_proof/src/canonicalize/impls/urdna_2015.rs @@ -1,4 +1,4 @@ -use identity_common::Object; +use identity_core::common::Object; use crate::{canonicalize::Canonicalize, error::Result}; diff --git a/identity_proof/src/canonicalize/impls/urgna_2012.rs b/identity_proof/src/canonicalize/impls/urgna_2012.rs index ec0c03fba6..ed85da65c2 100644 --- a/identity_proof/src/canonicalize/impls/urgna_2012.rs +++ b/identity_proof/src/canonicalize/impls/urgna_2012.rs @@ -1,4 +1,4 @@ -use identity_common::Object; +use identity_core::common::Object; use crate::{canonicalize::Canonicalize, error::Result}; diff --git a/identity_proof/src/canonicalize/traits.rs b/identity_proof/src/canonicalize/traits.rs index d379e98f7b..db8643d243 100644 --- a/identity_proof/src/canonicalize/traits.rs +++ b/identity_proof/src/canonicalize/traits.rs @@ -1,4 +1,4 @@ -use identity_common::Object; +use identity_core::common::Object; use crate::error::Result; diff --git a/identity_proof/src/document.rs b/identity_proof/src/document.rs index de1b390e7c..cd6b9ed2af 100644 --- a/identity_proof/src/document.rs +++ b/identity_proof/src/document.rs @@ -1,4 +1,4 @@ -use identity_common::{Object, SerdeInto}; +use identity_core::common::{Object, SerdeInto}; use serde::Serialize; use crate::error::Result; diff --git a/identity_proof/src/error.rs b/identity_proof/src/error.rs index dc80861526..9a974917fa 100644 --- a/identity_proof/src/error.rs +++ b/identity_proof/src/error.rs @@ -1,9 +1,5 @@ #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Failed to decode base58 data: {0}")] - DecodeBase58(#[from] bs58::decode::Error), - #[error("Failed to decode base64 data: {0}")] - DecodeBase64(#[from] base64::DecodeError), #[error("Failed to canonicalize object: {0}")] Canonicalize(anyhow::Error), #[error("Failed to pre-process document: {0}")] @@ -11,7 +7,7 @@ pub enum Error { #[error("Invalid LD Signature: {0}")] InvalidLDSignature(String), #[error(transparent)] - Common(#[from] identity_common::Error), + Common(#[from] identity_core::Error), #[error(transparent)] Crypto(#[from] identity_crypto::Error), #[error(transparent)] diff --git a/identity_proof/src/lib.rs b/identity_proof/src/lib.rs index ec03e1a163..a36ef061e1 100644 --- a/identity_proof/src/lib.rs +++ b/identity_proof/src/lib.rs @@ -2,10 +2,8 @@ pub mod canonicalize; pub mod document; pub mod error; pub mod signature; -pub mod utils; pub use canonicalize::{CanonicalJson, Canonicalize, Urdna2015, Urgna2012}; pub use document::LinkedDataDocument; pub use error::{Error, Result}; pub use signature::{LinkedDataSignature, SignatureOptions, SignatureProof, SignatureSuite}; -pub use utils::{decode_b64, encode_b64}; diff --git a/identity_proof/src/signature/linked_data_signature.rs b/identity_proof/src/signature/linked_data_signature.rs index 44798bb55e..2f50b5f6c4 100644 --- a/identity_proof/src/signature/linked_data_signature.rs +++ b/identity_proof/src/signature/linked_data_signature.rs @@ -1,4 +1,4 @@ -use identity_common::Timestamp; +use identity_core::common::Timestamp; use serde::{Deserialize, Serialize}; use crate::signature::{SignatureData, SignatureOptions}; diff --git a/identity_proof/src/signature/signature_data.rs b/identity_proof/src/signature/signature_data.rs index 948a5b3f50..29a884dcbf 100644 --- a/identity_proof/src/signature/signature_data.rs +++ b/identity_proof/src/signature/signature_data.rs @@ -1,4 +1,4 @@ -use identity_common::{Object, Value}; +use identity_core::common::{Object, Value}; use serde::{de::Error as _, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; diff --git a/identity_proof/src/signature/signature_options.rs b/identity_proof/src/signature/signature_options.rs index 8a6cb49036..fef38431ae 100644 --- a/identity_proof/src/signature/signature_options.rs +++ b/identity_proof/src/signature/signature_options.rs @@ -1,4 +1,4 @@ -use identity_common::{Object, Timestamp}; +use identity_core::common::{Object, Timestamp}; use serde::{Deserialize, Serialize}; /// Options permitted to create/customize a linked data signature diff --git a/identity_proof/src/signature/signature_proof.rs b/identity_proof/src/signature/signature_proof.rs index 51d3ce63ac..8d3e4a1205 100644 --- a/identity_proof/src/signature/signature_proof.rs +++ b/identity_proof/src/signature/signature_proof.rs @@ -1,4 +1,4 @@ -use identity_common::{Object, SerdeInto}; +use identity_core::common::{Object, SerdeInto}; use identity_crypto::{self as crypto, Error, Proof, PublicKey, SecretKey}; use std::marker::PhantomData; @@ -71,13 +71,16 @@ where fn create(&self, document: &Self::Document, secret: &SecretKey) -> crypto::Result { self.create_proof(document, secret) .and_then(|proof| Ok(proof.serde_into()?)) - .map_err(Error::create_proof) + .map_err(|error| Error::CreateProof(error.into())) } fn verify(&self, document: &Self::Document, proof: &Self::Output, public: &PublicKey) -> crypto::Result { proof .serde_into() - .map_err(Error::create_proof) - .and_then(|proof| self.verify_proof(document, &proof, public).map_err(Error::create_proof)) + .map_err(|error| Error::CreateProof(error.into())) + .and_then(|proof| { + self.verify_proof(document, &proof, public) + .map_err(|error| Error::CreateProof(error.into())) + }) } } diff --git a/identity_proof/src/signature/signature_suite.rs b/identity_proof/src/signature/signature_suite.rs index b81142b440..e14a8c7049 100644 --- a/identity_proof/src/signature/signature_suite.rs +++ b/identity_proof/src/signature/signature_suite.rs @@ -1,5 +1,8 @@ use anyhow::anyhow; -use identity_common::{Object, Timestamp}; +use identity_core::{ + common::{Object, Timestamp}, + utils::{decode_b64, encode_b64}, +}; use identity_crypto::{KeyGen, PublicKey, SecretKey, Sign, Verify}; use crate::{ @@ -7,7 +10,6 @@ use crate::{ document::LinkedDataDocument, error::{Error, Result}, signature::{LinkedDataSignature, SignatureData, SignatureOptions, SignatureValue}, - utils::{decode_b64, encode_b64}, }; const DEFAULT_PURPOSE: &str = "assertionMethod"; @@ -44,7 +46,7 @@ pub trait SignatureSuite: KeyGen + Sign + Verify { /// Decodes a `String`-encoded signature. fn decode_signature(&self, signature: &LinkedDataSignature) -> Result> { - decode_b64(signature.proof()) + decode_b64(signature.proof()).map_err(Into::into) } /// Creates a `SignatureValue` from a raw String. diff --git a/identity_proof/src/utils/mod.rs b/identity_proof/src/utils/mod.rs deleted file mode 100644 index 5fd1448f59..0000000000 --- a/identity_proof/src/utils/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod encoding; - -pub use self::encoding::*; diff --git a/identity_proof/tests/canonicalize.rs b/identity_proof/tests/canonicalize.rs index 78eb2fdf39..a39239ab57 100644 --- a/identity_proof/tests/canonicalize.rs +++ b/identity_proof/tests/canonicalize.rs @@ -1,4 +1,4 @@ -use identity_common::SerdeInto; +use identity_core::common::SerdeInto; use identity_proof::{CanonicalJson, Canonicalize}; use serde_json::json; diff --git a/identity_proof/tests/linked_data_signature.rs b/identity_proof/tests/linked_data_signature.rs index 73e0933fbd..f7f51ccd54 100644 --- a/identity_proof/tests/linked_data_signature.rs +++ b/identity_proof/tests/linked_data_signature.rs @@ -1,4 +1,4 @@ -use identity_common::Object; +use identity_core::common::Object; use identity_proof::LinkedDataSignature; use serde_json::{from_str, to_string}; diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml deleted file mode 100644 index 3adb5ad1de..0000000000 --- a/identity_resolver/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "identity_resolver" -version = "0.1.0" -authors = ["IOTA Identity"] -edition = "2018" -description = "Did Resolution Library" -readme = "../README.md" -repository = "https://github.com/iotaledger/identity.rs" -license = "Apache-2.0" -keywords = ["iota", "tangle", "identity"] -homepage = "https://www.iota.org" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs deleted file mode 100644 index 31e1bb209f..0000000000 --- a/identity_resolver/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/identity_schema/Cargo.toml b/identity_schema/Cargo.toml deleted file mode 100644 index 66471dabbe..0000000000 --- a/identity_schema/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "identity_schema" -version = "0.1.0" -authors = ["IOTA Identity"] -edition = "2018" -description = "A library for VC schema validation" -readme = "../README.md" -repository = "https://github.com/iotaledger/identity.rs" -license = "Apache-2.0" -keywords = ["iota", "tangle", "identity"] -homepage = "https://www.iota.org" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/identity_schema/src/lib.rs b/identity_schema/src/lib.rs deleted file mode 100644 index 31e1bb209f..0000000000 --- a/identity_schema/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/identity_vc/Cargo.toml b/identity_vc/Cargo.toml deleted file mode 100644 index 0a583aba12..0000000000 --- a/identity_vc/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "identity_vc" -version = "0.1.0" -authors = ["IOTA Identity"] -edition = "2018" -description = "An implementation of the Verifiable Credentials (VC) standard" -readme = "../README.md" -repository = "https://github.com/iotaledger/identity.rs" -license = "Apache-2.0" -keywords = ["iota", "tangle", "identity"] -homepage = "https://www.iota.org" - -[[bin]] -name = "vc_test_suite" -path = "bin/vc_test_suite.rs" - -[dependencies] -# error handling -thiserror = "1.0" -anyhow = "1.0" - -# serialization -serde = {version = "1.0", features = ["derive"]} -serde_json = { version = "1.0", features = ["preserve_order"] } -serde_cbor = "0.11" - -# identity -identity_common = { path = "../identity_common" } -identity_core = { path = "../identity_core" } -# identity_core = { git = "https://github.com/iotaledger/identity.rs", branch = "dev" } diff --git a/identity_vc/bin/vc_test_suite.rs b/identity_vc/bin/vc_test_suite.rs deleted file mode 100644 index 0695eb32c0..0000000000 --- a/identity_vc/bin/vc_test_suite.rs +++ /dev/null @@ -1,34 +0,0 @@ -use anyhow::Result; -use identity_vc::prelude::*; -use serde_json::{from_reader, to_string}; -use std::{env::args, fs::File, path::Path}; - -fn main() -> Result<()> { - let args: Vec = args().collect(); - - match args[1].as_str() { - "test-credential" => { - let path: &Path = Path::new(&args[2]); - let file: File = File::open(path)?; - let data: VerifiableCredential = from_reader(file)?; - - data.validate()?; - - println!("{}", to_string(&data)?); - } - "test-presentation" => { - let path: &Path = Path::new(&args[2]); - let file: File = File::open(path)?; - let data: VerifiablePresentation = from_reader(file)?; - - data.validate()?; - - println!("{}", to_string(&data)?); - } - test => { - panic!("Unknown Test: {:?}", test); - } - } - - Ok(()) -} diff --git a/identity_vc/src/common/credential/credential_schema.rs b/identity_vc/src/common/credential/credential_schema.rs deleted file mode 100644 index 85df6153b5..0000000000 --- a/identity_vc/src/common/credential/credential_schema.rs +++ /dev/null @@ -1,34 +0,0 @@ -use identity_common::{Object, Uri}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -use crate::{ - common::{try_take_object_id, try_take_object_type}, - error::Error, -}; - -/// Information used to validate the structure of a `Credential`. -/// -/// Ref: https://www.w3.org/TR/vc-data-model/#data-schemas -#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] -pub struct CredentialSchema { - pub id: Uri, - #[serde(rename = "type")] - pub type_: String, - #[serde(flatten)] - pub properties: Object, -} - -impl TryFrom for CredentialSchema { - type Error = Error; - - fn try_from(mut other: Object) -> Result { - let mut this: Self = Default::default(); - - this.id = try_take_object_id("CredentialSchema", &mut other)?.into(); - this.type_ = try_take_object_type("CredentialSchema", &mut other)?; - this.properties = other; - - Ok(this) - } -} diff --git a/identity_vc/src/common/credential/credential_status.rs b/identity_vc/src/common/credential/credential_status.rs deleted file mode 100644 index 7c82b61e84..0000000000 --- a/identity_vc/src/common/credential/credential_status.rs +++ /dev/null @@ -1,34 +0,0 @@ -use identity_common::{Object, OneOrMany, Uri}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -use crate::{ - common::{try_take_object_id, try_take_object_types}, - error::Error, -}; - -/// Information used to determine the current status of a `Credential`. -/// -/// Ref: https://www.w3.org/TR/vc-data-model/#status -#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] -pub struct CredentialStatus { - pub id: Uri, - #[serde(rename = "type")] - pub types: OneOrMany, - #[serde(flatten)] - pub properties: Object, -} - -impl TryFrom for CredentialStatus { - type Error = Error; - - fn try_from(mut other: Object) -> Result { - let mut this: Self = Default::default(); - - this.id = try_take_object_id("CredentialStatus", &mut other)?.into(); - this.types = try_take_object_types("CredentialStatus", &mut other)?; - this.properties = other; - - Ok(this) - } -} diff --git a/identity_vc/src/common/credential/evidence.rs b/identity_vc/src/common/credential/evidence.rs deleted file mode 100644 index 5acab3c44c..0000000000 --- a/identity_vc/src/common/credential/evidence.rs +++ /dev/null @@ -1,35 +0,0 @@ -use identity_common::{Object, OneOrMany}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -use crate::{ - common::{take_object_id, try_take_object_types}, - error::Error, -}; - -/// Information used to increase confidence in the claims of a `Credential` -/// -/// Ref: https://www.w3.org/TR/vc-data-model/#evidence -#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] -pub struct Evidence { - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - #[serde(rename = "type")] - pub types: OneOrMany, - #[serde(flatten)] - pub properties: Object, -} - -impl TryFrom for Evidence { - type Error = Error; - - fn try_from(mut other: Object) -> Result { - let mut this: Self = Default::default(); - - this.id = take_object_id(&mut other); - this.types = try_take_object_types("Evidence", &mut other)?; - this.properties = other; - - Ok(this) - } -} diff --git a/identity_vc/src/common/credential/mod.rs b/identity_vc/src/common/credential/mod.rs deleted file mode 100644 index eb6ff2336e..0000000000 --- a/identity_vc/src/common/credential/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod credential_schema; -mod credential_status; -mod credential_subject; -mod evidence; -mod refresh_service; -mod terms_of_use; -mod utils; - -pub use self::{ - credential_schema::*, credential_status::*, credential_subject::*, evidence::*, refresh_service::*, - terms_of_use::*, utils::*, -}; diff --git a/identity_vc/src/common/credential/refresh_service.rs b/identity_vc/src/common/credential/refresh_service.rs deleted file mode 100644 index 27c3a4200e..0000000000 --- a/identity_vc/src/common/credential/refresh_service.rs +++ /dev/null @@ -1,34 +0,0 @@ -use identity_common::{Object, OneOrMany, Uri}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -use crate::{ - common::{try_take_object_id, try_take_object_types}, - error::Error, -}; - -/// Information used to refresh or assert the status of a `Credential`. -/// -/// Ref: https://www.w3.org/TR/vc-data-model/#refreshing -#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] -pub struct RefreshService { - pub id: Uri, - #[serde(rename = "type")] - pub types: OneOrMany, - #[serde(flatten)] - pub properties: Object, -} - -impl TryFrom for RefreshService { - type Error = Error; - - fn try_from(mut other: Object) -> Result { - let mut this: Self = Default::default(); - - this.id = try_take_object_id("RefreshService", &mut other)?.into(); - this.types = try_take_object_types("RefreshService", &mut other)?; - this.properties = other; - - Ok(this) - } -} diff --git a/identity_vc/src/common/credential/terms_of_use.rs b/identity_vc/src/common/credential/terms_of_use.rs deleted file mode 100644 index 55b6661b7b..0000000000 --- a/identity_vc/src/common/credential/terms_of_use.rs +++ /dev/null @@ -1,36 +0,0 @@ -use identity_common::{Object, OneOrMany, Uri}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -use crate::{ - common::{take_object_id, try_take_object_types}, - error::Error, -}; - -/// Information used to express obligations, prohibitions, and permissions about -/// a `Credential` or `Presentation`. -/// -/// Ref: https://www.w3.org/TR/vc-data-model/#terms-of-use -#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] -pub struct TermsOfUse { - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - #[serde(rename = "type")] - pub types: OneOrMany, - #[serde(flatten)] - pub properties: Object, -} - -impl TryFrom for TermsOfUse { - type Error = Error; - - fn try_from(mut other: Object) -> Result { - let mut this: Self = Default::default(); - - this.id = take_object_id(&mut other).map(Into::into); - this.types = try_take_object_types("TermsOfUse", &mut other)?; - this.properties = other; - - Ok(this) - } -} diff --git a/identity_vc/src/common/credential/utils.rs b/identity_vc/src/common/credential/utils.rs deleted file mode 100644 index 7894cb3968..0000000000 --- a/identity_vc/src/common/credential/utils.rs +++ /dev/null @@ -1,49 +0,0 @@ -use identity_common::{Object, OneOrMany, Value}; - -use crate::error::{Error, Result}; - -pub fn take_object_id(object: &mut Object) -> Option { - match object.remove("id") { - Some(Value::String(id)) => Some(id), - Some(_) | None => None, - } -} - -pub fn try_take_object_id(name: &'static str, object: &mut Object) -> Result { - take_object_id(object).ok_or_else(|| Error::BadObjectConversion(name)) -} - -pub fn take_object_type(object: &mut Object) -> Option { - match object.remove("type") { - Some(Value::String(value)) => Some(value), - Some(_) | None => None, - } -} - -pub fn try_take_object_type(name: &'static str, object: &mut Object) -> Result { - take_object_type(object).ok_or_else(|| Error::BadObjectConversion(name)) -} - -pub fn take_object_types(object: &mut Object) -> Option> { - match object.remove("type") { - Some(Value::String(value)) => Some(value.into()), - Some(Value::Array(values)) => Some(collect_types(values)), - Some(_) | None => None, - } -} - -pub fn try_take_object_types(name: &'static str, object: &mut Object) -> Result> { - take_object_types(object).ok_or_else(|| Error::BadObjectConversion(name)) -} - -fn collect_types(values: Vec) -> OneOrMany { - let mut types: Vec = Vec::with_capacity(values.len()); - - for value in values { - if let Value::String(value) = value { - types.push(value); - } - } - - types.into() -} diff --git a/identity_vc/src/common/mod.rs b/identity_vc/src/common/mod.rs deleted file mode 100644 index b3fdb1c6b9..0000000000 --- a/identity_vc/src/common/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod context; -mod credential; -mod issuer; - -pub use self::{context::*, credential::*, issuer::*}; diff --git a/identity_vc/src/credential.rs b/identity_vc/src/credential.rs deleted file mode 100644 index e378989e77..0000000000 --- a/identity_vc/src/credential.rs +++ /dev/null @@ -1,227 +0,0 @@ -use identity_common::{impl_builder_setter, impl_builder_try_setter, Object, OneOrMany, Timestamp, Uri, Value}; -use serde::{Deserialize, Serialize}; - -use crate::{ - common::{ - Context, CredentialSchema, CredentialStatus, CredentialSubject, Evidence, Issuer, RefreshService, TermsOfUse, - }, - error::{Error, Result}, - utils::validate_credential_structure, - verifiable::VerifiableCredential, -}; - -/// A `Credential` represents a set of claims describing an entity. -/// -/// `Credential`s can be combined with `Proof`s to create `VerifiableCredential`s. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct Credential { - /// A set of URIs or `Object`s describing the applicable JSON-LD contexts. - /// - /// NOTE: The first URI MUST be `https://www.w3.org/2018/credentials/v1` - #[serde(rename = "@context")] - pub context: OneOrMany, - /// A unique `URI` referencing the subject of the credential. - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - /// One or more URIs defining the type of credential. - /// - /// NOTE: The VC spec defines this as a set of URIs BUT they are commonly - /// passed as non-`URI` strings and expected to be processed with JSON-LD. - /// We're using a `String` here since we don't currently use JSON-LD and - /// don't have any immediate plans to do so. - #[serde(rename = "type")] - pub types: OneOrMany, - /// One or more `Object`s representing the `Credential` subject(s). - #[serde(rename = "credentialSubject")] - pub credential_subject: OneOrMany, - /// A reference to the issuer of the `Credential`. - pub issuer: Issuer, - /// The date and time the `Credential` becomes valid. - #[serde(rename = "issuanceDate")] - pub issuance_date: Timestamp, - /// The date and time the `Credential` is no longer considered valid. - #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")] - pub expiration_date: Option, - /// TODO - #[serde(rename = "credentialStatus", skip_serializing_if = "Option::is_none")] - pub credential_status: Option>, - /// TODO - #[serde(rename = "credentialSchema", skip_serializing_if = "Option::is_none")] - pub credential_schema: Option>, - /// TODO - #[serde(rename = "refreshService", skip_serializing_if = "Option::is_none")] - pub refresh_service: Option>, - /// The terms of use issued by the `Credential` issuer - #[serde(rename = "termsOfUse", skip_serializing_if = "Option::is_none")] - pub terms_of_use: Option>, - /// TODO - #[serde(skip_serializing_if = "Option::is_none")] - pub evidence: Option>, - /// Indicates that the `Credential` must only be contained within a - /// `Presentation` with a proof issued from the `Credential` subject. - #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")] - pub non_transferable: Option, - /// Miscellaneous properties. - #[serde(flatten)] - pub properties: Object, -} - -impl Credential { - pub const BASE_CONTEXT: &'static str = "https://www.w3.org/2018/credentials/v1"; - - pub const BASE_TYPE: &'static str = "VerifiableCredential"; - - pub fn validate(&self) -> Result<()> { - validate_credential_structure(self) - } -} - -// ============================================================================= -// Credential Builder -// ============================================================================= - -/// A convenience for constructing a `Credential` or `VerifiableCredential` -/// from dynamic data. -/// -/// NOTE: Base context and type are automatically included. -#[derive(Debug)] -pub struct CredentialBuilder { - context: Vec, - id: Option, - types: Vec, - credential_subject: Vec, - issuer: Option, - issuance_date: Timestamp, - expiration_date: Option, - credential_status: Vec, - credential_schema: Vec, - refresh_service: Vec, - terms_of_use: Vec, - evidence: Vec, - non_transferable: Option, - properties: Object, -} - -impl CredentialBuilder { - pub fn new() -> Self { - Self { - context: vec![Credential::BASE_CONTEXT.into()], - id: None, - types: vec![Credential::BASE_TYPE.into()], - credential_subject: Vec::new(), - issuer: None, - issuance_date: Default::default(), - expiration_date: None, - credential_status: Vec::new(), - credential_schema: Vec::new(), - refresh_service: Vec::new(), - terms_of_use: Vec::new(), - evidence: Vec::new(), - non_transferable: None, - properties: Default::default(), - } - } - - pub fn context(mut self, value: impl Into) -> Self { - let value: Context = value.into(); - - if !matches!(value, Context::Uri(ref uri) if uri == Credential::BASE_CONTEXT) { - self.context.push(value); - } - - self - } - - pub fn type_(mut self, value: impl Into) -> Self { - let value: String = value.into(); - - if value != Credential::BASE_TYPE { - self.types.push(value); - } - - self - } - - pub fn property(mut self, key: impl Into, value: impl Into) -> Self { - self.properties.insert(key.into(), value.into()); - self - } - - impl_builder_setter!(id, id, Option); - impl_builder_setter!(subject, credential_subject, Vec); - impl_builder_setter!(issuer, issuer, Option); - impl_builder_setter!(issuance_date, issuance_date, Timestamp); - impl_builder_setter!(expiration_date, expiration_date, Option); - impl_builder_setter!(status, credential_status, Vec); - impl_builder_setter!(schema, credential_schema, Vec); - impl_builder_setter!(refresh, refresh_service, Vec); - impl_builder_setter!(terms_of_use, terms_of_use, Vec); - impl_builder_setter!(evidence, evidence, Vec); - impl_builder_setter!(non_transferable, non_transferable, Option); - impl_builder_setter!(properties, properties, Object); - - impl_builder_try_setter!(try_subject, credential_subject, Vec); - impl_builder_try_setter!(try_issuance_date, issuance_date, Timestamp); - impl_builder_try_setter!(try_expiration_date, expiration_date, Option); - impl_builder_try_setter!(try_status, credential_status, Vec); - impl_builder_try_setter!(try_schema, credential_schema, Vec); - impl_builder_try_setter!(try_refresh_service, refresh_service, Vec); - impl_builder_try_setter!(try_terms_of_use, terms_of_use, Vec); - impl_builder_try_setter!(try_evidence, evidence, Vec); - - /// Consumes the `CredentialBuilder`, returning a valid `Credential` - pub fn build(self) -> Result { - let mut credential: Credential = Credential { - context: self.context.into(), - id: self.id, - types: self.types.into(), - credential_subject: self.credential_subject.into(), - issuer: self.issuer.ok_or_else(|| Error::MissingCredentialIssuer)?, - issuance_date: self.issuance_date, - expiration_date: self.expiration_date, - credential_status: None, - credential_schema: None, - refresh_service: None, - terms_of_use: None, - evidence: None, - non_transferable: self.non_transferable, - properties: self.properties, - }; - - if !self.credential_status.is_empty() { - credential.credential_status = Some(self.credential_status.into()); - } - - if !self.credential_schema.is_empty() { - credential.credential_schema = Some(self.credential_schema.into()); - } - - if !self.refresh_service.is_empty() { - credential.refresh_service = Some(self.refresh_service.into()); - } - - if !self.terms_of_use.is_empty() { - credential.terms_of_use = Some(self.terms_of_use.into()); - } - - if !self.evidence.is_empty() { - credential.evidence = Some(self.evidence.into()); - } - - credential.validate()?; - - Ok(credential) - } - - /// Consumes the `CredentialBuilder`, returning a valid `VerifiableCredential` - pub fn build_verifiable(self, proof: impl Into>) -> Result { - self.build() - .map(|credential| VerifiableCredential::new(credential, proof)) - } -} - -impl Default for CredentialBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/identity_vc/src/error.rs b/identity_vc/src/error.rs deleted file mode 100644 index b64c6f7b83..0000000000 --- a/identity_vc/src/error.rs +++ /dev/null @@ -1,27 +0,0 @@ -use identity_common::Error as CommonError; -use std::result::Result as StdResult; -use thiserror::Error as ThisError; - -#[derive(Debug, ThisError)] -pub enum Error { - #[error("Cannot convert `Object` to `{0}`")] - BadObjectConversion(&'static str), - #[error("Missing base type for {0}")] - MissingBaseType(&'static str), - #[error("Missing base context for {0}")] - MissingBaseContext(&'static str), - #[error("Invalid base context for {0}")] - InvalidBaseContext(&'static str), - #[error("Invalid URI for {0}")] - InvalidURI(&'static str), - #[error("Missing `Credential` subject")] - MissingCredentialSubject, - #[error("Invalid `Credential` subject")] - InvalidCredentialSubject, - #[error("Missing `Credential` issuer")] - MissingCredentialIssuer, - #[error(transparent)] - CommonError(#[from] CommonError), -} - -pub type Result = StdResult; diff --git a/identity_vc/src/lib.rs b/identity_vc/src/lib.rs deleted file mode 100644 index 42bfb3d477..0000000000 --- a/identity_vc/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod common; -pub mod credential; -pub mod error; -pub mod presentation; -pub mod utils; -pub mod verifiable; - -pub const RESERVED_PROPERTIES: &[&str] = &["issued", "validFrom", "validUntil"]; - -pub mod prelude { - pub use crate::{ - common::{ - Context, CredentialSchema, CredentialStatus, CredentialSubject, Evidence, Issuer, RefreshService, - TermsOfUse, - }, - credential::{Credential, CredentialBuilder}, - error::{Error, Result}, - presentation::{Presentation, PresentationBuilder}, - verifiable::{VerifiableCredential, VerifiablePresentation}, - }; -} diff --git a/identity_vc/src/presentation.rs b/identity_vc/src/presentation.rs deleted file mode 100644 index 5ec71adf0c..0000000000 --- a/identity_vc/src/presentation.rs +++ /dev/null @@ -1,164 +0,0 @@ -use identity_common::{impl_builder_setter, impl_builder_try_setter, Object, OneOrMany, Uri, Value}; -use serde::{Deserialize, Serialize}; - -use crate::{ - common::{Context, RefreshService, TermsOfUse}, - credential::Credential, - error::Result, - utils::validate_presentation_structure, - verifiable::{VerifiableCredential, VerifiablePresentation}, -}; - -/// A `Presentation` represents a bundle of one or more `VerifiableCredential`s. -/// -/// `Presentation`s can be combined with `Proof`s to create `VerifiablePresentation`s. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct Presentation { - /// A set of URIs or `Object`s describing the applicable JSON-LD contexts. - /// - /// NOTE: The first URI MUST be `https://www.w3.org/2018/credentials/v1` - #[serde(rename = "@context")] - pub context: OneOrMany, - /// A unique `URI` referencing the subject of the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - /// One or more URIs defining the type of presentation. - /// - /// NOTE: The VC spec defines this as a set of URIs BUT they are commonly - /// passed as non-`URI` strings and expected to be processed with JSON-LD. - /// We're using a `String` here since we don't currently use JSON-LD and - /// don't have any immediate plans to do so. - #[serde(rename = "type")] - pub types: OneOrMany, - /// TODO - #[serde(rename = "verifiableCredential")] - pub verifiable_credential: OneOrMany, - /// The entity that generated the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - pub holder: Option, - /// TODO - #[serde(rename = "refreshService", skip_serializing_if = "Option::is_none")] - pub refresh_service: Option>, - /// The terms of use issued by the presentation holder - #[serde(rename = "termsOfUse", skip_serializing_if = "Option::is_none")] - pub terms_of_use: Option>, - /// Miscellaneous properties. - #[serde(flatten)] - pub properties: Object, -} - -impl Presentation { - pub const BASE_TYPE: &'static str = "VerifiablePresentation"; - - pub fn validate(&self) -> Result<()> { - validate_presentation_structure(self) - } -} - -// ============================================================================= -// Presentation Builder -// ============================================================================= - -/// A convenience for constructing a `Presentation` or `VerifiablePresentation` -/// from dynamic data. -/// -/// NOTE: Base context and type are automatically included. -#[derive(Debug)] -pub struct PresentationBuilder { - context: Vec, - id: Option, - types: Vec, - verifiable_credential: Vec, - holder: Option, - refresh_service: Vec, - terms_of_use: Vec, - properties: Object, -} - -impl PresentationBuilder { - pub fn new() -> Self { - Self { - context: vec![Credential::BASE_CONTEXT.into()], - id: None, - types: vec![Presentation::BASE_TYPE.into()], - verifiable_credential: Vec::new(), - holder: None, - refresh_service: Vec::new(), - terms_of_use: Vec::new(), - properties: Default::default(), - } - } - - pub fn context(mut self, value: impl Into) -> Self { - let value: Context = value.into(); - - if !matches!(value, Context::Uri(ref uri) if uri == Credential::BASE_CONTEXT) { - self.context.push(value); - } - - self - } - - pub fn type_(mut self, value: impl Into) -> Self { - let value: String = value.into(); - - if value != Presentation::BASE_TYPE { - self.types.push(value); - } - - self - } - - pub fn property(mut self, key: impl Into, value: impl Into) -> Self { - self.properties.insert(key.into(), value.into()); - self - } - - impl_builder_setter!(id, id, Option); - impl_builder_setter!(credential, verifiable_credential, Vec); - impl_builder_setter!(holder, holder, Option); - impl_builder_setter!(refresh, refresh_service, Vec); - impl_builder_setter!(terms_of_use, terms_of_use, Vec); - impl_builder_setter!(properties, properties, Object); - - impl_builder_try_setter!(try_refresh_service, refresh_service, Vec); - impl_builder_try_setter!(try_terms_of_use, terms_of_use, Vec); - - /// Consumes the `PresentationBuilder`, returning a valid `Presentation` - pub fn build(self) -> Result { - let mut presentation: Presentation = Presentation { - context: self.context.into(), - id: self.id, - types: self.types.into(), - verifiable_credential: self.verifiable_credential.into(), - holder: self.holder, - refresh_service: None, - terms_of_use: None, - properties: self.properties, - }; - - if !self.refresh_service.is_empty() { - presentation.refresh_service = Some(self.refresh_service.into()); - } - - if !self.terms_of_use.is_empty() { - presentation.terms_of_use = Some(self.terms_of_use.into()); - } - - presentation.validate()?; - - Ok(presentation) - } - - /// Consumes the `PresentationBuilder`, returning a valid `VerifiablePresentation` - pub fn build_verifiable(self, proof: impl Into>) -> Result { - self.build() - .map(|credential| VerifiablePresentation::new(credential, proof)) - } -} - -impl Default for PresentationBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/identity_vc/src/utils/mod.rs b/identity_vc/src/utils/mod.rs deleted file mode 100644 index f630594d03..0000000000 --- a/identity_vc/src/utils/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod validation; - -pub use self::validation::*; diff --git a/identity_vc/src/utils/validation.rs b/identity_vc/src/utils/validation.rs deleted file mode 100644 index e1bd73adb1..0000000000 --- a/identity_vc/src/utils/validation.rs +++ /dev/null @@ -1,92 +0,0 @@ -use identity_common::{OneOrMany, Uri}; - -use crate::{ - common::Context, - credential::Credential, - error::{Error, Result}, - presentation::Presentation, -}; - -pub fn validate_credential_structure(credential: &Credential) -> Result<()> { - // Ensure the base context is present and in the correct location - validate_context("Credential", &credential.context)?; - - // The set of types MUST contain the base type - validate_types("Credential", Credential::BASE_TYPE, &credential.types)?; - - // Ensure the id URI (if provided) adheres to the correct format - validate_opt_uri("Credential id", credential.id.as_ref())?; - - // Ensure the issuer URI adheres to the correct format - validate_uri("Credential issuer", credential.issuer.uri())?; - - // Credentials MUST have at least one subject - if credential.credential_subject.is_empty() { - return Err(Error::MissingCredentialSubject); - } - - // Each subject is defined as one or more properties - no empty objects - for subject in credential.credential_subject.iter() { - if subject.id.is_none() && subject.properties.is_empty() { - return Err(Error::InvalidCredentialSubject); - } - } - - Ok(()) -} - -pub fn validate_presentation_structure(presentation: &Presentation) -> Result<()> { - // Ensure the base context is present and in the correct location - validate_context("Presentation", &presentation.context)?; - - // The set of types MUST contain the base type - validate_types("Presentation", Presentation::BASE_TYPE, &presentation.types)?; - - // Ensure the id URI (if provided) adheres to the correct format - validate_opt_uri("Presentation id", presentation.id.as_ref())?; - - // Ensure the holder URI (if provided) adheres to the correct format - validate_opt_uri("Presentation holder", presentation.holder.as_ref())?; - - // Validate all verifiable credentials - for credential in presentation.verifiable_credential.iter() { - credential.validate()?; - } - - Ok(()) -} - -pub fn validate_types(name: &'static str, base: &str, types: &OneOrMany) -> Result<()> { - if !types.contains(&base.into()) { - return Err(Error::MissingBaseType(name)); - } - - Ok(()) -} - -pub fn validate_context(name: &'static str, context: &OneOrMany) -> Result<()> { - // The first Credential/Presentation context MUST be a URI representing the base context - match context.get(0) { - Some(Context::Uri(uri)) if uri == Credential::BASE_CONTEXT => Ok(()), - Some(_) => Err(Error::InvalidBaseContext(name)), - None => Err(Error::MissingBaseContext(name)), - } -} - -pub fn validate_uri(name: &'static str, uri: &Uri) -> Result<()> { - const KNOWN: [&str; 4] = ["did:", "urn:", "http:", "https:"]; - - // TODO: Proper URI validation - if !KNOWN.iter().any(|scheme| uri.starts_with(scheme)) { - return Err(Error::InvalidURI(name)); - } - - Ok(()) -} - -pub fn validate_opt_uri(name: &'static str, uri: Option<&Uri>) -> Result<()> { - match uri { - Some(uri) => validate_uri(name, uri), - None => Ok(()), - } -} diff --git a/identity_vc/src/verifiable/mod.rs b/identity_vc/src/verifiable/mod.rs deleted file mode 100644 index 6368af51f9..0000000000 --- a/identity_vc/src/verifiable/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod credential; -mod presentation; - -pub use self::{credential::*, presentation::*}; diff --git a/identity_vc/tests/credential.rs b/identity_vc/tests/credential.rs deleted file mode 100644 index 07e24e9b53..0000000000 --- a/identity_vc/tests/credential.rs +++ /dev/null @@ -1,89 +0,0 @@ -#[macro_use] -mod macros; - -use identity_common::object; -use identity_vc::prelude::*; - -#[test] -fn test_builder_valid() { - let issuance = timestamp!("2010-01-01T00:00:00Z"); - - let credential = CredentialBuilder::new() - .issuer("did:example:issuer") - .context("https://www.w3.org/2018/credentials/examples/v1") - .context(object!(id: "did:context:1234", type: "CustomContext2020")) - .id("did:example:123") - .type_("RelationshipCredential") - .try_subject(object!(id: "did:iota:alice", spouse: "did:iota:bob")) - .unwrap() - .try_subject(object!(id: "did:iota:bob", spouse: "did:iota:alice")) - .unwrap() - .issuance_date(issuance) - .build() - .unwrap(); - - assert_eq!(credential.context.len(), 3); - assert_matches!(credential.context.get(0).unwrap(), Context::Uri(ref uri) if uri == Credential::BASE_CONTEXT); - assert_matches!(credential.context.get(1).unwrap(), Context::Uri(ref uri) if uri == "https://www.w3.org/2018/credentials/examples/v1"); - - assert_eq!(credential.id, Some("did:example:123".into())); - - assert_eq!(credential.types.len(), 2); - assert_eq!(credential.types.get(0).unwrap(), Credential::BASE_TYPE); - assert_eq!(credential.types.get(1).unwrap(), "RelationshipCredential"); - - assert_eq!(credential.credential_subject.len(), 2); - assert_eq!( - credential.credential_subject.get(0).unwrap().id, - Some("did:iota:alice".into()) - ); - assert_eq!( - credential.credential_subject.get(1).unwrap().id, - Some("did:iota:bob".into()) - ); - - assert_eq!(credential.issuer.uri(), "did:example:issuer"); - - assert_eq!(credential.issuance_date, issuance); -} - -#[test] -#[should_panic = "Missing `Credential` subject"] -fn test_builder_missing_subjects() { - CredentialBuilder::new() - .issuer("did:issuer") - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} - -#[test] -#[should_panic = "Invalid `Credential` subject"] -fn test_builder_invalid_subjects() { - CredentialBuilder::new() - .issuer("did:issuer") - .try_subject(object!()) - .unwrap_or_else(|error| panic!("{}", error)) - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} - -#[test] -#[should_panic = "Missing `Credential` issuer"] -fn test_builder_missing_issuer() { - CredentialBuilder::new() - .try_subject(object!(id: "did:sub")) - .unwrap_or_else(|error| panic!("{}", error)) - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} - -#[test] -#[should_panic = "Invalid URI for Credential issuer"] -fn test_builder_invalid_issuer() { - CredentialBuilder::new() - .try_subject(object!(id: "did:sub")) - .unwrap_or_else(|error| panic!("{}", error)) - .issuer("foo") - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} diff --git a/identity_vc/tests/macros.rs b/identity_vc/tests/macros.rs deleted file mode 100644 index 3796eb189a..0000000000 --- a/identity_vc/tests/macros.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_export] -macro_rules! assert_matches { - ($($tt:tt)*) => { - assert!(matches!($($tt)*)) - }; -} - -#[macro_export] -macro_rules! timestamp { - ($expr:expr) => {{ - use ::std::convert::TryFrom; - ::identity_common::Timestamp::try_from($expr).unwrap() - }}; -} diff --git a/identity_vc/tests/presentation.rs b/identity_vc/tests/presentation.rs deleted file mode 100644 index ca52a02694..0000000000 --- a/identity_vc/tests/presentation.rs +++ /dev/null @@ -1,104 +0,0 @@ -#[macro_use] -mod macros; - -use identity_common::object; -use identity_vc::prelude::*; - -#[test] -fn test_builder_valid() { - let issuance = timestamp!("2010-01-01T00:00:00Z"); - - let credential = CredentialBuilder::new() - .issuer("did:example:issuer") - .context("https://www.w3.org/2018/credentials/examples/v1") - .type_("PrescriptionCredential") - .try_subject(object!(id: "did:iota:alice")) - .unwrap() - .issuance_date(issuance) - .build() - .unwrap(); - - let verifiable = VerifiableCredential::new(credential, object!()); - - let presentation = PresentationBuilder::new() - .context("https://www.w3.org/2018/credentials/examples/v1") - .id("did:example:id:123") - .type_("PrescriptionCredential") - .credential(verifiable.clone()) - .try_refresh_service(object!(id: "", type: "Refresh2020")) - .unwrap() - .try_terms_of_use(object!(type: "Policy2019")) - .unwrap() - .try_terms_of_use(object!(type: "Policy2020")) - .unwrap() - .build() - .unwrap(); - - assert_eq!(presentation.context.len(), 2); - assert_matches!(presentation.context.get(0).unwrap(), Context::Uri(ref uri) if uri == Credential::BASE_CONTEXT); - assert_matches!(presentation.context.get(1).unwrap(), Context::Uri(ref uri) if uri == "https://www.w3.org/2018/credentials/examples/v1"); - - assert_eq!(presentation.id, Some("did:example:id:123".into())); - - assert_eq!(presentation.types.len(), 2); - assert_eq!(presentation.types.get(0).unwrap(), Presentation::BASE_TYPE); - assert_eq!(presentation.types.get(1).unwrap(), "PrescriptionCredential"); - - assert_eq!(presentation.verifiable_credential.len(), 1); - assert_eq!(presentation.verifiable_credential.get(0).unwrap(), &verifiable); - - assert_eq!(presentation.refresh_service.unwrap().len(), 1); - assert_eq!(presentation.terms_of_use.unwrap().len(), 2); -} - -#[test] -#[should_panic = "Invalid URI for Presentation id"] -fn test_builder_invalid_id_fmt() { - PresentationBuilder::new() - .id("foo") - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} - -#[test] -#[should_panic = "Invalid URI for Presentation holder"] -fn test_builder_invalid_holder_fmt() { - PresentationBuilder::new() - .id("did:iota:123") - .holder("d00m") - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} - -#[test] -#[should_panic = "Cannot convert `Object` to `RefreshService`"] -fn test_builder_invalid_refresh_service_missing_id() { - PresentationBuilder::new() - .id("did:iota:123") - .try_refresh_service(object!(type: "RefreshServiceType")) - .unwrap_or_else(|error| panic!("{}", error)) - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} - -#[test] -#[should_panic = "Cannot convert `Object` to `RefreshService`"] -fn test_builder_invalid_refresh_service_missing_type() { - PresentationBuilder::new() - .id("did:iota:123") - .try_refresh_service(object!(id: "did:iota:rsv:123")) - .unwrap_or_else(|error| panic!("{}", error)) - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} - -#[test] -#[should_panic = "Cannot convert `Object` to `TermsOfUse`"] -fn test_builder_invalid_terms_of_use_missing_type() { - PresentationBuilder::new() - .id("did:iota:123") - .try_terms_of_use(object!(id: "did:iota:rsv:123")) - .unwrap_or_else(|error| panic!("{}", error)) - .build() - .unwrap_or_else(|error| panic!("{}", error)); -} diff --git a/identity_vc/tests/serde.rs b/identity_vc/tests/serde.rs deleted file mode 100644 index 505fc1f32c..0000000000 --- a/identity_vc/tests/serde.rs +++ /dev/null @@ -1,38 +0,0 @@ -use identity_vc::prelude::*; -use serde_json::from_str; - -fn try_credential(data: &(impl AsRef + ?Sized)) { - from_str::(data.as_ref()) - .unwrap() - .validate() - .unwrap() -} - -fn try_presentation(data: &(impl AsRef + ?Sized)) { - from_str::(data.as_ref()) - .unwrap() - .validate() - .unwrap() -} - -#[test] -fn test_parse_credential_examples() { - try_credential(include_str!("input/example-01.json")); - try_credential(include_str!("input/example-02.json")); - try_credential(include_str!("input/example-03.json")); - try_credential(include_str!("input/example-04.json")); - try_credential(include_str!("input/example-05.json")); - try_credential(include_str!("input/example-06.json")); - try_credential(include_str!("input/example-07.json")); - - try_credential(include_str!("input/example-09.json")); - try_credential(include_str!("input/example-10.json")); - try_credential(include_str!("input/example-11.json")); - try_credential(include_str!("input/example-12.json")); - try_credential(include_str!("input/example-13.json")); -} - -#[test] -fn test_parse_presentation_examples() { - try_presentation(include_str!("input/example-08.json")); -} diff --git a/rustfmt.toml b/rustfmt.toml index 3a091b97bd..2b98e07a9b 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -5,4 +5,4 @@ max_width = 120 merge_imports = true normalize_comments = false normalize_doc_attributes = false -wrap_comments = true \ No newline at end of file +wrap_comments = true