From 4133d5a1b8d3e272002ab5df90311c251a6ed581 Mon Sep 17 00:00:00 2001 From: DyrellC Date: Thu, 4 Jan 2024 15:10:44 -0700 Subject: [PATCH 1/2] Commit core functionality --- Cargo.toml | 15 +++++ README.md | 12 +++- src/annotations.rs | 60 ++++++++++++++++++++ src/annotator.rs | 6 ++ src/constants.rs | 104 ++++++++++++++++++++++++++++++++++ src/errors.rs | 24 ++++++++ src/lib.rs | 17 ++++++ src/providers.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100644 src/annotations.rs create mode 100644 src/annotator.rs create mode 100644 src/constants.rs create mode 100644 src/errors.rs create mode 100644 src/lib.rs create mode 100644 src/providers.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..050261f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "alvarium-annotator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.143", features = ["derive"]} +serde_json = "1.0.96" +thiserror = "1.0.40" +ulid = "1.0.0" +chrono = "0.4.22" +async-trait = "0.1.68" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/README.md b/README.md index ce1d547..4151fa4 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# alvarium_annotator \ No newline at end of file +# Alvarium Annotator Core + +This crate contains traits, types and constants for use in the Alvarium SDK. +This includes interface definitions for [Annotators](src/annotator.rs) and +[Annotations](src/annotations.rs), as well as Hash, Signature, and Stream +[Providers](src/providers.rs). + +The type definitions such as HashType and KeyAlgorithm provided flexible +wrappers that can be expanded beyond the preset constants. This allows for +custom annotators to be developed that still conforms to the interface +patterns required for use within the SDK. diff --git a/src/annotations.rs b/src/annotations.rs new file mode 100644 index 0000000..2d9633f --- /dev/null +++ b/src/annotations.rs @@ -0,0 +1,60 @@ +use crate::constants::{self, AnnotationType, HashType}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct Annotation { + pub id: String, + pub key: String, + pub hash: HashType, + pub host: String, + pub kind: AnnotationType, + pub signature: String, + #[serde(rename = "isSatisfied")] + pub is_satisfied: bool, + pub timestamp: String, +} + +#[derive(Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct AnnotationList { + pub items: Vec, +} + +impl Annotation { + pub fn new( + key: &str, + hash: HashType, + host: &str, + kind: AnnotationType, + is_satisfied: bool, + ) -> Self { + let timestamp = chrono::Local::now().to_rfc3339(); + Annotation { + id: ulid::Ulid::new().to_string(), + key: key.to_string(), + hash, + host: host.to_string(), + kind, + signature: String::new(), + is_satisfied, + timestamp, + } + } + + pub fn with_signature(&mut self, signature: &str) { + self.signature = signature.to_string() + } + + pub fn validate_base(&self) -> bool { + self.hash.is_base_hash_type() && self.kind.is_base_annotation_type() + } +} + +pub fn mock_annotation() -> Annotation { + let key = "The hash of the contents"; + let hash = constants::SHA256_HASH.clone(); + let host = "Host Device"; + let kind = constants::ANNOTATION_SOURCE.clone(); + let satisfied = true; + + Annotation::new(key, hash, host, kind, satisfied) +} diff --git a/src/annotator.rs b/src/annotator.rs new file mode 100644 index 0000000..627332a --- /dev/null +++ b/src/annotator.rs @@ -0,0 +1,6 @@ +use crate::Annotation; + +pub trait Annotator { + type Error: std::error::Error; + fn annotate(&mut self, data: &[u8]) -> Result; +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..37fe723 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,104 @@ +use crate::errors::{Error, Result}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; + +pub trait Validate { + fn validate(&self) -> bool; +} + +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct HashType(pub String); + +lazy_static! { + pub static ref MD5_HASH: HashType = HashType(String::from("md5")); + pub static ref SHA256_HASH: HashType = HashType(String::from("sha256")); + pub static ref NO_HASH: HashType = HashType(String::from("none")); +} + +impl HashType { + pub fn is_base_hash_type(&self) -> bool { + self == MD5_HASH.deref() || self == SHA256_HASH.deref() || self == NO_HASH.deref() + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct KeyAlgorithm(pub String); + +lazy_static! { + pub static ref ED25519_KEY: KeyAlgorithm = KeyAlgorithm(String::from("ed25519")); +} + +impl KeyAlgorithm { + pub fn is_base_key_algorithm(&self) -> bool { + self == ED25519_KEY.deref() + } +} + +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct StreamType(pub String); + +lazy_static! { + pub static ref IOTA_STREAM: StreamType = StreamType(String::from("iota")); + pub static ref MOCK_STREAM: StreamType = StreamType(String::from("mock")); + pub static ref MQTT_STREAM: StreamType = StreamType(String::from("mqtt")); +} + +impl StreamType { + pub fn is_base_stream_type(&self) -> bool { + self == IOTA_STREAM.deref() || self == MOCK_STREAM.deref() || self == MQTT_STREAM.deref() + } +} +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct AnnotationType(pub String); + +lazy_static! { + pub static ref ANNOTATION_PKI: AnnotationType = AnnotationType(String::from("pki")); + pub static ref ANNOTATION_SOURCE: AnnotationType = AnnotationType(String::from("source")); + pub static ref ANNOTATION_TLS: AnnotationType = AnnotationType(String::from("tls")); + pub static ref ANNOTATION_TPM: AnnotationType = AnnotationType(String::from("tpm")); +} + +impl AnnotationType { + pub fn kind(&self) -> &str { + &self.0 + } + pub fn is_base_annotation_type(&self) -> bool { + self == ANNOTATION_PKI.deref() + || self == ANNOTATION_SOURCE.deref() + || self == ANNOTATION_TLS.deref() + || self == ANNOTATION_TPM.deref() + } +} + +impl TryFrom<&str> for AnnotationType { + type Error = Error; + fn try_from(kind: &str) -> Result { + match kind { + "source" => Ok(ANNOTATION_SOURCE.clone()), + "pki" => Ok(ANNOTATION_PKI.clone()), + "tls" => Ok(ANNOTATION_TLS.clone()), + "tpm" => Ok(ANNOTATION_TPM.clone()), + _ => Err(Error::UnknownAnnotation), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SdkAction(pub String); + +lazy_static! { + pub static ref ACTION_CREATE: SdkAction = SdkAction(String::from("create")); + pub static ref ACTION_MUTATE: SdkAction = SdkAction(String::from("mutate")); + pub static ref ACTION_TRANSIT: SdkAction = SdkAction(String::from("transit")); + pub static ref ACTION_PUBLISH: SdkAction = SdkAction(String::from("publish")); +} + +impl SdkAction { + pub fn is_base_action(&self) -> bool { + self == ACTION_CREATE.deref() + || self == ACTION_MUTATE.deref() + || self == ACTION_TRANSIT.deref() + || self == ACTION_PUBLISH.deref() + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..46225b9 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +pub type Result = core::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("unknown annotation type")] + UnknownAnnotation, + #[error("annotation failed to serialize: {0}")] + AnnotationSerialize(serde_json::Error), + + #[cfg(test)] + #[error("provider failed to sign")] + SignatureError, + #[cfg(test)] + #[error("provider failed to verify")] + VerificationError, +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Error::AnnotationSerialize(err) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c1814ee --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,17 @@ +mod annotations; +mod annotator; +mod errors; +mod providers; +mod config { + pub trait StreamConfigWrapper { + fn stream_type(&self) -> &crate::constants::StreamType; + } +} + +pub use annotations::*; +pub use annotator::Annotator; +pub use config::StreamConfigWrapper; +pub use errors::Error; +pub use providers::*; + +pub mod constants; diff --git a/src/providers.rs b/src/providers.rs new file mode 100644 index 0000000..fc3c402 --- /dev/null +++ b/src/providers.rs @@ -0,0 +1,137 @@ +mod hash_providers { + + pub trait HashProvider { + fn derive(&self, data: &[u8]) -> String; + } + + pub fn derive_hash(hash_type: H, data: &[u8]) -> String { + hash_type.derive(data) + } + + #[cfg(test)] + mod hash_provider_tests { + use crate::HashProvider; + + struct MockHashProvider {} + + impl HashProvider for MockHashProvider { + fn derive(&self, _data: &[u8]) -> String { + "Derived".to_string() + } + } + + #[test] + fn test_mock_derive() { + let hash_provider = MockHashProvider {}; + let derived = hash_provider.derive("data".as_bytes()); + assert_eq!("Derived", derived) + } + } +} + +mod signature_provider { + use crate::Annotation; + + pub trait SignProvider { + type Error: std::error::Error; + fn sign(&self, content: &[u8]) -> Result; + fn verify(&self, content: &[u8], signed: &[u8]) -> Result; + } + + pub fn serialise_and_sign

(provider: &P, annotation: &Annotation) -> Result + where + P: SignProvider, +

::Error: From + { + let serialised = serde_json::to_vec(annotation)?; + provider.sign(&serialised) + } + + #[cfg(test)] + mod annotation_utility_tests { + use super::serialise_and_sign; + use crate::annotations::mock_annotation; + use crate::errors::{Error, Result}; + use crate::SignProvider; + + struct MockSignProvider { + pub public: String, + pub private: String, + } + + impl SignProvider for MockSignProvider { + type Error = crate::errors::Error; + fn sign(&self, _content: &[u8]) -> Result { + match self.private.as_str().eq("A known and correct key") { + true => Ok("Signed".to_string()), + false => Err(Error::SignatureError), + } + } + + fn verify(&self, _content: &[u8], _signed: &[u8]) -> Result { + match self.public.as_str().eq("A known and correct key") { + true => Ok(true), + false => Err(Error::VerificationError), + } + } + } + + #[test] + fn mock_sign_provider() { + let correct_key = "A known and correct key".to_string(); + let unknown_key = "An unknown key".to_string(); + + let mock_provider = MockSignProvider { + private: correct_key.clone(), + public: correct_key.clone(), + }; + + let bad_mock_provider = MockSignProvider { + private: unknown_key.clone(), + public: unknown_key.clone(), + }; + + let annotation = mock_annotation(); + + let failed_signature = serialise_and_sign(&bad_mock_provider, &annotation); + assert!(failed_signature.is_err()); + let signature = serialise_and_sign(&mock_provider, &annotation); + assert!(signature.is_ok()); + + let ann_bytes = serde_json::to_vec(&annotation).unwrap(); + let failed_verify = bad_mock_provider.verify("Content".as_bytes(), &ann_bytes); + assert!(failed_verify.is_err()); + let verified = mock_provider.verify("Content".as_bytes(), &ann_bytes); + assert!(verified.is_ok()) + } + } +} + +mod stream_provider { + use crate::constants::SdkAction; + use crate::StreamConfigWrapper; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct MessageWrapper<'a> { + pub action: SdkAction, + #[serde(rename = "messageType")] + pub message_type: &'a str, + pub content: &'a str, + } + + #[async_trait::async_trait] + pub trait Publisher: Sized { + type StreamConfig: StreamConfigWrapper; + type Error: std::error::Error; + async fn new(cfg: &Self::StreamConfig) -> Result; + async fn close(&mut self) -> Result<(), Self::Error>; + async fn connect(&mut self) -> Result<(), Self::Error>; + async fn reconnect(&mut self) -> Result<(), Self::Error>; + async fn publish(&mut self, msg: MessageWrapper<'_>) -> Result<(), Self::Error>; + } +} + +pub use hash_providers::{derive_hash, HashProvider}; +pub use signature_provider::{serialise_and_sign, SignProvider}; +pub use stream_provider::{MessageWrapper, Publisher}; From eb9a9dd7fb8c0402475904191f3f3af8e30bf8d1 Mon Sep 17 00:00:00 2001 From: DyrellC Date: Fri, 5 Jan 2024 10:55:44 -0700 Subject: [PATCH 2/2] iota -> demia --- src/constants.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 37fe723..9dd026f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -39,14 +39,14 @@ impl KeyAlgorithm { pub struct StreamType(pub String); lazy_static! { - pub static ref IOTA_STREAM: StreamType = StreamType(String::from("iota")); + pub static ref DEMIA_STREAM: StreamType = StreamType(String::from("demia")); pub static ref MOCK_STREAM: StreamType = StreamType(String::from("mock")); pub static ref MQTT_STREAM: StreamType = StreamType(String::from("mqtt")); } impl StreamType { pub fn is_base_stream_type(&self) -> bool { - self == IOTA_STREAM.deref() || self == MOCK_STREAM.deref() || self == MQTT_STREAM.deref() + self == DEMIA_STREAM.deref() || self == MOCK_STREAM.deref() || self == MQTT_STREAM.deref() } } #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]