From a6bc98524981fc78c56077ba4063354a3edcf45a Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Thu, 11 Jan 2024 22:32:13 +0900 Subject: [PATCH] add tests for http message components --- src/lib.rs | 5 +- src/message_component.rs | 385 +++++++++++++++++++++++++++++++++++++++ src/signature_base.rs | 14 ++ src/signature_params.rs | 350 ++++++----------------------------- src/signer.rs | 10 + src/util.rs | 9 + 6 files changed, 482 insertions(+), 291 deletions(-) create mode 100644 src/message_component.rs create mode 100644 src/signature_base.rs create mode 100644 src/util.rs diff --git a/src/lib.rs b/src/lib.rs index 3561547..d320039 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ mod crypto; +mod message_component; +mod signature_base; mod signature_params; mod trace; +mod util; use crate::{ crypto::{PublicKey, SecretKey, SigningKey, VerifyingKey}, @@ -29,7 +32,7 @@ MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs= "@signature-params": ("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519""##; const SIGNATURE_VALUE: &str = "wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw=="; - const SIGNATURE_RESULT: &str = r##"Signature-Input: sig-b26=("date" "@method" "@path" "@authority" \ + const _SIGNATURE_RESULT: &str = r##"Signature-Input: sig-b26=("date" "@method" "@path" "@authority" \ "content-type" "content-length");created=1618884473\ ;keyid="test-key-ed25519" Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1\ diff --git a/src/message_component.rs b/src/message_component.rs new file mode 100644 index 0000000..8698f55 --- /dev/null +++ b/src/message_component.rs @@ -0,0 +1,385 @@ +use rustc_hash::FxHashSet as HashSet; + +/* ---------------------------------------------------------------- */ +/// Http message component +pub(crate) struct HttpMessageComponent { + /// Http message component identifier + id: HttpMessageComponentIdentifier, + /// Http message component value + value: HttpMessageComponentValue, +} + +impl TryFrom<&str> for HttpMessageComponent { + type Error = anyhow::Error; + fn try_from(val: &str) -> std::result::Result { + let Some((id, value)) = val.split_once(':') else { + return Err(anyhow::anyhow!("Invalid http message component: {}", val)); + }; + Ok(Self { + id: HttpMessageComponentIdentifier::from(id.trim()), + value: HttpMessageComponentValue::from(value.trim()), + }) + } +} + +impl std::fmt::Display for HttpMessageComponent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // This always can append single trailing space (SP) for empty value + // https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1 + write!(f, "{}: {}", self.id, self.value) + } +} + +/* ---------------------------------------------------------------- */ +/// Http message component value +pub(crate) struct HttpMessageComponentValue { + /// inner value originally from http message header or derived from http message + inner: String, +} + +impl From<&str> for HttpMessageComponentValue { + fn from(val: &str) -> Self { + Self { inner: val.to_string() } + } +} + +impl std::fmt::Display for HttpMessageComponentValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +/* ---------------------------------------------------------------- */ +#[derive(PartialEq, Eq, Hash, Debug)] +/// Http message component identifier +pub(crate) enum HttpMessageComponentIdentifier { + /// Http field component + HttpField(HttpFieldComponentId), + /// Derived commponent + Derived(DerivedComponentId), +} + +impl From<&str> for HttpMessageComponentIdentifier { + fn from(val: &str) -> Self { + if val.starts_with("\"@") { + Self::Derived(DerivedComponentId::from(val)) + } else { + Self::HttpField(HttpFieldComponentId::from(val)) + } + } +} + +impl std::fmt::Display for HttpMessageComponentIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::HttpField(val) => write!(f, "{}", val), + Self::Derived(val) => write!(f, "{}", val), + } + } +} + +/* ---------------------------------------------------------------- */ +#[derive(PartialEq, Eq, Hash, Debug, Clone)] +/// Http message component parameters that appends with `;` in the signature input +/// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#secion-2.1 +pub(crate) enum HttpMessageComponentParam { + /// sf: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.1 + Sf, + /// key: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.2 + /// This will be encoded to `;key="..."` in the signature input + Key(String), + /// bs: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.3 + Bs, + // tr: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.4 + Tr, + // req: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.4 + Req, +} + +impl From for String { + fn from(val: HttpMessageComponentParam) -> Self { + match val { + HttpMessageComponentParam::Sf => "sf".to_string(), + HttpMessageComponentParam::Key(val) => format!("key=\"{}\"", val), + HttpMessageComponentParam::Bs => "bs".to_string(), + HttpMessageComponentParam::Tr => "tr".to_string(), + HttpMessageComponentParam::Req => "req".to_string(), + } + } +} +impl From<&str> for HttpMessageComponentParam { + fn from(val: &str) -> Self { + match val { + "sf" => Self::Sf, + "bs" => Self::Bs, + "tr" => Self::Tr, + "req" => Self::Req, + _ => { + if val.starts_with("key=\"") && val.ends_with('"') { + Self::Key(val[5..val.len() - 1].to_string()) + } else { + panic!("Invalid http field param: {}", val) + } + } + } + } +} + +#[derive(PartialEq, Eq, Debug)] +pub(crate) struct HttpMessageComponentParams(pub(crate) HashSet); +impl std::hash::Hash for HttpMessageComponentParams { + fn hash(&self, state: &mut H) { + let mut params = self.0.iter().map(|v| v.clone().into()).collect::>(); + params.sort(); + params.hash(state); + } +} + +/* ---------------------------------------------------------------- */ +#[derive(PartialEq, Eq, Clone, Hash, Debug)] +/// Derive components from http message, which is expressed as @method, @path, @authority, etc. in @signature-params +/// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-derived-components +pub(crate) enum DerivedComponentName { + Method, + TargetUri, + Authority, + Scheme, + RequestTarget, + Path, + Query, + QueryParam, + Status, +} +impl AsRef for DerivedComponentName { + fn as_ref(&self) -> &str { + match self { + Self::Method => "\"@method\"", + Self::TargetUri => "\"@target-uri\"", + Self::Authority => "\"@authority\"", + Self::Scheme => "\"@scheme\"", + Self::RequestTarget => "\"@request-target\"", + Self::Path => "\"@path\"", + Self::Query => "\"@query\"", + Self::QueryParam => "\"@query-param\"", + Self::Status => "\"@status\"", + } + } +} +impl From for String { + fn from(val: DerivedComponentName) -> Self { + val.as_ref().to_string() + } +} +impl From<&str> for DerivedComponentName { + fn from(val: &str) -> Self { + match val { + "\"@method\"" => Self::Method, + "\"@target-uri\"" => Self::TargetUri, + "\"@authority\"" => Self::Authority, + "\"@scheme\"" => Self::Scheme, + "\"@request-target\"" => Self::RequestTarget, + "\"@path\"" => Self::Path, + "\"@query\"" => Self::Query, + "\"@query-param\"" => Self::QueryParam, + "\"@status\"" => Self::Status, + _ => panic!("Invalid derived component: {}", val), + } + } +} + +#[derive(PartialEq, Eq, Hash, Debug)] +/// Http derived component setting with optional parameters +pub(crate) struct DerivedComponentId { + /// derived component + pub(crate) component_name: DerivedComponentName, + /// parameters + pub(crate) params: HttpMessageComponentParams, +} + +impl From<&str> for DerivedComponentId { + /// this feeds `"";` format, e.g., `"@method";req` + fn from(val: &str) -> Self { + let mut iter = val.split(';'); + let component = iter.next().unwrap(); + // only `req` field param is allowed for derived components unlike general http fields + let params = iter + .map(HttpMessageComponentParam::from) + .filter(|v| matches!(v, HttpMessageComponentParam::Req)) + .collect::>(); + Self { + component_name: DerivedComponentName::from(component), + params: HttpMessageComponentParams(params), + } + } +} +impl std::fmt::Display for DerivedComponentId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s: String = self.component_name.clone().into(); + if !self.params.0.is_empty() { + s.push(';'); + s.push_str( + &self + .params + .0 + .iter() + .map(|v| v.clone().into()) + .collect::>() + .join(";"), + ); + } + write!(f, "{}", s) + } +} + +/* ---------------------------------------------------------------- */ +#[derive(PartialEq, Eq, Hash, Debug)] +/// Http field component setting with optional parameters +/// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-http-fields +pub(crate) struct HttpFieldComponentId { + /// field name + pub(crate) filed_name: String, + /// parameters + pub(crate) params: HttpMessageComponentParams, +} + +impl From<&str> for HttpFieldComponentId { + /// this feeds `"";` format, e.g., `"example-header";bs` + fn from(val: &str) -> Self { + let mut iter = val.split(';'); + let field_name = iter.next().unwrap(); + let params = iter.map(HttpMessageComponentParam::from).collect::>(); + Self { + filed_name: field_name.to_string(), + params: HttpMessageComponentParams(params), + } + } +} + +impl std::fmt::Display for HttpFieldComponentId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s: String = self.filed_name.clone(); + if !self.params.0.is_empty() { + s.push(';'); + s.push_str( + &self + .params + .0 + .iter() + .map(|v| v.clone().into()) + .collect::>() + .join(";"), + ); + } + write!(f, "{}", s) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_http_message_component() { + let comp = HttpMessageComponent::try_from("\"example-header\";bs: example-value").unwrap(); + assert_eq!( + comp.id, + HttpMessageComponentIdentifier::HttpField(HttpFieldComponentId { + filed_name: "\"example-header\"".to_string(), + params: HttpMessageComponentParams(vec![HttpMessageComponentParam::Bs].into_iter().collect::>()) + }) + ); + assert_eq!(comp.value.inner, "example-value"); + assert_eq!(comp.to_string(), "\"example-header\";bs: example-value"); + + let comp = HttpMessageComponent::try_from("\"@method\";req: POST").unwrap(); + assert_eq!( + comp.id, + HttpMessageComponentIdentifier::Derived(DerivedComponentId { + component_name: DerivedComponentName::Method, + params: HttpMessageComponentParams(vec![HttpMessageComponentParam::Req].into_iter().collect::>()) + }) + ); + assert_eq!(comp.value.inner, "POST"); + assert_eq!(comp.to_string(), "\"@method\";req: POST"); + + let comp = HttpMessageComponent::try_from("\"x-empty-header\": ").unwrap(); + assert_eq!( + comp.id, + HttpMessageComponentIdentifier::HttpField(HttpFieldComponentId { + filed_name: "\"x-empty-header\"".to_string(), + params: HttpMessageComponentParams(HashSet::default()) + }) + ); + assert_eq!(comp.value.inner, ""); + assert_eq!(comp.to_string(), "\"x-empty-header\": "); + } + #[test] + fn test_http_message_component_value() { + let val = HttpMessageComponentValue::from("example-value"); + assert_eq!(val.inner, "example-value"); + assert_eq!(val.to_string(), "example-value"); + + let val = HttpMessageComponentValue::from(""); + assert_eq!(val.inner, ""); + assert_eq!(val.to_string(), ""); + } + #[test] + fn test_http_message_component_id_enum() { + let params = HttpMessageComponentIdentifier::from("\"example-header\";bs"); + assert_eq!( + params, + HttpMessageComponentIdentifier::HttpField(HttpFieldComponentId { + filed_name: "\"example-header\"".to_string(), + params: HttpMessageComponentParams(vec![HttpMessageComponentParam::Bs].into_iter().collect::>()) + }) + ); + + let params = HttpMessageComponentIdentifier::from("\"@method\";req"); + assert_eq!( + params, + HttpMessageComponentIdentifier::Derived(DerivedComponentId { + component_name: DerivedComponentName::Method, + params: HttpMessageComponentParams(vec![HttpMessageComponentParam::Req].into_iter().collect::>()) + }) + ); + } + #[test] + fn test_derived_components() { + let params = DerivedComponentId::from("\"@method\";req"); + assert_eq!(params.component_name, DerivedComponentName::Method); + assert_eq!( + params.params.0, + vec![HttpMessageComponentParam::Req].into_iter().collect::>() + ); + + // drops invalid field params + let params = DerivedComponentId::from("\"@path\";req;key=\"test\""); + assert_eq!(params.component_name, DerivedComponentName::Path); + assert_eq!( + params.params.0, + vec![HttpMessageComponentParam::Req].into_iter().collect::>() + ); + } + + #[test] + fn test_http_general_field() { + let params = HttpFieldComponentId::from("\"example-header\";bs"); + assert_eq!(params.filed_name, "\"example-header\""); + assert_eq!( + params.params.0, + vec![HttpMessageComponentParam::Bs].into_iter().collect::>() + ); + + // keeps field params + let params = HttpFieldComponentId::from("\"example-header\";bs;key=\"test\""); + assert_eq!(params.filed_name, "\"example-header\""); + assert_eq!( + params.params.0, + vec![ + HttpMessageComponentParam::Bs, + HttpMessageComponentParam::Key("test".to_string()) + ] + .into_iter() + .collect::>() + ); + } +} diff --git a/src/signature_base.rs b/src/signature_base.rs new file mode 100644 index 0000000..d94f274 --- /dev/null +++ b/src/signature_base.rs @@ -0,0 +1,14 @@ +use crate::{message_component::HttpMessageComponent, signature_params::HttpSignatureParams}; + +/// Signature Base +/// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.5 +struct SignatureBase { + /// HTTP message field and derived components ordered as in the vector in signature params + component_lines: Vec, + /// signature params + signature_params: HttpSignatureParams, +} + +// creating signature base from http header lines and signature params builder config + +// creating signature base from http header lines including signature params diff --git a/src/signature_params.rs b/src/signature_params.rs index b25cda2..c19c175 100644 --- a/src/signature_params.rs +++ b/src/signature_params.rs @@ -1,215 +1,14 @@ -use crate::{crypto::VerifyingKey, trace::*}; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use crate::{ + crypto::VerifyingKey, + message_component::{DerivedComponentId, HttpFieldComponentId, HttpMessageComponentIdentifier}, + util::has_unique_elements, +}; +use base64::{engine::general_purpose, Engine as _}; use rand::Rng; -use rustc_hash::FxHashSet as HashSet; use std::time::{SystemTime, UNIX_EPOCH}; const DEFAULT_EXPIRES_IN: u64 = 300; -// API design: -// let signature_params = SignatureParamsBuilder::default() -// .created(1618884473) -// .key_id("test-key-ed25519") // Should key_id be set at signer builder? -// .headers(vec![...]) -// .build(); -// let signer = HttpSignatureSignerBuilder::default() -// .secret_key(SecretKey::HmacSha256(SymmetricKey::from(b"secret"))) -// .signature_params(signature_params) -// .build(); - -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -/// Http field parameters that appends with `;` in the signature input -/// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#secion-2.1 -enum HttpFieldParam { - /// sf: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.1 - Sf, - /// key: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.2 - /// This will be encoded to `;key="..."` in the signature input - Key(String), - /// bs: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.3 - Bs, - // tr: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.1.4 - Tr, - // req: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2.4 - Req, -} - -impl From for String { - fn from(val: HttpFieldParam) -> Self { - match val { - HttpFieldParam::Sf => "sf".to_string(), - HttpFieldParam::Key(val) => format!("key=\"{}\"", val), - HttpFieldParam::Bs => "bs".to_string(), - HttpFieldParam::Tr => "tr".to_string(), - HttpFieldParam::Req => "req".to_string(), - } - } -} -impl From<&str> for HttpFieldParam { - fn from(val: &str) -> Self { - match val { - "sf" => Self::Sf, - "bs" => Self::Bs, - "tr" => Self::Tr, - "req" => Self::Req, - _ => { - if val.starts_with("key=\"") && val.ends_with('"') { - Self::Key(val[5..val.len() - 1].to_string()) - } else { - panic!("Invalid http field param: {}", val) - } - } - } - } -} - -#[derive(PartialEq, Eq, Debug)] -struct HttpFieldParams(HashSet); -impl std::hash::Hash for HttpFieldParams { - fn hash(&self, state: &mut H) { - let mut params = self.0.iter().map(|v| v.clone().into()).collect::>(); - params.sort(); - params.hash(state); - } -} - -#[derive(PartialEq, Eq, Clone, Hash, Debug)] -/// Derive components from http message, which is expressed as @method, @path, @authority, etc. in @signature-params -/// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-derived-components -enum HttpDerivedComponentInner { - Method, - TargetUri, - Authority, - Scheme, - RequestTarget, - Path, - Query, - QueryParam, - Status, -} -impl AsRef for HttpDerivedComponentInner { - fn as_ref(&self) -> &str { - match self { - Self::Method => "\"@method\"", - Self::TargetUri => "\"@target-uri\"", - Self::Authority => "\"@authority\"", - Self::Scheme => "\"@scheme\"", - Self::RequestTarget => "\"@request-target\"", - Self::Path => "\"@path\"", - Self::Query => "\"@query\"", - Self::QueryParam => "\"@query-param\"", - Self::Status => "\"@status\"", - } - } -} -impl From for String { - fn from(val: HttpDerivedComponentInner) -> Self { - val.as_ref().to_string() - } -} -impl From<&str> for HttpDerivedComponentInner { - fn from(val: &str) -> Self { - match val { - "\"@method\"" => Self::Method, - "\"@target-uri\"" => Self::TargetUri, - "\"@authority\"" => Self::Authority, - "\"@scheme\"" => Self::Scheme, - "\"@request-target\"" => Self::RequestTarget, - "\"@path\"" => Self::Path, - "\"@query\"" => Self::Query, - "\"@query-param\"" => Self::QueryParam, - "\"@status\"" => Self::Status, - _ => panic!("Invalid derived component: {}", val), - } - } -} - -#[derive(PartialEq, Eq, Hash, Debug)] -/// Http derived component setting with optional parameters -struct HttpDerivedComponent { - /// derived component - component: HttpDerivedComponentInner, - /// parameters - params: HttpFieldParams, -} - -impl From<&str> for HttpDerivedComponent { - /// this feeds `"";` format, e.g., `"@method";req` - fn from(val: &str) -> Self { - let mut iter = val.split(';'); - let component = iter.next().unwrap(); - // only `req` field param is allowed for derived components unlike general http fields - let params = iter - .map(HttpFieldParam::from) - .filter(|v| matches!(v, HttpFieldParam::Req)) - .collect::>(); - Self { - component: HttpDerivedComponentInner::from(component), - params: HttpFieldParams(params), - } - } -} -impl std::fmt::Display for HttpDerivedComponent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut s: String = self.component.clone().into(); - if !self.params.0.is_empty() { - s.push(';'); - s.push_str( - &self - .params - .0 - .iter() - .map(|v| v.clone().into()) - .collect::>() - .join(";"), - ); - } - write!(f, "{}", s) - } -} - -#[derive(PartialEq, Eq, Hash, Debug)] -/// Http field setting with optional parameters -/// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-http-fields -struct HttpField { - /// field name - filed_name: String, - /// parameters - params: HttpFieldParams, -} - -impl From<&str> for HttpField { - /// this feeds `"";` format, e.g., `"example-header";bs` - fn from(val: &str) -> Self { - let mut iter = val.split(';'); - let field_name = iter.next().unwrap(); - let params = iter.map(HttpFieldParam::from).collect::>(); - Self { - filed_name: field_name.to_string(), - params: HttpFieldParams(params), - } - } -} - -impl std::fmt::Display for HttpField { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut s: String = self.filed_name.clone(); - if !self.params.0.is_empty() { - s.push(';'); - s.push_str( - &self - .params - .0 - .iter() - .map(|v| v.clone().into()) - .collect::>() - .join(";"), - ); - } - write!(f, "{}", s) - } -} - /// Struct defining Http message signature parameters /// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-signature-parameters pub struct HttpSignatureParams { @@ -225,13 +24,13 @@ pub struct HttpSignatureParams { keyid: Option, /// tag tag: Option, - /// ordered message components, i.e., string of http_fields and derived_components - message_components: Vec, + /// covered component vector string: ordered message components, i.e., string of http_fields and derived_components + covered_components: Vec, } impl std::fmt::Display for HttpSignatureParams { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut s: String = format!("({})", self.message_components.join(" ")); + let mut s: String = format!("({})", self.covered_components.join(" ")); if self.created.is_some() { s.push_str(&format!(";created={}", self.created.unwrap())); } @@ -262,7 +61,7 @@ impl TryFrom<&str> for HttpSignatureParams { anyhow::bail!("Invalid message components: {}", value); } let mut iter = value[1..].split(')'); - let message_components = iter + let covered_components = iter .next() .unwrap() .split(' ') @@ -276,7 +75,7 @@ impl TryFrom<&str> for HttpSignatureParams { alg: None, keyid: None, tag: None, - message_components, + covered_components, }; // then extract signature parameters @@ -326,11 +125,9 @@ pub struct HttpSignatureParamsBuildConfig { keyid: Option, /// tag tag: Option, - /// derived components - derived_components: HashSet, - /// http fields like `date`, `content-type`, `content-length`, etc. - /// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-http-fields - http_fields: HashSet, + /// derived components and http field component ike `date`, `content-type`, `content-length`, etc. + /// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#section-2 + covered_components: Vec, } impl Default for HttpSignatureParamsBuildConfig { @@ -345,8 +142,7 @@ impl Default for HttpSignatureParamsBuildConfig { alg: None, keyid: None, tag: None, - derived_components: HashSet::default(), - http_fields: HashSet::default(), + covered_components: vec![], } } } @@ -357,22 +153,30 @@ impl HttpSignatureParamsBuildConfig { self.keyid = Some(key.key_id().to_string()); self.alg = Some(key.alg().to_string()); } - /// Set derived components - pub fn set_derived_components(&mut self, components: &[&str]) { - self.derived_components = components.iter().map(|&c| HttpDerivedComponent::from(c)).collect(); + /// Set covered components + pub fn set_covered_message_component_ids(&mut self, components: &[&str]) { + self.covered_components = components + .iter() + .map(|&c| HttpMessageComponentIdentifier::from(c)) + .collect(); + assert!(has_unique_elements(self.covered_components.iter())) } - /// Set http fields - pub fn set_http_fields(&mut self, fields: &[&str]) { - self.http_fields = fields.iter().map(|&f| HttpField::from(f)).collect(); + /// Extend covered conmpoents + pub fn extend_covered_message_component_ids(&mut self, components: &[&str]) { + self + .covered_components + .extend(components.iter().map(|&c| HttpMessageComponentIdentifier::from(c))); + // check duplicates + assert!(has_unique_elements(self.covered_components.iter())) } + /// Derive HttpSignatureParams object pub fn derive_http_signature_params(&self) -> HttpSignatureParams { - let mut message_components = self - .derived_components + let mut covered_components = self + .covered_components .iter() .map(|c| c.to_string()) .collect::>(); - message_components.extend(self.http_fields.iter().map(|f| f.to_string()).collect::>()); let created = if self.set_created { if self.created.is_none() { Some(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()) @@ -396,7 +200,7 @@ impl HttpSignatureParamsBuildConfig { // generate 32 bytes random nonce in base64url encoding let mut rng = rand::thread_rng(); let nonce = rng.gen::<[u8; 32]>(); - Some(URL_SAFE_NO_PAD.encode(nonce)) + Some(general_purpose::URL_SAFE_NO_PAD.encode(nonce)) } else { self.nonce.clone() } @@ -411,7 +215,7 @@ impl HttpSignatureParamsBuildConfig { alg: self.alg.clone(), keyid: self.keyid.clone(), tag: self.tag.clone(), - message_components, + covered_components, } } } @@ -420,7 +224,7 @@ impl HttpSignatureParamsBuildConfig { mod tests { use super::*; use crate::crypto::PublicKey; - const EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- + const _EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIDSHAE++q1BP7T8tk+mJtS+hLf81B0o6CFyWgucDFN/C -----END PRIVATE KEY----- "##; @@ -430,55 +234,15 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= "##; const EDDSA_KEY_ID: &str = "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is"; - #[test] - fn test_derived_components() { - let params = HttpDerivedComponent::from("\"@method\";req"); - assert_eq!(params.component, HttpDerivedComponentInner::Method); - assert_eq!( - params.params.0, - vec![HttpFieldParam::Req].into_iter().collect::>() - ); - - // drops invalid field params - let params = HttpDerivedComponent::from("\"@path\";req;key=\"test\""); - assert_eq!(params.component, HttpDerivedComponentInner::Path); - assert_eq!( - params.params.0, - vec![HttpFieldParam::Req].into_iter().collect::>() - ); - } - - #[test] - fn test_http_general_field() { - let params = HttpField::from("\"example-header\";bs"); - assert_eq!(params.filed_name, "\"example-header\""); - assert_eq!( - params.params.0, - vec![HttpFieldParam::Bs].into_iter().collect::>() - ); - - // keeps field params - let params = HttpField::from("\"example-header\";bs;key=\"test\""); - assert_eq!(params.filed_name, "\"example-header\""); - assert_eq!( - params.params.0, - vec![HttpFieldParam::Bs, HttpFieldParam::Key("test".to_string())] - .into_iter() - .collect::>() - ); - } - #[test] fn test_set_key_info() { let mut params = HttpSignatureParamsBuildConfig::default(); - params.set_derived_components(&["\"@method\"", "\"@path\";bs", "\"@authority\""]); - assert_eq!( - params.derived_components, - vec!["\"@method\"", "\"@path\";bs", "\"@authority\""] - .into_iter() - .map(HttpDerivedComponent::from) - .collect::>() - ); + params.set_covered_message_component_ids(&["\"@method\"", "\"@path\";bs", "\"@authority\""]); + let x = vec!["\"@method\"", "\"@path\";bs", "\"@authority\""] + .into_iter() + .map(HttpMessageComponentIdentifier::from) + .collect::>(); + assert_eq!(params.covered_components, x); params.set_key_info(&PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap()); assert_eq!(params.keyid, Some(EDDSA_KEY_ID.to_string())); assert_eq!(params.alg, Some("ed25519".to_string())); @@ -487,8 +251,15 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[test] fn test_http_signature_params() { let mut params = HttpSignatureParamsBuildConfig::default(); - params.set_derived_components(&["\"@method\"", "\"@path\";bs", "\"@authority\"", "\"@scheme\";req"]); - params.set_http_fields(&["\"date\"", "\"content-type\";bs", "\"content-length\""]); + params.set_covered_message_component_ids(&[ + "\"@method\"", + "\"@path\";bs", + "\"@authority\"", + "\"@scheme\";req", + "\"date\"", + "\"content-type\";bs", + "\"content-length\"", + ]); params.set_key_info(&PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap()); let params = params.derive_http_signature_params(); @@ -499,21 +270,20 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert_eq!(params.keyid, Some(EDDSA_KEY_ID.to_string())); assert_eq!(params.tag, None); - assert!(params.message_components.contains(&"\"@method\"".to_string())); + assert!(params.covered_components.contains(&"\"@method\"".to_string())); // only `req` field param is allowed for derived components unlike general http fields // drops bs field param - assert!(params.message_components.contains(&"\"@path\"".to_string())); + assert!(params.covered_components.contains(&"\"@path\"".to_string())); // req remains - assert!(params.message_components.contains(&"\"@scheme\";req".to_string())); + assert!(params.covered_components.contains(&"\"@scheme\";req".to_string())); - assert!(params.message_components.contains(&"\"@authority\"".to_string())); - assert!(params.message_components.contains(&"\"date\"".to_string())); - assert!(params.message_components.contains(&"\"content-type\";bs".to_string())); - assert!(params.message_components.contains(&"\"content-length\"".to_string())); + assert!(params.covered_components.contains(&"\"@authority\"".to_string())); + assert!(params.covered_components.contains(&"\"date\"".to_string())); + assert!(params.covered_components.contains(&"\"content-type\";bs".to_string())); + assert!(params.covered_components.contains(&"\"content-length\"".to_string())); - println!("{}", params); + // println!("{}", params); } - #[test] fn test_from_string_signature_params() { let value = r##"("@method" "@path" "@scheme";req "@authority" "content-type";bs "date" "content-length");created=1704972031;alg="ed25519";keyid="gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is""##; @@ -527,7 +297,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert_eq!(params.keyid, Some(EDDSA_KEY_ID.to_string())); assert_eq!(params.tag, None); assert_eq!( - params.message_components, + params.covered_components, vec![ "\"@method\"", "\"@path\"", @@ -554,6 +324,6 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert_eq!(params.alg, Some("ed25519".to_string())); assert_eq!(params.keyid, Some(EDDSA_KEY_ID.to_string())); assert_eq!(params.tag, None); - assert!(params.message_components.is_empty()); + assert!(params.covered_components.is_empty()); } } diff --git a/src/signer.rs b/src/signer.rs index e69de29..9d0ef7b 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -0,0 +1,10 @@ +// API design: +// let signature_params = SignatureParamsBuilder::default() +// .created(1618884473) +// .key_id("test-key-ed25519") // Should key_id be set at signer builder? +// .headers(vec![...]) +// .build(); +// let signer = HttpSignatureSignerBuilder::default() +// .secret_key(SecretKey::HmacSha256(SymmetricKey::from(b"secret"))) +// .signature_params(signature_params) +// .build(); diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..b95f7b6 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,9 @@ +/// Check duplicate elements in a vector +pub(crate) fn has_unique_elements(iter: T) -> bool +where + T: IntoIterator, + T::Item: Eq + std::hash::Hash, +{ + let mut uniq = rustc_hash::FxHashSet::default(); + iter.into_iter().all(move |x| uniq.insert(x)) +}