diff --git a/affinidi-did-resolver-methods/did-peer/Cargo.toml b/affinidi-did-resolver-methods/did-peer/Cargo.toml index e68e255..4517e61 100644 --- a/affinidi-did-resolver-methods/did-peer/Cargo.toml +++ b/affinidi-did-resolver-methods/did-peer/Cargo.toml @@ -24,6 +24,8 @@ ssi.workspace = true thiserror.workspace = true wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true +toml.workspace = true +regex.workspace = true [dev-dependencies] askar-crypto.workspace = true diff --git a/affinidi-did-resolver-methods/did-peer/conf/did-peer-conf.toml b/affinidi-did-resolver-methods/did-peer/conf/did-peer-conf.toml new file mode 100644 index 0000000..4131949 --- /dev/null +++ b/affinidi-did-resolver-methods/did-peer/conf/did-peer-conf.toml @@ -0,0 +1,7 @@ +### max_did_size_in_kb: Maximum size in KB of did to be resolved as FLOAT +### default: 1 +max_did_size_in_kb = "${MAX_DID_SIZE_IN_KB:1.0}" + +### max_did_parts: Maximum number of parts after splitting did on "." +### Default: 5 +max_did_parts = "${MAX_DID_PARTS:5}" diff --git a/affinidi-did-resolver-methods/did-peer/src/config.rs b/affinidi-did-resolver-methods/did-peer/src/config.rs new file mode 100644 index 0000000..a2e4ac6 --- /dev/null +++ b/affinidi-did-resolver-methods/did-peer/src/config.rs @@ -0,0 +1,125 @@ +use regex::{Captures, Regex}; +use serde::{Deserialize, Serialize}; +use std::{ + env, fmt, + fs::File, + io::{self, BufRead}, + path::Path, +}; + +use crate::DIDPeerError; + +/// ConfigRaw Struct is used to deserialize the configuration file +/// We then convert this to the CacheConfig Struct +#[derive(Debug, Serialize, Deserialize)] +struct ConfigRaw { + pub max_did_size_in_kb: String, + pub max_did_parts: String, +} +#[derive(Clone)] +pub struct Config { + pub max_did_size_in_kb: f64, + pub max_did_parts: usize, +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Config") + .field("max_did_size_in_kb", &self.max_did_size_in_kb) + .field("max_did_parts", &self.max_did_parts) + .finish() + } +} + +impl Default for Config { + fn default() -> Self { + Config { + max_did_size_in_kb: 1.0, + max_did_parts: 5, + } + } +} + +impl TryFrom for Config { + type Error = DIDPeerError; + + fn try_from(raw: ConfigRaw) -> Result { + println!("RAW conf: {:?}", raw.max_did_size_in_kb); + println!("RAW conf: {:?}", raw.max_did_size_in_kb); + Ok(Config { + max_did_parts: raw.max_did_parts.parse().unwrap_or(5), + max_did_size_in_kb: raw.max_did_size_in_kb.parse::().unwrap_or(1.0), + }) + } +} + +/// Read the primary configuration file for the mediator +/// Returns a ConfigRaw struct, that still needs to be processed for additional information +/// and conversion to Config struct +fn read_config_file(file_name: &str) -> Result { + // Read configuration file parameters + let raw_config = read_file_lines(file_name)?; + + let config_with_vars = expand_env_vars(&raw_config); + match toml::from_str(&config_with_vars.join("\n")) { + Ok(config) => Ok(config), + Err(err) => Err(DIDPeerError::ConfigError(format!( + "Could not parse configuration settings. Reason: {:?}", + err + ))), + } +} + +/// Reads a file and returns a vector of strings, one for each line in the file. +/// It also strips any lines starting with a # (comments) +/// You can join the Vec back into a single string with `.join("\n")` +pub(crate) fn read_file_lines

(file_name: P) -> Result, DIDPeerError> +where + P: AsRef, +{ + let file = File::open(file_name.as_ref()).map_err(|err| { + DIDPeerError::ConfigError(format!( + "Could not open file({}). {}", + file_name.as_ref().display(), + err + )) + })?; + + let mut lines = Vec::new(); + for line in io::BufReader::new(file).lines().map_while(Result::ok) { + // Strip comments out + if !line.starts_with('#') { + lines.push(line); + } + } + + Ok(lines) +} + +/// Replaces all strings ${VAR_NAME:default_value} +/// with the corresponding environment variables (e.g. value of ${VAR_NAME}) +/// or with `default_value` if the variable is not defined. +fn expand_env_vars(raw_config: &Vec) -> Vec { + let re = Regex::new(r"\$\{(?P[A-Z_]{1,}[0-9A-Z_]*):(?P.*)\}").unwrap(); + let mut result: Vec = Vec::new(); + for line in raw_config { + result.push( + re.replace_all(line, |caps: &Captures| match env::var(&caps["env_var"]) { + Ok(val) => val, + Err(_) => (caps["default_value"]).into(), + }) + .into_owned(), + ); + } + result +} + +pub fn init() -> Result { + // Read configuration file parameters + let config_raw = read_config_file("conf/did-peer-conf.toml")?; + + match Config::try_from(config_raw) { + Ok(parsed_config) => Ok(parsed_config), + Err(err) => Err(err), + } +} diff --git a/affinidi-did-resolver-methods/did-peer/src/lib.rs b/affinidi-did-resolver-methods/did-peer/src/lib.rs index ac1dff4..b2988b7 100644 --- a/affinidi-did-resolver-methods/did-peer/src/lib.rs +++ b/affinidi-did-resolver-methods/did-peer/src/lib.rs @@ -17,7 +17,10 @@ //! } //! ``` //! +mod config; + use base64::prelude::*; +use config::init; use iref::UriBuf; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -42,6 +45,8 @@ use std::{collections::BTreeMap, fmt}; use thiserror::Error; use wasm_bindgen::prelude::*; +const BYTES_PER_KILO_BYTE: f64 = 1000.0; + #[derive(Error, Debug)] pub enum DIDPeerError { #[error("Unsupported key type")] @@ -62,6 +67,8 @@ pub enum DIDPeerError { JsonParsingError(String), #[error("Internal error: {0}")] InternalError(String), + #[error("Configuration Error: {0}")] + ConfigError(String), } // Converts DIDPeerError to JsValue which is required for propagating errors to WASM @@ -268,6 +275,17 @@ impl DIDMethodResolver for DIDPeer { method_specific_id: &'a str, options: Options, ) -> Result>, Error> { + let config = init().unwrap(); + let did_size_in_kb = method_specific_id.len() as f64 / BYTES_PER_KILO_BYTE; + + // If DID's size is greater than 1KB we don't resolve it + if did_size_in_kb > config.max_did_size_in_kb { + return Err(Error::InvalidMethodSpecificId(format!( + "Method specific id's size: {:.3} is greater than 1KB, size must be less than 1KB", + did_size_in_kb + ))); + } + // If did:peer is type 0, then treat it as a did:key if let Some(id) = method_specific_id.strip_prefix('0') { return DIDKey.resolve_method_representation(id, options).await; @@ -298,6 +316,13 @@ impl DIDMethodResolver for DIDPeer { let mut key_count: u32 = 1; let mut service_idx: u32 = 0; + if parts.len() > config.max_did_parts { + return Err(Error::InvalidMethodSpecificId(format!( + "Must have less than or equal 5 keys and/or services combined, found {}", + parts.len() + ))); + } + for part in parts { let ch = part.chars().next(); match ch {