Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Commit of Alvarium Annotator traits package #1

Merged
merged 2 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "alvarium-annotator"
version = "0.1.0"
edition = "2021"
tsconn23 marked this conversation as resolved.
Show resolved Hide resolved

# 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"
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# alvarium_annotator
# 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.
60 changes: 60 additions & 0 deletions src/annotations.rs
Original file line number Diff line number Diff line change
@@ -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<Annotation>,
}

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)
}
6 changes: 6 additions & 0 deletions src/annotator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use crate::Annotation;

pub trait Annotator {
type Error: std::error::Error;
fn annotate(&mut self, data: &[u8]) -> Result<Annotation, Self::Error>;
}
104 changes: 104 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -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 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 == DEMIA_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<Self> {
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()
}
}
24 changes: 24 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use thiserror::Error;

pub type Result<T> = core::result::Result<T, Error>;

#[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<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::AnnotationSerialize(err)
}
}
17 changes: 17 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
137 changes: 137 additions & 0 deletions src/providers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
mod hash_providers {

pub trait HashProvider {
fn derive(&self, data: &[u8]) -> String;
}

pub fn derive_hash<H: HashProvider>(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<String, Self::Error>;
fn verify(&self, content: &[u8], signed: &[u8]) -> Result<bool, Self::Error>;
}

pub fn serialise_and_sign<P>(provider: &P, annotation: &Annotation) -> Result<String, P::Error>
where
P: SignProvider,
<P as SignProvider>::Error: From<serde_json::Error>
{
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<String> {
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<bool> {
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<Self, Self::Error>;
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};