From 1b23161a65e42b5f845f45840bd5a3008ebcb0cf Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Wed, 24 Jan 2024 19:32:39 +0900 Subject: [PATCH] wip: signature base design --- Cargo.toml | 3 ++ src/lib.rs | 2 +- src/message_component.rs | 41 ++++++++++++++++++---- src/signature_base.rs | 76 ++++++++++++++++++++++++++++++++++++++-- src/signature_params.rs | 61 ++++++++++++++++++++------------ 5 files changed, 151 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78dc344..e23e345 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,6 @@ sha2 = { version = "0.10.8", default-features = false } # encoding base64 = { version = "0.21.6" } + +# # for request and response headers +# http = "1.0.0" diff --git a/src/lib.rs b/src/lib.rs index d320039..705b79d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ mod util; use crate::{ crypto::{PublicKey, SecretKey, SigningKey, VerifyingKey}, - signature_params::{HttpSignatureParams, HttpSignatureParamsBuildConfig}, + signature_params::{HttpSignatureParams, HttpSignatureParamsBuilder}, }; #[cfg(test)] diff --git a/src/message_component.rs b/src/message_component.rs index 8698f55..5070fc7 100644 --- a/src/message_component.rs +++ b/src/message_component.rs @@ -1,12 +1,13 @@ use rustc_hash::FxHashSet as HashSet; /* ---------------------------------------------------------------- */ +#[derive(Debug, Clone)] /// Http message component pub(crate) struct HttpMessageComponent { /// Http message component identifier - id: HttpMessageComponentIdentifier, + pub(crate) id: HttpMessageComponentIdentifier, /// Http message component value - value: HttpMessageComponentValue, + pub(crate) value: HttpMessageComponentValue, } impl TryFrom<&str> for HttpMessageComponent { @@ -15,13 +16,38 @@ impl TryFrom<&str> for HttpMessageComponent { let Some((id, value)) = val.split_once(':') else { return Err(anyhow::anyhow!("Invalid http message component: {}", val)); }; + let id = id.trim(); + if ensure_component_id(id).is_err() { + return Err(anyhow::anyhow!("Invalid http message component id: {}", id)); + } Ok(Self { - id: HttpMessageComponentIdentifier::from(id.trim()), + id: HttpMessageComponentIdentifier::from(id), value: HttpMessageComponentValue::from(value.trim()), }) } } +impl TryFrom<(&str, &str)> for HttpMessageComponent { + type Error = anyhow::Error; + fn try_from((k, v): (&str, &str)) -> std::result::Result { + let id = k.trim(); + if ensure_component_id(id).is_err() { + return Err(anyhow::anyhow!("Invalid http message component id: {}", id)); + } + Ok(Self { + id: HttpMessageComponentIdentifier::from(id), + value: HttpMessageComponentValue::from(v.trim()), + }) + } +} + +fn ensure_component_id(id: &str) -> anyhow::Result<()> { + if !id.starts_with('"') || !(id.ends_with('"') || id[1..].contains("\";")) { + return Err(anyhow::anyhow!("Invalid http message component id: {}", id)); + } + Ok(()) +} + 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 @@ -31,6 +57,7 @@ impl std::fmt::Display for HttpMessageComponent { } /* ---------------------------------------------------------------- */ +#[derive(Debug, Clone)] /// Http message component value pub(crate) struct HttpMessageComponentValue { /// inner value originally from http message header or derived from http message @@ -50,7 +77,7 @@ impl std::fmt::Display for HttpMessageComponentValue { } /* ---------------------------------------------------------------- */ -#[derive(PartialEq, Eq, Hash, Debug)] +#[derive(PartialEq, Eq, Hash, Debug, Clone)] /// Http message component identifier pub(crate) enum HttpMessageComponentIdentifier { /// Http field component @@ -125,7 +152,7 @@ impl From<&str> for HttpMessageComponentParam { } } -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Clone)] pub(crate) struct HttpMessageComponentParams(pub(crate) HashSet); impl std::hash::Hash for HttpMessageComponentParams { fn hash(&self, state: &mut H) { @@ -187,7 +214,7 @@ impl From<&str> for DerivedComponentName { } } -#[derive(PartialEq, Eq, Hash, Debug)] +#[derive(PartialEq, Eq, Hash, Debug, Clone)] /// Http derived component setting with optional parameters pub(crate) struct DerivedComponentId { /// derived component @@ -232,7 +259,7 @@ impl std::fmt::Display for DerivedComponentId { } /* ---------------------------------------------------------------- */ -#[derive(PartialEq, Eq, Hash, Debug)] +#[derive(PartialEq, Eq, Hash, Debug, Clone)] /// 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 { diff --git a/src/signature_base.rs b/src/signature_base.rs index d94f274..4646f0c 100644 --- a/src/signature_base.rs +++ b/src/signature_base.rs @@ -9,6 +9,78 @@ struct SignatureBase { signature_params: HttpSignatureParams, } -// creating signature base from http header lines and signature params builder config +impl SignatureBase { + pub(crate) fn try_new( + component_lines: &Vec, + signature_params: &HttpSignatureParams, + ) -> anyhow::Result { + // check if the order of component lines is the same as the order of covered message component ids + if component_lines.len() != signature_params.covered_components.len() { + anyhow::bail!("The number of component lines is not the same as the number of covered message component ids"); + } -// creating signature base from http header lines including signature params + let assertion = component_lines + .iter() + .zip(signature_params.covered_components.iter()) + .all(|(component_line, covered_component_id)| { + let component_line_id_string = &component_line.id.to_string(); + component_line_id_string == covered_component_id + }); + if !assertion { + anyhow::bail!("The order of component lines is not the same as the order of covered message component ids"); + } + + Ok(Self { + component_lines: component_lines.clone(), + signature_params: signature_params.clone(), + }) + } +} + +impl std::fmt::Display for SignatureBase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut signature_base = String::new(); + for component_line in &self.component_lines { + signature_base.push_str(&component_line.to_string()); + signature_base.push('\n'); + } + signature_base.push_str(&format!("\"@signature-params\": {}", self.signature_params.to_string())); + write!(f, "{}", signature_base) + } +} + +// TODO: creating signature base from http header lines and signature params builder config + +// TODO: creating signature base from http header lines including signature params + +#[cfg(test)] +mod test { + use super::*; + use crate::signature_params::HttpSignatureParamsBuilder; + + #[test] + fn test_signature_base() { + let mut signature_params = HttpSignatureParamsBuilder::default(); + signature_params.created(1706091731); + signature_params.key_id("key_id"); + signature_params.algorithm("rsa-sha256"); + signature_params.covered_message_component_ids(&["\"@method\";req", "\"date\"", "\"content-digest\""]); + let signature_params = signature_params.build(); + + let component_lines = vec![ + HttpMessageComponent::try_from(("\"@method\";req", "GET")).unwrap(), + HttpMessageComponent::try_from(("\"date\"", "Tue, 07 Jun 2014 20:51:35 GMT")).unwrap(), + HttpMessageComponent::try_from(( + "\"content-digest\"", + "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", + )) + .unwrap(), + ]; + let signature_base = SignatureBase::try_new(&component_lines, &signature_params).unwrap(); + let test_string = r##""@method";req: GET +"date": Tue, 07 Jun 2014 20:51:35 GMT +"content-digest": SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= +"@signature-params": ("@method";req "date" "content-digest");created=1706091731;alg="rsa-sha256";keyid="key_id""##; + assert_eq!(signature_base.to_string(), test_string); + } +} diff --git a/src/signature_params.rs b/src/signature_params.rs index 84081c3..3c8070a 100644 --- a/src/signature_params.rs +++ b/src/signature_params.rs @@ -5,23 +5,24 @@ use std::time::{SystemTime, UNIX_EPOCH}; const DEFAULT_EXPIRES_IN: u64 = 300; +#[derive(Debug, Clone)] /// 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 { /// created unix timestamp. - created: Option, + pub(crate) created: Option, /// signature expires unix timestamp. - expires: Option, + pub(crate) expires: Option, /// nonce - nonce: Option, + pub(crate) nonce: Option, /// algorithm name - alg: Option, + pub(crate) alg: Option, /// key id. - keyid: Option, + pub(crate) keyid: Option, /// tag - tag: Option, + pub(crate) tag: Option, /// covered component vector string: ordered message components, i.e., string of http_fields and derived_components - covered_components: Vec, + pub(crate) covered_components: Vec, } impl std::fmt::Display for HttpSignatureParams { @@ -99,9 +100,9 @@ impl TryFrom<&str> for HttpSignatureParams { } } -/// Configuration for Http message signature parameters +/// Builder of Http message signature parameters /// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html#name-signature-parameters -pub struct HttpSignatureParamsBuildConfig { +pub struct HttpSignatureParamsBuilder { /// created unix timestamp. if none and set_created = false, use current timestamp created: Option, /// if true, `created` is set. @@ -126,7 +127,7 @@ pub struct HttpSignatureParamsBuildConfig { covered_components: Vec, } -impl Default for HttpSignatureParamsBuildConfig { +impl Default for HttpSignatureParamsBuilder { fn default() -> Self { Self { created: None, @@ -143,14 +144,30 @@ impl Default for HttpSignatureParamsBuildConfig { } } -impl HttpSignatureParamsBuildConfig { +impl HttpSignatureParamsBuilder { + /// Set key id + pub fn key_id(&mut self, keyid: &str) -> &mut Self { + self.keyid = Some(keyid.to_string()); + self + } + /// set algorithm + pub fn algorithm(&mut self, alg: &str) -> &mut Self { + self.alg = Some(alg.to_string()); + self + } /// Set keyid and alg from the key - pub fn set_key_info(&mut self, key: &impl VerifyingKey) { + pub fn key_info(&mut self, key: &impl VerifyingKey) { self.keyid = Some(key.key_id().to_string()); self.alg = Some(key.alg().to_string()); } + /// Set artificial created + pub fn created(&mut self, created: u64) -> &mut Self { + self.created = Some(created); + self.set_created = true; + self + } /// Set covered components - pub fn set_covered_message_component_ids(&mut self, components: &[&str]) { + pub fn covered_message_component_ids(&mut self, components: &[&str]) { self.covered_components = components .iter() .map(|&c| HttpMessageComponentIdentifier::from(c)) @@ -167,8 +184,8 @@ impl HttpSignatureParamsBuildConfig { } /// Derive HttpSignatureParams object - pub fn derive_http_signature_params(&self) -> HttpSignatureParams { - let mut covered_components = self + pub fn build(&self) -> HttpSignatureParams { + let covered_components = self .covered_components .iter() .map(|c| c.to_string()) @@ -232,22 +249,22 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[test] fn test_set_key_info() { - let mut params = HttpSignatureParamsBuildConfig::default(); - params.set_covered_message_component_ids(&["\"@method\"", "\"@path\";bs", "\"@authority\""]); + let mut params = HttpSignatureParamsBuilder::default(); + params.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()); + params.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())); } #[test] fn test_http_signature_params() { - let mut params = HttpSignatureParamsBuildConfig::default(); - params.set_covered_message_component_ids(&[ + let mut params = HttpSignatureParamsBuilder::default(); + params.covered_message_component_ids(&[ "\"@method\"", "\"@path\";bs", "\"@authority\"", @@ -256,9 +273,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= "\"content-type\";bs", "\"content-length\"", ]); - params.set_key_info(&PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap()); + params.key_info(&PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap()); - let params = params.derive_http_signature_params(); + let params = params.build(); assert!(params.created.is_some()); assert!(params.expires.is_none()); assert!(params.nonce.is_none());