From f6db2034197890b7a3f1e7e6a5558b06f8a1c87c Mon Sep 17 00:00:00 2001 From: Rinat Shigapov Date: Wed, 20 May 2026 20:25:56 +0800 Subject: [PATCH 1/5] perf: use static strings for static error messages --- httpsig-hyper/src/error.rs | 8 +- httpsig-hyper/src/hyper_http.rs | 46 ++++---- httpsig-hyper/src/hyper_http_tests.rs | 2 +- httpsig/src/error.rs | 8 +- httpsig/src/lib.rs | 6 +- httpsig/src/message_component/component.rs | 104 ++++++++++-------- .../src/message_component/component_value.rs | 20 ++-- httpsig/src/signature_base.rs | 21 ++-- httpsig/src/signature_params.rs | 12 +- 9 files changed, 117 insertions(+), 110 deletions(-) diff --git a/httpsig-hyper/src/error.rs b/httpsig-hyper/src/error.rs index 23a5e7b..4b12f16 100644 --- a/httpsig-hyper/src/error.rs +++ b/httpsig-hyper/src/error.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use httpsig::prelude::HttpSigError; use thiserror::Error; @@ -9,7 +11,7 @@ pub type HyperSigResult = std::result::Result; pub enum HyperSigError { /// No signature headers found #[error("No signature headers found: {0}")] - NoSignatureHeaders(String), + NoSignatureHeaders(&'static str), /// Failed to parse signature headers #[error("Failed to stringify signature headers: {0}")] @@ -21,7 +23,7 @@ pub enum HyperSigError { /// Invalid component name #[error("Invalid component name: {0}")] - InvalidComponentName(String), + InvalidComponentName(Cow<'static, str>), /// Invalid component param #[error("Invalid component param: {0}")] @@ -29,7 +31,7 @@ pub enum HyperSigError { /// Invalid signature #[error("Invalid signature: {0}")] - InvalidSignature(String), + InvalidSignature(&'static str), /// Inherited from HttpSigError #[error("HttpSigError: {0}")] diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 9f1a6c0..c1e0cc3 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -337,7 +337,7 @@ where { if !self.has_message_signature() { return Err(HyperSigError::NoSignatureHeaders( - "The request does not have signature and signature-input headers".to_string(), + "The request does not have signature and signature-input headers", )); } let map_signature_with_base = self.extract_signatures()?; @@ -468,7 +468,7 @@ where { if !self.has_message_signature() { return Err(HyperSigError::NoSignatureHeaders( - "The response does not have signature and signature-input headers".to_string(), + "The response does not have signature and signature-input headers", )); } let map_signature_with_base = self.extract_signatures(req_for_param)?; @@ -615,8 +615,8 @@ fn get_alg_key_ids_inner( let alg = headers .signature_params() .alg - .clone() - .map(|a| AlgorithmName::from_str(&a)) + .as_ref() + .map(|a| AlgorithmName::from_str(a)) .transpose() .ok() .flatten(); @@ -646,11 +646,11 @@ fn extract_signatures_inner( ) -> HyperSigResult> { let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; let extracted = signature_headers_map - .iter() + .into_iter() .filter_map(|(name, headers)| { build_signature_base(req_or_res, headers.signature_params(), req_for_param) .ok() - .map(|base| (name.clone(), (base, headers.clone()))) + .map(|base| (name, (base, headers))) }) .collect(); Ok(extracted) @@ -679,7 +679,7 @@ where async move { if filtered.is_empty() { return Err(HyperSigError::NoSignatureHeaders( - "No signature as appropriate target for verification".to_string(), + "No signature as appropriate target for verification", )); } // check if any one of the signature headers is valid @@ -690,9 +690,7 @@ where if !successful_sig_names.is_empty() { Ok(successful_sig_names.first().unwrap().clone()) } else { - Err(HyperSigError::InvalidSignature( - "Invalid signature for the verifying key".to_string(), - )) + Err(HyperSigError::InvalidSignature("Invalid signature for the verifying key")) } } }); @@ -712,16 +710,14 @@ impl RequestOrResponse<'_, B> { fn method(&self) -> HyperSigResult<&http::Method> { match self { RequestOrResponse::Request(req) => Ok(req.method()), - _ => Err(HyperSigError::InvalidComponentName( - "`method` is only for request".to_string(), - )), + _ => Err(HyperSigError::InvalidComponentName("`method` is only for request".into())), } } fn uri(&self) -> HyperSigResult<&http::Uri> { match self { RequestOrResponse::Request(req) => Ok(req.uri()), - _ => Err(HyperSigError::InvalidComponentName("`uri` is only for request".to_string())), + _ => Err(HyperSigError::InvalidComponentName("`uri` is only for request".into())), } } @@ -735,9 +731,7 @@ impl RequestOrResponse<'_, B> { fn status(&self) -> HyperSigResult { match self { RequestOrResponse::Response(res) => Ok(res.status()), - _ => Err(HyperSigError::InvalidComponentName( - "`status` is only for response".to_string(), - )), + _ => Err(HyperSigError::InvalidComponentName("`status` is only for response".into())), } } } @@ -747,7 +741,7 @@ fn extract_signature_headers_with_name(req_or_res: &RequestOrResponse) -> let headers = req_or_res.headers(); if !(headers.contains_key("signature-input") && headers.contains_key("signature")) { return Err(HyperSigError::NoSignatureHeaders( - "The request does not have signature and signature-input headers".to_string(), + "The request does not have signature and signature-input headers", )); }; @@ -800,14 +794,14 @@ fn build_signature_base( }) .collect::, _>>()?; - HttpSignatureBase::try_new(&component_lines, signature_params).map_err(|e| e.into()) + HttpSignatureBase::try_new(component_lines, signature_params).map_err(|e| e.into()) } /// Extract http field from hyper http request/response fn extract_http_field(req_or_res: &RequestOrResponse, id: &HttpMessageComponentId) -> HyperSigResult { let HttpMessageComponentName::HttpField(header_name) = &id.name else { return Err(HyperSigError::InvalidComponentName( - "invalid http message component name as http field".to_string(), + "invalid http message component name as http field".into(), )); }; let headers = match req_or_res { @@ -821,7 +815,7 @@ fn extract_http_field(req_or_res: &RequestOrResponse, id: &HttpMessageComp .map(|v| v.to_str().map(|s| s.to_owned())) .collect::, _>>()?; - HttpMessageComponent::try_from((id, field_values.as_slice())).map_err(|e| e.into()) + HttpMessageComponent::try_from((id, field_values)).map_err(|e| e.into()) } /// Extract derived component from hyper http request/response @@ -831,7 +825,7 @@ fn extract_derived_component( ) -> HyperSigResult { let HttpMessageComponentName::Derived(derived_id) = &id.name else { return Err(HyperSigError::InvalidComponentName( - "invalid http message component name as derived component".to_string(), + "invalid http message component name as derived component".into(), )); }; // Validate parameters allowed on derived components (RFC 9421). @@ -860,9 +854,7 @@ fn extract_derived_component( match req_or_res { RequestOrResponse::Request(_) => { if matches!(derived_id, DerivedComponentName::Status) { - return Err(HyperSigError::InvalidComponentName( - "`status` is only for response".to_string(), - )); + return Err(HyperSigError::InvalidComponentName("`status` is only for response".into())); } } RequestOrResponse::Response(_) => { @@ -874,7 +866,7 @@ fn extract_derived_component( && !has_req { return Err(HyperSigError::InvalidComponentName( - "derived components other than `@status` and `@signature-params` require `req` parameter for response".to_string(), + "derived components other than `@status` and `@signature-params` require `req` parameter for response".into(), )); } // `@status` must not have `req` parameter @@ -924,7 +916,7 @@ fn extract_derived_component( .collect::>(), }; - HttpMessageComponent::try_from((id, field_values.as_slice())).map_err(|e| e.into()) + HttpMessageComponent::try_from((id, field_values)).map_err(|e| e.into()) } /* --------------------------------------- */ diff --git a/httpsig-hyper/src/hyper_http_tests.rs b/httpsig-hyper/src/hyper_http_tests.rs index ef98f39..196838c 100644 --- a/httpsig-hyper/src/hyper_http_tests.rs +++ b/httpsig-hyper/src/hyper_http_tests.rs @@ -116,7 +116,7 @@ async fn test_extract_signature_params_from_request() { let component = extract_http_message_component(&req_or_res, &component_id).unwrap(); assert_eq!(component.to_string(), "\"@signature-params\": (\"@method\" \"@authority\")"); assert_eq!(component.value.to_string(), r##"("@method" "@authority")"##); - assert_eq!(component.value.as_field_value(), r##"sig1=("@method" "@authority")"##); + assert_eq!(component.value.to_field_value(), r##"sig1=("@method" "@authority")"##); assert_eq!(component.value.as_component_value(), r##"("@method" "@authority")"##); assert_eq!(component.value.key(), Some("sig1")); } diff --git a/httpsig/src/error.rs b/httpsig/src/error.rs index 0a342d0..33272e7 100644 --- a/httpsig/src/error.rs +++ b/httpsig/src/error.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use thiserror::Error; /// Result type for http signature @@ -38,16 +40,16 @@ pub enum HttpSigError { InvalidComponentId(String), /// Invalid http message component #[error("Invalid http message component: {0}")] - InvalidComponent(String), + InvalidComponent(Cow<'static, str>), /* ----- Signature params errors ----- */ /// Invalid signature params #[error("Invalid signature params: {0}")] - InvalidSignatureParams(String), + InvalidSignatureParams(&'static str), /// Error in building signature header #[error("Failed to build signature header: {0}")] - BuildSignatureHeaderError(String), + BuildSignatureHeaderError(&'static str), /// Error in building signature base #[error("Failed to build signature base: {0}")] diff --git a/httpsig/src/lib.rs b/httpsig/src/lib.rs index dcc69e7..e5f1b0c 100644 --- a/httpsig/src/lib.rs +++ b/httpsig/src/lib.rs @@ -122,7 +122,7 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw .map(|&line| message_component::HttpMessageComponent::try_from(line).unwrap()) .collect::>(); - let signature_base = HttpSignatureBase::try_new(&component_lines, &signature_params).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines, &signature_params).unwrap(); let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); @@ -140,7 +140,7 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw // sender let signature_params = HttpSignatureParams::try_from(SIGNATURE_PARAMS).unwrap(); - let signature_base = HttpSignatureBase::try_new(&component_lines, &signature_params).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines.clone(), &signature_params).unwrap(); let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let signature_headers = signature_base.build_signature_headers(&sk, Some("sig-b26")).unwrap(); let signature_params_header_string = signature_headers.signature_input_header_value(); @@ -153,7 +153,7 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw let header_map = HttpSignatureHeaders::try_parse(&signature_header_string, &signature_params_header_string).unwrap(); let received_signature_headers = header_map.get("sig-b26").unwrap(); let received_signature_base = - HttpSignatureBase::try_new(&component_lines, received_signature_headers.signature_params()).unwrap(); + HttpSignatureBase::try_new(component_lines, received_signature_headers.signature_params()).unwrap(); let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_result = received_signature_base.verify_signature_headers(&pk, received_signature_headers); assert!(verification_result.is_ok()); diff --git a/httpsig/src/message_component/component.rs b/httpsig/src/message_component/component.rs index b6a724c..d5d1d02 100644 --- a/httpsig/src/message_component/component.rs +++ b/httpsig/src/message_component/component.rs @@ -25,9 +25,9 @@ impl TryFrom<&str> for HttpMessageComponent { /// We suppose that the value was correctly serialized as a line of signature base. fn try_from(val: &str) -> Result { let Some((id, value)) = val.split_once(':') else { - return Err(HttpSigError::InvalidComponent(format!( - "Invalid http message component: {val}" - ))); + return Err(HttpSigError::InvalidComponent( + format!("Invalid http message component: {val}").into(), + )); }; let id = id.trim(); @@ -40,16 +40,16 @@ impl TryFrom<&str> for HttpMessageComponent { Ok(Self { id: HttpMessageComponentId::try_from(id)?, - value: HttpMessageComponentValue::from(value.trim()), + value: HttpMessageComponentValue::from(value.trim().to_owned()), }) } } -impl TryFrom<(&HttpMessageComponentId, &[String])> for HttpMessageComponent { +impl TryFrom<(&HttpMessageComponentId, Vec)> for HttpMessageComponent { type Error = HttpSigError; /// Build http message component from given id and its associated field values - fn try_from((id, field_values): (&HttpMessageComponentId, &[String])) -> Result { + fn try_from((id, field_values): (&HttpMessageComponentId, Vec)) -> Result { match &id.name { HttpMessageComponentName::HttpField(_) => build_http_field_component(id, field_values), HttpMessageComponentName::Derived(_) => build_derived_component(id, field_values), @@ -69,16 +69,16 @@ impl std::fmt::Display for HttpMessageComponent { /// Build derived component from given id and its associated field values pub(super) fn build_derived_component( id: &HttpMessageComponentId, - field_values: &[String], + field_values: Vec, ) -> HttpSigResult { let HttpMessageComponentName::Derived(derived_id) = &id.name else { return Err(HttpSigError::InvalidComponent( - "invalid http message component name as derived component".to_string(), + "invalid http message component name as derived component".into(), )); }; if field_values.is_empty() { return Err(HttpSigError::InvalidComponent( - "derived component requires field values".to_string(), + "derived component requires field values".into(), )); } // ensure only `req` and `name` are allowed for derived component parameters @@ -89,19 +89,23 @@ pub(super) fn build_derived_component( .all(|p| matches!(p, HttpMessageComponentParam::Req | HttpMessageComponentParam::Name(_))) { return Err(HttpSigError::InvalidComponent( - "invalid parameter for derived component".to_string(), + "invalid parameter for derived component".into(), )); } + fn first(field_values: Vec) -> String { + field_values.into_iter().next().expect("not empty") + } + let value = match derived_id { - DerivedComponentName::Method => HttpMessageComponentValue::from(field_values[0].to_ascii_uppercase().as_ref()), - DerivedComponentName::TargetUri => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()), - DerivedComponentName::Authority => HttpMessageComponentValue::from(field_values[0].to_ascii_lowercase().as_ref()), - DerivedComponentName::Scheme => HttpMessageComponentValue::from(field_values[0].to_ascii_lowercase().as_ref()), - DerivedComponentName::RequestTarget => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()), - DerivedComponentName::Path => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()), - DerivedComponentName::Query => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()), - DerivedComponentName::Status => HttpMessageComponentValue::from(field_values[0].to_string().as_ref()), + DerivedComponentName::Method => HttpMessageComponentValue::from(field_values[0].to_ascii_uppercase()), + DerivedComponentName::TargetUri => HttpMessageComponentValue::from(first(field_values)), + DerivedComponentName::Authority => HttpMessageComponentValue::from(field_values[0].to_ascii_lowercase()), + DerivedComponentName::Scheme => HttpMessageComponentValue::from(field_values[0].to_ascii_lowercase()), + DerivedComponentName::RequestTarget => HttpMessageComponentValue::from(first(field_values)), + DerivedComponentName::Path => HttpMessageComponentValue::from(first(field_values)), + DerivedComponentName::Query => HttpMessageComponentValue::from(first(field_values)), + DerivedComponentName::Status => HttpMessageComponentValue::from(first(field_values)), DerivedComponentName::QueryParam => { let name = id.params.0.iter().find_map(|p| match p { HttpMessageComponentParam::Name(name) => Some(name), @@ -109,7 +113,7 @@ pub(super) fn build_derived_component( }); if name.is_none() { return Err(HttpSigError::InvalidComponent( - "query-param derived component requires name parameter".to_string(), + "query-param derived component requires name parameter".into(), )); }; let name = name.unwrap(); @@ -120,18 +124,18 @@ pub(super) fn build_derived_component( .filter(|(k, _)| *k == name.as_str()) .map(|(_, v)| v) .collect::>(); - HttpMessageComponentValue::from(kvs.join(", ").as_ref()) + HttpMessageComponentValue::from(kvs.join(", ")) } DerivedComponentName::SignatureParams => { - let value = field_values[0].to_string(); + let value = first(field_values); let opt_pair = value.trim().split_once('='); if opt_pair.is_none() { return Err(HttpSigError::InvalidComponent( - "invalid signature-params derived component".to_string(), + "invalid signature-params derived component".into(), )); } let (key, value) = opt_pair.unwrap(); - HttpMessageComponentValue::from((key, value)) + HttpMessageComponentValue::from((key.to_owned(), value.to_owned())) } }; let component = HttpMessageComponent { id: id.clone(), value }; @@ -143,9 +147,8 @@ pub(super) fn build_derived_component( /// NOTE: field_value must be ones of request for `req` param pub(super) fn build_http_field_component( id: &HttpMessageComponentId, - field_values: &[String], + mut field_values: Vec, ) -> HttpSigResult { - let mut field_values = field_values.to_vec(); let params = &id.params; for p in params.0.iter() { @@ -154,7 +157,7 @@ pub(super) fn build_http_field_component( handle_params_sf(&mut field_values)?; } HttpMessageComponentParam::Key(key) => { - field_values = handle_params_key_into(&field_values, key)?; + field_values = handle_params_key_into(field_values.as_ref(), key)?; } HttpMessageComponentParam::Bs => { return Err(HttpSigError::NotYetImplemented("`bs` is not supported yet".to_string())); @@ -177,7 +180,7 @@ pub(super) fn build_http_field_component( let component = HttpMessageComponent { id: id.clone(), - value: HttpMessageComponentValue::from(field_values_str.as_ref()), + value: HttpMessageComponentValue::from(field_values_str), }; Ok(component) } @@ -209,7 +212,7 @@ mod tests { } else { assert!(!comp.id.params.0.is_empty()); } - assert_eq!(comp.value.as_field_value(), value); + assert_eq!(comp.value.to_field_value(), value); assert_eq!(comp.value.key(), None); assert_eq!(comp.to_string(), format!("{}: {}", id, value)); } @@ -224,7 +227,7 @@ mod tests { comp.id.params.0.get(&HttpMessageComponentParam::Name("key".to_string())), Some(&HttpMessageComponentParam::Name("key".to_string())) ); - assert_eq!(comp.value.as_field_value(), value); + assert_eq!(comp.value.to_field_value(), value); assert_eq!(comp.value.key(), None); assert_eq!(comp.to_string(), format!("{}: {}", id, value)); } @@ -245,7 +248,7 @@ mod tests { } else { assert!(!comp.id.params.0.is_empty()); } - assert_eq!(comp.value.as_field_value(), value); + assert_eq!(comp.value.to_field_value(), value); assert_eq!(comp.to_string(), format!("{}: {}", id, value)); } } @@ -299,20 +302,23 @@ mod tests { fn test_build_http_field_component() { let id = HttpMessageComponentId::try_from("content-type").unwrap(); let field_values = vec!["application/json".to_owned()]; - let component = build_http_field_component(&id, &field_values).unwrap(); + let component = build_http_field_component(&id, field_values).unwrap(); assert_eq!(component.id, id); - assert_eq!(component.value, HttpMessageComponentValue::from("application/json")); + assert_eq!( + component.value, + HttpMessageComponentValue::from("application/json".to_owned()) + ); assert_eq!(component.to_string(), "\"content-type\": application/json"); } #[test] fn test_build_http_field_component_multiple_values() { let id = HttpMessageComponentId::try_from("\"content-type\"").unwrap(); let field_values = vec!["application/json".to_owned(), "application/json-patch+json".to_owned()]; - let component = build_http_field_component(&id, &field_values).unwrap(); + let component = build_http_field_component(&id, field_values).unwrap(); assert_eq!(component.id, id); assert_eq!( component.value, - HttpMessageComponentValue::from("application/json, application/json-patch+json") + HttpMessageComponentValue::from("application/json, application/json-patch+json".to_owned()) ); assert_eq!( component.to_string(), @@ -326,11 +332,11 @@ mod tests { "application/json; patched=true".to_owned(), "application/json-patch+json;patched".to_owned(), ]; - let component = build_http_field_component(&id, &field_values).unwrap(); + let component = build_http_field_component(&id, field_values).unwrap(); assert_eq!(component.id, id); assert_eq!( component.value, - HttpMessageComponentValue::from("application/json;patched=true, application/json-patch+json;patched") + HttpMessageComponentValue::from("application/json;patched=true, application/json-patch+json;patched".to_owned()) ); assert_eq!( component.to_string(), @@ -341,9 +347,9 @@ mod tests { fn test_build_http_field_component_key() { let id = HttpMessageComponentId::try_from("\"example-header\";key=\"patched\"").unwrap(); let field_values = vec!["patched=12345678".to_owned()]; - let component = build_http_field_component(&id, &field_values).unwrap(); + let component = build_http_field_component(&id, field_values).unwrap(); assert_eq!(component.id, id); - assert_eq!(component.value, HttpMessageComponentValue::from("12345678")); + assert_eq!(component.value, HttpMessageComponentValue::from("12345678".to_string())); assert_eq!(component.to_string(), "\"example-header\";key=\"patched\": 12345678"); } #[test] @@ -354,9 +360,12 @@ mod tests { "patched=87654321".to_owned(), "not-patched=12345678".to_owned(), ]; - let component = build_http_field_component(&id, &field_values).unwrap(); + let component = build_http_field_component(&id, field_values).unwrap(); assert_eq!(component.id, id); - assert_eq!(component.value, HttpMessageComponentValue::from("12345678, 87654321")); + assert_eq!( + component.value, + HttpMessageComponentValue::from("12345678, 87654321".to_string()) + ); assert_eq!( component.to_string(), "\"example-header\";key=\"patched\": 12345678, 87654321" @@ -367,16 +376,19 @@ mod tests { fn test_build_derived_component() { let id = HttpMessageComponentId::try_from("@method").unwrap(); let field_values = vec!["GET".to_owned()]; - let component = build_derived_component(&id, &field_values).unwrap(); + let component = build_derived_component(&id, field_values).unwrap(); assert_eq!(component.id, id); - assert_eq!(component.value, HttpMessageComponentValue::from("GET")); + assert_eq!(component.value, HttpMessageComponentValue::from("GET".to_owned())); assert_eq!(component.to_string(), "\"@method\": GET"); let id = HttpMessageComponentId::try_from("@target-uri").unwrap(); let field_values = vec!["https://example.com/foo".to_owned()]; - let component = build_derived_component(&id, &field_values).unwrap(); + let component = build_derived_component(&id, field_values).unwrap(); assert_eq!(component.id, id); - assert_eq!(component.value, HttpMessageComponentValue::from("https://example.com/foo")); + assert_eq!( + component.value, + HttpMessageComponentValue::from("https://example.com/foo".to_owned()) + ); assert_eq!(component.to_string(), "\"@target-uri\": https://example.com/foo"); } #[test] @@ -384,11 +396,11 @@ mod tests { let id = HttpMessageComponentId::try_from("\"@query-param\";name=\"var\"").unwrap(); let query_param = "var=this%20is%20a%20big%0Amultiline%20value&bar=with+plus+whitespace&fa%C3%A7ade%22%3A%20=something&ok"; let field_values = query_param.split('&').map(|v| v.to_owned()).collect::>(); - let component = build_derived_component(&id, &field_values).unwrap(); + let component = build_derived_component(&id, field_values).unwrap(); assert_eq!(component.id, id); assert_eq!( component.value, - HttpMessageComponentValue::from("this%20is%20a%20big%0Amultiline%20value") + HttpMessageComponentValue::from("this%20is%20a%20big%0Amultiline%20value".to_owned()) ); assert_eq!( component.to_string(), diff --git a/httpsig/src/message_component/component_value.rs b/httpsig/src/message_component/component_value.rs index e39cdef..9e260ca 100644 --- a/httpsig/src/message_component/component_value.rs +++ b/httpsig/src/message_component/component_value.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + /* ---------------------------------------------------------------- */ #[derive(Debug, Clone, PartialEq, Eq)] /// Http message component value @@ -6,18 +8,18 @@ pub struct HttpMessageComponentValue { inner: HttpMessageComponentValueInner, } -impl From<&str> for HttpMessageComponentValue { - fn from(val: &str) -> Self { +impl From for HttpMessageComponentValue { + fn from(val: String) -> Self { Self { - inner: HttpMessageComponentValueInner::String(val.to_string()), + inner: HttpMessageComponentValueInner::String(val), } } } -impl From<(&str, &str)> for HttpMessageComponentValue { - fn from((key, val): (&str, &str)) -> Self { +impl From<(String, String)> for HttpMessageComponentValue { + fn from((key, val): (String, String)) -> Self { Self { - inner: HttpMessageComponentValueInner::KeyValue((key.to_string(), val.to_string())), + inner: HttpMessageComponentValueInner::KeyValue((key, val)), } } } @@ -55,10 +57,10 @@ impl HttpMessageComponentValue { } } /// Get key value connected with `=`, or just value - pub fn as_field_value(&self) -> String { + pub fn to_field_value(&self) -> Cow<'_, str> { match &self.inner { - HttpMessageComponentValueInner::String(val) => val.to_owned(), - HttpMessageComponentValueInner::KeyValue((key, val)) => format!("{}={}", key, val), + HttpMessageComponentValueInner::String(val) => val.into(), + HttpMessageComponentValueInner::KeyValue((key, val)) => format!("{}={}", key, val).into(), } } /// Get value only diff --git a/httpsig/src/signature_base.rs b/httpsig/src/signature_base.rs index 4f07dd5..a6ca1d0 100644 --- a/httpsig/src/signature_base.rs +++ b/httpsig/src/signature_base.rs @@ -33,7 +33,7 @@ impl HttpSignatureHeaders { let signature_input: sfv::Dictionary = Parser::new(signature_input_header) .parse() .map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; - let signature: sfv::Dictionary = Parser::new(signature_header) + let mut signature: sfv::Dictionary = Parser::new(signature_header) .parse() .map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; // let signature_input = @@ -43,13 +43,13 @@ impl HttpSignatureHeaders { if signature.len() != signature_input.len() { return Err(HttpSigError::BuildSignatureHeaderError( - "The number of signature and signature-input headers are not the same".to_string(), + "The number of signature and signature-input headers are not the same", )); } if !signature.keys().all(|k| signature_input.contains_key(k)) { return Err(HttpSigError::BuildSignatureHeaderError( - "The signature and signature-input headers are not the same".to_string(), + "The signature and signature-input headers are not the same", )); } if !signature.values().all(|v| { @@ -62,12 +62,12 @@ impl HttpSignatureHeaders { ) }) { return Err(HttpSigError::BuildSignatureHeaderError( - "The signature header is not a dictionary".to_string(), + "The signature header is not a dictionary", )); } if !signature_input.values().all(|v| matches!(v, ListEntry::InnerList(_))) { return Err(HttpSigError::BuildSignatureHeaderError( - "The signature-input header is not a dictionary".to_string(), + "The signature-input header is not a dictionary", )); } @@ -77,14 +77,14 @@ impl HttpSignatureHeaders { let signature_name = k.to_string(); let signature_params = HttpSignatureParams::try_from(v)?; - let signature_bytes = match signature.get(k) { + let signature_bytes = match signature.swap_remove(k) { Some(ListEntry::Item(Item { bare_item: BareItem::ByteSequence(v), .. })) => v, _ => unreachable!(), }; - let signature = HttpSignature(signature_bytes.to_vec()); + let signature = HttpSignature(signature_bytes); Ok(( signature_name.clone(), @@ -148,7 +148,7 @@ impl HttpSignatureBase { /// This should not be exposed to user and not used directly. /// Use wrapper functions generating SignatureBase from base HTTP request and Signer itself instead when newly generating signature /// When verifying signature, use wrapper functions generating SignatureBase from HTTP request containing signature params itself instead. - pub fn try_new(component_lines: &[HttpMessageComponent], signature_params: &HttpSignatureParams) -> HttpSigResult { + pub fn try_new(component_lines: Vec, signature_params: &HttpSignatureParams) -> HttpSigResult { // 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() { return Err(HttpSigError::BuildSignatureBaseError( @@ -167,7 +167,8 @@ impl HttpSignatureBase { } Ok(Self { - component_lines: component_lines.to_vec(), + component_lines, + // defer clone on happy path signature_params: signature_params.clone(), }) } @@ -270,7 +271,7 @@ mod test { .map(|&s| HttpMessageComponent::try_from(s)) .collect::, _>>() .unwrap(); - let signature_base = HttpSignatureBase::try_new(&component_lines, &signature_params).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines, &signature_params).unwrap(); let test_string = r##""@method": GET "@path": / "date": Tue, 07 Jun 2014 20:51:35 GMT diff --git a/httpsig/src/signature_params.rs b/httpsig/src/signature_params.rs index 122b66c..b735b38 100644 --- a/httpsig/src/signature_params.rs +++ b/httpsig/src/signature_params.rs @@ -38,9 +38,7 @@ impl HttpSignatureParams { pub fn try_new(covered_components: &[HttpMessageComponentId]) -> HttpSigResult { let created = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); if !has_unique_elements(covered_components.iter()) { - return Err(HttpSigError::InvalidSignatureParams( - "duplicate covered component ids".to_string(), - )); + return Err(HttpSigError::InvalidSignatureParams("duplicate covered component ids")); } Ok(Self { @@ -153,7 +151,7 @@ impl TryFrom<&ListEntry> for HttpSignatureParams { /// Convert from ListEntry to HttpSignatureParams fn try_from(value: &ListEntry) -> HttpSigResult { if !matches!(value, ListEntry::InnerList(_)) { - return Err(HttpSigError::InvalidSignatureParams("Invalid signature params".to_string())); + return Err(HttpSigError::InvalidSignatureParams("Invalid signature params")); } let inner_list_with_params = match value { ListEntry::InnerList(v) => v, @@ -171,9 +169,7 @@ impl TryFrom<&ListEntry> for HttpSignatureParams { .collect::, _>>()?; if !has_unique_elements(covered_components.iter()) { - return Err(HttpSigError::InvalidSignatureParams( - "duplicate covered component ids".to_string(), - )); + return Err(HttpSigError::InvalidSignatureParams("duplicate covered component ids")); } let mut params = Self { @@ -213,7 +209,7 @@ impl TryFrom<&str> for HttpSignatureParams { .map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; // let sfv_parsed = Parser::parse_list(value.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; if sfv_parsed.len() != 1 || !matches!(sfv_parsed[0], ListEntry::InnerList(_)) { - return Err(HttpSigError::InvalidSignatureParams("Invalid signature params".to_string())); + return Err(HttpSigError::InvalidSignatureParams("Invalid signature params")); } HttpSignatureParams::try_from(&sfv_parsed[0]) } From d6b3c97a78dfc753c32b1bc0f08223b2a1458cb3 Mon Sep 17 00:00:00 2001 From: Rinat Shigapov Date: Wed, 20 May 2026 12:10:35 +0800 Subject: [PATCH 2/5] perf: add signature base construction/parsing benches --- httpsig/Cargo.toml | 5 +++ httpsig/benches/noncrypto.rs | 81 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 httpsig/benches/noncrypto.rs diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index b20d835..31a8199 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -59,3 +59,8 @@ sfv = { version = "0.14.0" } [dev-dependencies] rand-085 = { package = "rand", version = "0.8.5" } # testing only +criterion = "0.8" + +[[bench]] +name = "noncrypto" +harness = false diff --git a/httpsig/benches/noncrypto.rs b/httpsig/benches/noncrypto.rs new file mode 100644 index 0000000..63e7a32 --- /dev/null +++ b/httpsig/benches/noncrypto.rs @@ -0,0 +1,81 @@ +//! Benches of non crypto logic like signature base extraction or serialization. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use httpsig::prelude::{ + AlgorithmName, HttpSignatureBase, HttpSignatureHeaders, HttpSignatureParams, SecretKey, message_component::HttpMessageComponent, +}; + +const COMPONENT_LINES: &[&str] = &[ + r##""date": Tue, 20 Apr 2021 02:07:55 GMT"##, + r##""@method": POST"##, + r##""@path": /foo"##, + r##""@authority": example.com"##, + r##""content-type": application/json"##, + r##""content-length": 18"##, +]; +const SIGNATURE_PARAMS: &str = + r##"("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519""##; + +fn construct_signature_base(signature_params: &str, component_lines: &[&str]) -> HttpSignatureBase { + let signature_params = HttpSignatureParams::try_from(signature_params).unwrap(); + let component_lines = component_lines + .iter() + .map(|&line| HttpMessageComponent::try_from(line).unwrap()) + .collect::>(); + HttpSignatureBase::try_new(component_lines, &signature_params).unwrap() +} + +fn parse_signature_base(signature_input_header: &str, signature_header: &str, component_lines: &[&str]) -> HttpSignatureBase { + let header_map = HttpSignatureHeaders::try_parse(signature_header, signature_input_header).unwrap(); + let received_signature_headers = header_map.get("sig-b26").unwrap(); + let component_lines = component_lines + .iter() + .map(|&line| HttpMessageComponent::try_from(line).unwrap()) + .collect::>(); + HttpSignatureBase::try_new(component_lines, received_signature_headers.signature_params()).unwrap() +} + +/* ----------------------------------------------------------------- */ +// params from https://datatracker.ietf.org/doc/html/rfc9421#name-signing-a-request-using-ed2 +const EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF +-----END PRIVATE KEY----- +"##; + +fn setup_signature_input() -> (String, String, &'static [&'static str]) { + let component_lines = COMPONENT_LINES + .iter() + .map(|&line| HttpMessageComponent::try_from(line).unwrap()) + .collect::>(); + + // sender + let signature_params = HttpSignatureParams::try_from(SIGNATURE_PARAMS).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines, &signature_params).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let signature_headers = signature_base.build_signature_headers(&sk, Some("sig-b26")).unwrap(); + ( + signature_headers.signature_input_header_value(), + signature_headers.signature_header_value(), + COMPONENT_LINES, + ) +} + +fn construct_parse(c: &mut Criterion) { + c.bench_function("sig base try_new", |b| { + b.iter(|| construct_signature_base(black_box(SIGNATURE_PARAMS), black_box(COMPONENT_LINES))) + }); + c.bench_function("sig base try_parse", |b| { + b.iter_batched_ref( + || setup_signature_input(), + |(signature_input_header, signature_header, component_lines)| { + parse_signature_base(signature_input_header, signature_header, component_lines) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +criterion_group!(benches, construct_parse); +criterion_main!(benches); From 8a15bf3a30048220a4b5bf24f7b85cc1e7d6771a Mon Sep 17 00:00:00 2001 From: Rinat Shigapov Date: Fri, 22 May 2026 17:49:44 +0800 Subject: [PATCH 3/5] perf: pass signature_params by value --- httpsig-hyper/examples/hyper-request.rs | 4 +- httpsig-hyper/examples/hyper-response.rs | 4 +- httpsig-hyper/src/hyper_http.rs | 142 +++++++++++------------ httpsig-hyper/src/hyper_http_tests.rs | 38 +++--- httpsig-hyper/src/lib.rs | 8 +- httpsig/benches/noncrypto.rs | 10 +- httpsig/src/lib.rs | 14 +-- httpsig/src/signature_base.rs | 22 ++-- 8 files changed, 120 insertions(+), 122 deletions(-) diff --git a/httpsig-hyper/examples/hyper-request.rs b/httpsig-hyper/examples/hyper-request.rs index d9243f3..1331b8e 100644 --- a/httpsig-hyper/examples/hyper-request.rs +++ b/httpsig-hyper/examples/hyper-request.rs @@ -48,7 +48,7 @@ async fn sender_ed25519(req: &mut Request) { // set signature with custom signature name req - .set_message_signature(&signature_params, &secret_key, Some("siged25519")) + .set_message_signature(signature_params, &secret_key, Some("siged25519")) .await .unwrap(); } @@ -70,7 +70,7 @@ async fn sender_hs256(req: &mut Request) { signature_params.set_random_nonce(); req - .set_message_signature(&signature_params, &shared_key, Some("sighs256")) + .set_message_signature(signature_params, &shared_key, Some("sighs256")) .await .unwrap(); } diff --git a/httpsig-hyper/examples/hyper-response.rs b/httpsig-hyper/examples/hyper-response.rs index e096d29..32a8ab7 100644 --- a/httpsig-hyper/examples/hyper-response.rs +++ b/httpsig-hyper/examples/hyper-response.rs @@ -60,7 +60,7 @@ async fn sender_ed25519(res: &mut Response, received_req: &Request, received_req: &Request( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, ) -> impl Future> + Send @@ -45,13 +46,11 @@ pub trait MessageSignatureReq { T: SigningKey + Sync; /// Set the http message signatures from given tuples of (http signature params, signing key, name) - fn set_message_signatures( - &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], - ) -> impl Future> + Send + fn set_message_signatures<'a, T, I>(&mut self, params_key_name: I) -> impl Future> + Send where Self: Sized, - T: SigningKey + Sync; + T: SigningKey + Sync + 'a, + I: IntoIterator)> + Send; /// Verify the http message signature with given verifying key if the request has signature and signature-input headers fn verify_message_signature( @@ -72,35 +71,36 @@ pub trait MessageSignatureReq { Self: Sized, T: VerifyingKey + Sync; - /// Extract all signature bases contained in the request headers - fn extract_signatures(&self) -> Result, Self::Error>; + /// Extract all signature bases and signatures contained in the request headers + fn extract_signatures(&self) -> Result, Self::Error>; } /// A trait about http message signature for response pub trait MessageSignatureRes { type Error; /// Set the http message signature from given http signature params and signing key - fn set_message_signature( + fn set_message_signature<'a, T, B>( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, req_for_param: Option<&Request>, ) -> impl Future> + Send where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, B: Sync; /// Set the http message signatures from given tuples of (http signature params, signing key, name) - fn set_message_signatures( + fn set_message_signatures<'a, T, I, B>( &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + params_key_name: I, req_for_param: Option<&Request>, ) -> impl Future> + Send where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, + I: IntoIterator)> + Send, B: Sync; /// Verify the http message signature with given verifying key if the request has signature and signature-input headers @@ -126,11 +126,11 @@ pub trait MessageSignatureRes { T: VerifyingKey + Sync, B: Sync; - /// Extract all signature bases contained in the request headers + /// Extract all signature bases and signatures contained in the request headers fn extract_signatures( &self, req_for_param: Option<&Request>, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; } /* --------------------------------------- */ @@ -146,7 +146,7 @@ pub trait MessageSignatureRes { pub trait MessageSignatureReqSync: MessageSignatureReq { fn set_message_signature_sync( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, ) -> Result<(), Self::Error> @@ -154,13 +154,11 @@ pub trait MessageSignatureReqSync: MessageSignatureReq { Self: Sized, T: SigningKey + Sync; - fn set_message_signatures_sync( - &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], - ) -> Result<(), Self::Error> + fn set_message_signatures_sync<'a, T, I>(&mut self, params_key_name: I) -> Result<(), Self::Error> where Self: Sized, - T: SigningKey + Sync; + I: IntoIterator)> + Send, + T: SigningKey + Sync + 'a; fn verify_message_signature_sync(&self, verifying_key: &T, key_id: Option<&str>) -> Result where @@ -188,7 +186,7 @@ pub trait MessageSignatureReqSync: MessageSignatureReq { pub trait MessageSignatureResSync: MessageSignatureRes { fn set_message_signature_sync( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, req_for_param: Option<&Request>, @@ -198,14 +196,15 @@ pub trait MessageSignatureResSync: MessageSignatureRes { T: SigningKey + Sync, B: Sync; - fn set_message_signatures_sync( + fn set_message_signatures_sync<'a, T, I, B>( &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + params_key_name: I, req_for_param: Option<&Request>, ) -> Result<(), Self::Error> where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, + I: IntoIterator)> + Send, B: Sync; fn verify_message_signature_sync( @@ -264,7 +263,7 @@ where /// Set the http message signature from given http signature params and signing key async fn set_message_signature( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, ) -> HyperSigResult<()> @@ -273,23 +272,21 @@ where T: SigningKey + Sync, { self - .set_message_signatures(&[(signature_params, signing_key, signature_name)]) + .set_message_signatures([(signature_params, signing_key, signature_name)]) .await } - async fn set_message_signatures( - &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], - ) -> Result<(), Self::Error> + async fn set_message_signatures<'a, T, I>(&mut self, params_key_name: I) -> Result<(), Self::Error> where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, + I: IntoIterator)> + Send, { let req_or_res = RequestOrResponse::Request(self); let vec_signature_bases = params_key_name - .iter() + .into_iter() .map(|(params, key, name)| { - build_signature_base(&req_or_res, params, None as Option<&Request<()>>).map(|base| (base, *key, *name)) + build_signature_base(&req_or_res, params, None as Option<&Request<()>>).map(|base| (base, key, name)) }) .collect::, _>>()?; let vec_signature_headers = futures::future::join_all( @@ -345,7 +342,7 @@ where } /// Extract all signature bases contained in the request headers - fn extract_signatures(&self) -> Result, Self::Error> { + fn extract_signatures(&self) -> Result, Self::Error> { let req_or_res = RequestOrResponse::Request(self); extract_signatures_inner(&req_or_res, None as Option<&Request<()>>) } @@ -383,37 +380,38 @@ where type Error = HyperSigError; /// Set the http message signature from given http signature params and signing key - async fn set_message_signature( + async fn set_message_signature<'a, T, B>( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, req_for_param: Option<&Request>, ) -> Result<(), Self::Error> where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, B: Sync, { self - .set_message_signatures(&[(signature_params, signing_key, signature_name)], req_for_param) + .set_message_signatures([(signature_params, signing_key, signature_name)], req_for_param) .await } - async fn set_message_signatures( + async fn set_message_signatures<'a, T, I, B>( &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + params_key_name: I, req_for_param: Option<&Request>, ) -> Result<(), Self::Error> where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, + I: IntoIterator)> + Send, { let req_or_res = RequestOrResponse::Response(self); let vec_signature_bases = params_key_name - .iter() - .map(|(params, key, name)| build_signature_base(&req_or_res, params, req_for_param).map(|base| (base, *key, *name))) + .into_iter() + .map(|(params, key, name)| build_signature_base(&req_or_res, params, req_for_param).map(|base| (base, key, name))) .collect::, _>>()?; let vec_signature_headers = futures::future::join_all( vec_signature_bases @@ -479,7 +477,7 @@ where fn extract_signatures( &self, req_for_param: Option<&Request>, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { let req_or_res = RequestOrResponse::Response(self); extract_signatures_inner(&req_or_res, req_for_param) } @@ -493,7 +491,7 @@ where { fn set_message_signature_sync( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, ) -> Result<(), Self::Error> @@ -504,13 +502,11 @@ where futures::executor::block_on(self.set_message_signature(signature_params, signing_key, signature_name)) } - fn set_message_signatures_sync( - &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], - ) -> Result<(), Self::Error> + fn set_message_signatures_sync<'a, T, I>(&mut self, params_key_name: I) -> Result<(), Self::Error> where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, + I: IntoIterator)> + Send, { futures::executor::block_on(self.set_message_signatures(params_key_name)) } @@ -542,7 +538,7 @@ where { fn set_message_signature_sync( &mut self, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, req_for_param: Option<&Request>, @@ -555,14 +551,15 @@ where futures::executor::block_on(self.set_message_signature(signature_params, signing_key, signature_name, req_for_param)) } - fn set_message_signatures_sync( + fn set_message_signatures_sync<'a, T, I, B>( &mut self, - params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + params_key_name: I, req_for_param: Option<&Request>, ) -> Result<(), Self::Error> where Self: Sized, - T: SigningKey + Sync, + T: SigningKey + Sync + 'a, + I: IntoIterator)> + Send, B: Sync, { futures::executor::block_on(self.set_message_signatures(params_key_name, req_for_param)) @@ -609,19 +606,19 @@ fn get_alg_key_ids_inner( ) -> HyperSigResult, Option)>> { let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; let res = signature_headers_map - .iter() + .into_iter() .map(|(name, headers)| { // Unknown or unsupported algorithm strings are mapped to None - let alg = headers - .signature_params() + let (_, params) = headers.into_signature_and_params(); + let alg = params .alg .as_ref() .map(|a| AlgorithmName::from_str(a)) .transpose() .ok() .flatten(); - let key_id = headers.signature_params().keyid.clone(); - (name.clone(), (alg, key_id)) + let key_id = params.keyid; + (name, (alg, key_id)) }) .collect(); Ok(res) @@ -633,8 +630,8 @@ fn get_signature_params_inner( ) -> HyperSigResult> { let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; let res = signature_headers_map - .iter() - .map(|(name, headers)| (name.clone(), headers.signature_params().clone())) + .into_iter() + .map(|(name, headers)| (name, headers.into_signature_and_params().1)) .collect(); Ok(res) } @@ -643,14 +640,15 @@ fn get_signature_params_inner( fn extract_signatures_inner( req_or_res: &RequestOrResponse, req_for_param: Option<&Request>, -) -> HyperSigResult> { +) -> HyperSigResult> { let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; let extracted = signature_headers_map .into_iter() .filter_map(|(name, headers)| { - build_signature_base(req_or_res, headers.signature_params(), req_for_param) + let (signature, params) = headers.into_signature_and_params(); + build_signature_base(req_or_res, params, req_for_param) .ok() - .map(|base| (name, (base, headers))) + .map(|base| (name, (base, signature))) }) .collect(); Ok(extracted) @@ -658,7 +656,7 @@ fn extract_signatures_inner( /// Verify multiple signatures inner function async fn verify_message_signatures_inner( - map_signature_with_base: &IndexMap, + map_signature_with_base: &IndexMap, key_and_id: &[(&T, Option<&str>)], ) -> HyperSigResult>> where @@ -685,10 +683,10 @@ where // check if any one of the signature headers is valid let successful_sig_names = filtered .iter() - .filter_map(|(&name, (base, headers))| base.verify_signature_headers(*key, headers).ok().map(|_| name.clone())) + .filter_map(|(&name, (base, signature))| base.verify_signature(*key, signature).ok().map(|_| name.clone())) .collect::>(); - if !successful_sig_names.is_empty() { - Ok(successful_sig_names.first().unwrap().clone()) + if let Some(first_successful) = successful_sig_names.first() { + Ok(first_successful.clone()) } else { Err(HyperSigError::InvalidSignature("Invalid signature for the verifying key")) } @@ -768,7 +766,7 @@ fn extract_signature_headers_with_name(req_or_res: &RequestOrResponse) -> /// - req_for_param: corresponding request to be considered in the signature base in response fn build_signature_base( req_or_res: &RequestOrResponse, - signature_params: &HttpSignatureParams, + signature_params: HttpSignatureParams, req_for_param: Option<&Request>, ) -> HyperSigResult { let component_lines = signature_params diff --git a/httpsig-hyper/src/hyper_http_tests.rs b/httpsig-hyper/src/hyper_http_tests.rs index 196838c..8820644 100644 --- a/httpsig-hyper/src/hyper_http_tests.rs +++ b/httpsig-hyper/src/hyper_http_tests.rs @@ -130,7 +130,7 @@ async fn test_build_signature_base_from_request() { let signature_params = HttpSignatureParams::try_from(format!("({}){}", values.0, values.1).as_str()).unwrap(); let req_or_res = RequestOrResponse::Request(&req); - let signature_base = build_signature_base(&req_or_res, &signature_params, None as Option<&Request<()>>).unwrap(); + let signature_base = build_signature_base(&req_or_res, signature_params, None as Option<&Request<()>>).unwrap(); assert_eq!( signature_base.to_string(), r##""@method": GET @@ -175,7 +175,7 @@ async fn test_set_verify_message_signature_req() { let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); - req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); + req.set_message_signature(signature_params, &secret_key, None).await.unwrap(); let signature_input = req.headers().get("signature-input").unwrap().to_str().unwrap(); assert!(signature_input.starts_with(r##"sig=("@method" "date" "content-type" "content-digest")"##)); @@ -195,7 +195,7 @@ async fn test_set_verify_message_signature_res() { signature_params.set_key_info(&secret_key); res - .set_message_signature(&signature_params, &secret_key, None, Some(&req)) + .set_message_signature(signature_params, &secret_key, None, Some(&req)) .await .unwrap(); let signature_input = res.headers().get("signature-input").unwrap().to_str().unwrap(); @@ -216,7 +216,7 @@ async fn test_expired_signature() { signature_params.set_expires(created - 1); assert!(signature_params.is_expired()); - req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); + req.set_message_signature(signature_params, &secret_key, None).await.unwrap(); let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature(&public_key, None).await; @@ -231,7 +231,7 @@ async fn test_set_verify_with_signature_name() { signature_params.set_key_info(&secret_key); req - .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name")) + .set_message_signature(signature_params, &secret_key, Some("custom_sig_name")) .await .unwrap(); @@ -252,7 +252,7 @@ async fn test_set_verify_with_key_id() { let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); - req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); + req.set_message_signature(signature_params, &secret_key, None).await.unwrap(); let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let key_id = public_key.key_id(); @@ -275,7 +275,7 @@ async fn test_set_verify_with_key_id_hmac_sha256() { // Random nonce is highly recommended for HMAC signature_params.set_random_nonce(); - req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); + req.set_message_signature(signature_params, &secret_key, None).await.unwrap(); let org_key_id = VerifyingKey::key_id(&secret_key); let (alg, key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; @@ -297,7 +297,7 @@ async fn test_get_alg_key_ids() { let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); - req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); + req.set_message_signature(signature_params, &secret_key, None).await.unwrap(); let key_ids = req.get_alg_key_ids().unwrap(); assert_eq!(key_ids.len(), 1); assert_eq!(key_ids[0].0.as_ref().unwrap(), &AlgorithmName::Ed25519); @@ -327,9 +327,9 @@ async fn test_set_verify_multiple_signatures() { let mut signature_params_hmac = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params_hmac.set_key_info(&secret_key_p256); - let params_key_name = &[ - (&signature_params_eddsa, &secret_key_eddsa, Some("eddsa_sig")), - (&signature_params_hmac, &secret_key_p256, Some("p256_sig")), + let params_key_name = [ + (signature_params_eddsa, &secret_key_eddsa, Some("eddsa_sig")), + (signature_params_hmac, &secret_key_p256, Some("p256_sig")), ]; req.set_message_signatures(params_key_name).await.unwrap(); @@ -362,7 +362,7 @@ fn test_blocking_set_verify_message_signature_req() { let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); - req.set_message_signature_sync(&signature_params, &secret_key, None).unwrap(); + req.set_message_signature_sync(signature_params, &secret_key, None).unwrap(); let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature_sync(&public_key, None); @@ -378,7 +378,7 @@ fn test_blocking_set_verify_message_signature_res() { let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_res()).unwrap(); signature_params.set_key_info(&secret_key); res - .set_message_signature_sync(&signature_params, &secret_key, None, Some(&req)) + .set_message_signature_sync(signature_params, &secret_key, None, Some(&req)) .unwrap(); let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); @@ -406,7 +406,7 @@ fn test_query_param_sign_verify_sync() { signature_params.set_key_info(&secret_key); req - .set_message_signature_sync(&signature_params, &secret_key, Some("qp")) + .set_message_signature_sync(signature_params, &secret_key, Some("qp")) .unwrap(); assert!( @@ -441,7 +441,7 @@ async fn test_query_param_sign_verify_async() { signature_params.set_key_info(&secret_key); req - .set_message_signature(&signature_params, &secret_key, Some("qp")) + .set_message_signature(signature_params, &secret_key, Some("qp")) .await .unwrap(); @@ -505,7 +505,7 @@ async fn test_set_message_signature_propagates_build_error() { signature_params.set_key_info(&secret_key); let result = req - .set_message_signature(&signature_params, &secret_key, None as Option<&str>) + .set_message_signature(signature_params, &secret_key, None as Option<&str>) .await; assert!(result.is_err(), "expected Err when using `@status` on request, got Ok"); } @@ -526,7 +526,7 @@ fn test_set_message_signature_sync_propagates_build_error() { let mut signature_params = HttpSignatureParams::try_new(&covered).unwrap(); signature_params.set_key_info(&secret_key); - let result = req.set_message_signature_sync(&signature_params, &secret_key, None); + let result = req.set_message_signature_sync(signature_params, &secret_key, None); assert!(result.is_err(), "expected Err when using `@status` on request, got Ok"); } @@ -597,7 +597,7 @@ async fn test_response_with_query_param_req_sign_verify() { signature_params.set_key_info(&secret_key); res - .set_message_signature(&signature_params, &secret_key, None, Some(&req)) + .set_message_signature(signature_params, &secret_key, None, Some(&req)) .await .unwrap(); @@ -641,7 +641,7 @@ async fn test_response_rejects_derived_component_without_req() { signature_params.set_key_info(&secret_key); let result = res - .set_message_signature(&signature_params, &secret_key, None, None as Option<&Request<()>>) + .set_message_signature(signature_params, &secret_key, None, None as Option<&Request<()>>) .await; assert!(result.is_err(), "expected Err when using `@method` without `req` on response"); } diff --git a/httpsig-hyper/src/lib.rs b/httpsig-hyper/src/lib.rs index 691a392..dd0b890 100644 --- a/httpsig-hyper/src/lib.rs +++ b/httpsig-hyper/src/lib.rs @@ -136,7 +136,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // set custom signature name req - .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name")) + .set_message_signature(signature_params, &secret_key, Some("custom_sig_name")) .await .unwrap(); let signature_input = req.headers().get("signature-input").unwrap().to_str().unwrap(); @@ -181,7 +181,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // set custom signature name, and `req` field param if needed (e.g., request method, uri, content-digest, etc.) included only in response res - .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name"), Some(&req)) + .set_message_signature(signature_params, &secret_key, Some("custom_sig_name"), Some(&req)) .await .unwrap(); let signature_input = res.headers().get("signature-input").unwrap().to_str().unwrap(); @@ -227,7 +227,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // set key information, alg and keyid signature_params.set_key_info(&secret_key); // set signature - req.set_message_signature_sync(&signature_params, &secret_key, None).unwrap(); + req.set_message_signature_sync(signature_params, &secret_key, None).unwrap(); let (alg, _key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap(); @@ -252,7 +252,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= signature_params.set_key_info(&secret_key); // set signature res - .set_message_signature_sync(&signature_params, &secret_key, None, Some(&req)) + .set_message_signature_sync(signature_params, &secret_key, None, Some(&req)) .unwrap(); let (alg, _key_id) = res.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; diff --git a/httpsig/benches/noncrypto.rs b/httpsig/benches/noncrypto.rs index 63e7a32..e158a96 100644 --- a/httpsig/benches/noncrypto.rs +++ b/httpsig/benches/noncrypto.rs @@ -24,17 +24,17 @@ fn construct_signature_base(signature_params: &str, component_lines: &[&str]) -> .iter() .map(|&line| HttpMessageComponent::try_from(line).unwrap()) .collect::>(); - HttpSignatureBase::try_new(component_lines, &signature_params).unwrap() + HttpSignatureBase::try_new(component_lines, signature_params).unwrap() } fn parse_signature_base(signature_input_header: &str, signature_header: &str, component_lines: &[&str]) -> HttpSignatureBase { - let header_map = HttpSignatureHeaders::try_parse(signature_header, signature_input_header).unwrap(); - let received_signature_headers = header_map.get("sig-b26").unwrap(); + let mut header_map = HttpSignatureHeaders::try_parse(signature_header, signature_input_header).unwrap(); + let received_signature_headers = header_map.swap_remove("sig-b26").unwrap(); let component_lines = component_lines .iter() .map(|&line| HttpMessageComponent::try_from(line).unwrap()) .collect::>(); - HttpSignatureBase::try_new(component_lines, received_signature_headers.signature_params()).unwrap() + HttpSignatureBase::try_new(component_lines, received_signature_headers.into_signature_and_params().1).unwrap() } /* ----------------------------------------------------------------- */ @@ -52,7 +52,7 @@ fn setup_signature_input() -> (String, String, &'static [&'static str]) { // sender let signature_params = HttpSignatureParams::try_from(SIGNATURE_PARAMS).unwrap(); - let signature_base = HttpSignatureBase::try_new(component_lines, &signature_params).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines, signature_params).unwrap(); let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let signature_headers = signature_base.build_signature_headers(&sk, Some("sig-b26")).unwrap(); ( diff --git a/httpsig/src/lib.rs b/httpsig/src/lib.rs index e5f1b0c..07d8647 100644 --- a/httpsig/src/lib.rs +++ b/httpsig/src/lib.rs @@ -122,7 +122,7 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw .map(|&line| message_component::HttpMessageComponent::try_from(line).unwrap()) .collect::>(); - let signature_base = HttpSignatureBase::try_new(component_lines, &signature_params).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines, signature_params).unwrap(); let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); @@ -140,7 +140,7 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw // sender let signature_params = HttpSignatureParams::try_from(SIGNATURE_PARAMS).unwrap(); - let signature_base = HttpSignatureBase::try_new(component_lines.clone(), &signature_params).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines.clone(), signature_params).unwrap(); let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let signature_headers = signature_base.build_signature_headers(&sk, Some("sig-b26")).unwrap(); let signature_params_header_string = signature_headers.signature_input_header_value(); @@ -150,12 +150,12 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw assert!(signature_header_string.starts_with("sig-b26=:") && signature_header_string.ends_with(':')); // receiver - let header_map = HttpSignatureHeaders::try_parse(&signature_header_string, &signature_params_header_string).unwrap(); - let received_signature_headers = header_map.get("sig-b26").unwrap(); - let received_signature_base = - HttpSignatureBase::try_new(component_lines, received_signature_headers.signature_params()).unwrap(); + let mut header_map = HttpSignatureHeaders::try_parse(&signature_header_string, &signature_params_header_string).unwrap(); + let received_signature_headers = header_map.swap_remove("sig-b26").unwrap(); + let (signature, params) = received_signature_headers.into_signature_and_params(); + let received_signature_base = HttpSignatureBase::try_new(component_lines, params).unwrap(); let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_result = received_signature_base.verify_signature_headers(&pk, received_signature_headers); + let verification_result = received_signature_base.verify_signature(&pk, &signature); assert!(verification_result.is_ok()); } } diff --git a/httpsig/src/signature_base.rs b/httpsig/src/signature_base.rs index a6ca1d0..2e481e7 100644 --- a/httpsig/src/signature_base.rs +++ b/httpsig/src/signature_base.rs @@ -114,6 +114,11 @@ impl HttpSignatureHeaders { &self.signature_params } + /// Converts into the (signature, signature params value pair) without name for signature-input header + pub fn into_signature_and_params(self) -> (HttpSignature, HttpSignatureParams) { + (self.signature, self.signature_params) + } + /// Returns the signature value of "Signature" http header in the form of "=::" pub fn signature_header_value(&self) -> String { format!("{}=:{}:", self.signature_name, self.signature) @@ -148,7 +153,7 @@ impl HttpSignatureBase { /// This should not be exposed to user and not used directly. /// Use wrapper functions generating SignatureBase from base HTTP request and Signer itself instead when newly generating signature /// When verifying signature, use wrapper functions generating SignatureBase from HTTP request containing signature params itself instead. - pub fn try_new(component_lines: Vec, signature_params: &HttpSignatureParams) -> HttpSigResult { + pub fn try_new(component_lines: Vec, signature_params: HttpSignatureParams) -> HttpSigResult { // 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() { return Err(HttpSigError::BuildSignatureBaseError( @@ -168,8 +173,7 @@ impl HttpSignatureBase { Ok(Self { component_lines, - // defer clone on happy path - signature_params: signature_params.clone(), + signature_params, }) } @@ -200,17 +204,13 @@ impl HttpSignatureBase { } /// Verify the signature using the given verifying key - pub fn verify_signature_headers( - &self, - verifying_key: &impl VerifyingKey, - signature_headers: &HttpSignatureHeaders, - ) -> HttpSigResult<()> { - if signature_headers.signature_params().is_expired() { + pub fn verify_signature(&self, verifying_key: &impl VerifyingKey, signature: &HttpSignature) -> HttpSigResult<()> { + if self.signature_params.is_expired() { return Err(HttpSigError::ExpiredSignatureParams( "Signature params is expired".to_string(), )); } - let signature_bytes = signature_headers.signature.0.as_slice(); + let signature_bytes = signature.0.as_slice(); verifying_key.verify(&self.as_bytes(), signature_bytes) } @@ -271,7 +271,7 @@ mod test { .map(|&s| HttpMessageComponent::try_from(s)) .collect::, _>>() .unwrap(); - let signature_base = HttpSignatureBase::try_new(component_lines, &signature_params).unwrap(); + let signature_base = HttpSignatureBase::try_new(component_lines, signature_params).unwrap(); let test_string = r##""@method": GET "@path": / "date": Tue, 07 Jun 2014 20:51:35 GMT From eadff5311a4baa2b8449f99e8678475b96f7ccf5 Mon Sep 17 00:00:00 2001 From: Rinat Shigapov Date: Fri, 22 May 2026 18:19:15 +0800 Subject: [PATCH 4/5] perf: don't duplicate signature name, available in header map key --- httpsig-hyper/src/hyper_http.rs | 35 +++++++++++++----------- httpsig-hyper/src/hyper_http_tests.rs | 8 +++--- httpsig/benches/noncrypto.rs | 6 ++--- httpsig/src/lib.rs | 6 ++--- httpsig/src/signature_base.rs | 38 ++++++++------------------- 5 files changed, 41 insertions(+), 52 deletions(-) diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 0e56b68..daad4ea 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -254,6 +254,9 @@ where } } +/// Default signature name used to indicate signature in http header (`signature` and `signature-input`) +const DEFAULT_SIGNATURE_NAME: &str = "sig"; + impl MessageSignatureReq for Request where D: Send + Body + Sync, @@ -289,21 +292,21 @@ where build_signature_base(&req_or_res, params, None as Option<&Request<()>>).map(|base| (base, key, name)) }) .collect::, _>>()?; - let vec_signature_headers = futures::future::join_all( - vec_signature_bases - .into_iter() - .map(|(base, key, name)| async move { base.build_signature_headers(key, name) }), - ) + let vec_signature_headers = futures::future::join_all(vec_signature_bases.into_iter().map(|(base, key, name)| async move { + base + .build_signature_headers(key) + .map(|headers| (name.unwrap_or(DEFAULT_SIGNATURE_NAME), headers)) + })) .await .into_iter() .collect::, _>>()?; - vec_signature_headers.iter().try_for_each(|headers| { + vec_signature_headers.iter().try_for_each(|(name, headers)| { self .headers_mut() - .append("signature-input", headers.signature_input_header_value().parse()?); + .append("signature-input", headers.signature_input_header_value(name).parse()?); self .headers_mut() - .append("signature", headers.signature_header_value().parse()?); + .append("signature", headers.signature_header_value(name).parse()?); Ok(()) as Result<(), HyperSigError> }) } @@ -413,22 +416,22 @@ where .into_iter() .map(|(params, key, name)| build_signature_base(&req_or_res, params, req_for_param).map(|base| (base, key, name))) .collect::, _>>()?; - let vec_signature_headers = futures::future::join_all( - vec_signature_bases - .into_iter() - .map(|(base, key, name)| async move { base.build_signature_headers(key, name) }), - ) + let vec_signature_headers = futures::future::join_all(vec_signature_bases.into_iter().map(|(base, key, name)| async move { + base + .build_signature_headers(key) + .map(|headers| (name.unwrap_or(DEFAULT_SIGNATURE_NAME), headers)) + })) .await .into_iter() .collect::, _>>()?; - vec_signature_headers.iter().try_for_each(|headers| { + vec_signature_headers.iter().try_for_each(|(name, headers)| { self .headers_mut() - .append("signature-input", headers.signature_input_header_value().parse()?); + .append("signature-input", headers.signature_input_header_value(name).parse()?); self .headers_mut() - .append("signature", headers.signature_header_value().parse()?); + .append("signature", headers.signature_header_value(name).parse()?); Ok(()) as Result<(), HyperSigError> }) } diff --git a/httpsig-hyper/src/hyper_http_tests.rs b/httpsig-hyper/src/hyper_http_tests.rs index 8820644..dbd0813 100644 --- a/httpsig-hyper/src/hyper_http_tests.rs +++ b/httpsig-hyper/src/hyper_http_tests.rs @@ -159,9 +159,10 @@ async fn test_extract_tuples_from_request() { let req_or_res = RequestOrResponse::Request(&req); let tuples = extract_signature_headers_with_name(&req_or_res).unwrap(); assert_eq!(tuples.len(), 1); - assert_eq!(tuples.get("sig11").unwrap().signature_name(), "sig11"); + let (signature_name, headers) = tuples.into_iter().next().expect("not empty"); + assert_eq!(signature_name, "sig11"); assert_eq!( - tuples.get("sig11").unwrap().signature_params().to_string(), + headers.signature_params().to_string(), r##"("@method" "@authority");created=1704972031"## ); } @@ -238,7 +239,8 @@ async fn test_set_verify_with_signature_name() { let req_or_res = RequestOrResponse::Request(&req); let signature_headers_map = extract_signature_headers_with_name(&req_or_res).unwrap(); assert_eq!(signature_headers_map.len(), 1); - assert_eq!(signature_headers_map[0].signature_name(), "custom_sig_name"); + let sig_name = signature_headers_map.keys().next().expect("not empty"); + assert_eq!(sig_name, "custom_sig_name"); let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature(&public_key, None).await; diff --git a/httpsig/benches/noncrypto.rs b/httpsig/benches/noncrypto.rs index e158a96..433247a 100644 --- a/httpsig/benches/noncrypto.rs +++ b/httpsig/benches/noncrypto.rs @@ -54,10 +54,10 @@ fn setup_signature_input() -> (String, String, &'static [&'static str]) { let signature_params = HttpSignatureParams::try_from(SIGNATURE_PARAMS).unwrap(); let signature_base = HttpSignatureBase::try_new(component_lines, signature_params).unwrap(); let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let signature_headers = signature_base.build_signature_headers(&sk, Some("sig-b26")).unwrap(); + let signature_headers = signature_base.build_signature_headers(&sk).unwrap(); ( - signature_headers.signature_input_header_value(), - signature_headers.signature_header_value(), + signature_headers.signature_input_header_value("sig-b26"), + signature_headers.signature_header_value("sig-b26"), COMPONENT_LINES, ) } diff --git a/httpsig/src/lib.rs b/httpsig/src/lib.rs index 07d8647..9b98fd0 100644 --- a/httpsig/src/lib.rs +++ b/httpsig/src/lib.rs @@ -142,9 +142,9 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw let signature_params = HttpSignatureParams::try_from(SIGNATURE_PARAMS).unwrap(); let signature_base = HttpSignatureBase::try_new(component_lines.clone(), signature_params).unwrap(); let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let signature_headers = signature_base.build_signature_headers(&sk, Some("sig-b26")).unwrap(); - let signature_params_header_string = signature_headers.signature_input_header_value(); - let signature_header_string = signature_headers.signature_header_value(); + let signature_headers = signature_base.build_signature_headers(&sk).unwrap(); + let signature_params_header_string = signature_headers.signature_input_header_value("sig-b26"); + let signature_header_string = signature_headers.signature_header_value("sig-b26"); assert_eq!(signature_params_header_string, format!("sig-b26={}", SIGNATURE_PARAMS)); assert!(signature_header_string.starts_with("sig-b26=:") && signature_header_string.ends_with(':')); diff --git a/httpsig/src/signature_base.rs b/httpsig/src/signature_base.rs index 2e481e7..3abaee2 100644 --- a/httpsig/src/signature_base.rs +++ b/httpsig/src/signature_base.rs @@ -13,14 +13,9 @@ use sfv::{BareItem, Item, ListEntry, Parser}; /// IndexMap of signature name and HttpSignatureHeaders pub type HttpSignatureHeadersMap = IndexMap; -/// Default signature name used to indicate signature in http header (`signature` and `signature-input`) -const DEFAULT_SIGNATURE_NAME: &str = "sig"; - #[derive(Debug, Clone)] /// Signature Headers derived from HttpSignatureBase pub struct HttpSignatureHeaders { - /// signature name coupling signature with signature input - signature_name: String, /// Signature value of "Signature" http header in the form of "=::" signature: HttpSignature, /// signature-params value of "Signature-Input" http header in the form of "=::" @@ -87,9 +82,8 @@ impl HttpSignatureHeaders { let signature = HttpSignature(signature_bytes); Ok(( - signature_name.clone(), + signature_name, Self { - signature_name, signature, signature_params, }, @@ -99,11 +93,6 @@ impl HttpSignatureHeaders { Ok(res) } - /// Returns the signature name - pub fn signature_name(&self) -> &str { - &self.signature_name - } - /// Returns the signature value without name pub fn signature(&self) -> &HttpSignature { &self.signature @@ -120,12 +109,12 @@ impl HttpSignatureHeaders { } /// Returns the signature value of "Signature" http header in the form of "=::" - pub fn signature_header_value(&self) -> String { - format!("{}=:{}:", self.signature_name, self.signature) + pub fn signature_header_value(&self, signature_name: &str) -> String { + format!("{}=:{}:", signature_name, self.signature) } /// Returns the signature input value of "Signature-Input" http header in the form of "=" - pub fn signature_input_header_value(&self) -> String { - format!("{}={}", self.signature_name, self.signature_params) + pub fn signature_input_header_value(&self, signature_name: &str) -> String { + format!("{}={}", signature_name, self.signature_params) } } @@ -190,16 +179,11 @@ impl HttpSignatureBase { } /// Build the signature and signature-input headers structs - pub fn build_signature_headers( - &self, - signing_key: &impl SigningKey, - signature_name: Option<&str>, - ) -> HttpSigResult { + pub fn build_signature_headers(self, signing_key: &impl SigningKey) -> HttpSigResult { let signature = self.build_raw_signature(signing_key)?; Ok(HttpSignatureHeaders { - signature_name: signature_name.unwrap_or(DEFAULT_SIGNATURE_NAME).to_string(), signature: HttpSignature(signature), - signature_params: self.signature_params.clone(), + signature_params: self.signature_params, }) } @@ -288,15 +272,15 @@ mod test { const SIGNATURE_INPUT: &str = r##"sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519", sig-b27=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519-alt""##; const SIGNATURE: &str = r##"sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:, sig-b27=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"##; - let header_map = HttpSignatureHeaders::try_parse(SIGNATURE, SIGNATURE_INPUT).unwrap(); + let mut header_map = HttpSignatureHeaders::try_parse(SIGNATURE, SIGNATURE_INPUT).unwrap(); assert!(header_map.len() == 2); - let http_signature_headers = header_map.get("sig-b26").unwrap(); + let http_signature_headers = header_map.swap_remove("sig-b26").unwrap(); assert_eq!( - http_signature_headers.signature_header_value(), + http_signature_headers.signature_header_value("sig-b26"), SIGNATURE.split(',').next().unwrap() ); assert_eq!( - http_signature_headers.signature_input_header_value(), + http_signature_headers.signature_input_header_value("sig-b26"), SIGNATURE_INPUT.split(',').next().unwrap() ); } From d40af1accbe028508496f428a37f623729bf0d74 Mon Sep 17 00:00:00 2001 From: Rinat Shigapov Date: Fri, 22 May 2026 18:49:03 +0800 Subject: [PATCH 5/5] perf: no allocations in Display impls, typical signature base buffer size --- httpsig-hyper/src/hyper_http.rs | 3 +- httpsig/src/lib.rs | 4 +- .../src/message_component/component_param.rs | 34 ++++++----- httpsig/src/signature_base.rs | 56 +++++++++++-------- 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index daad4ea..e65bf33 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -847,8 +847,7 @@ fn extract_derived_component( "`req`-tagged component must be extracted from the source request".to_string(), )), _ => Err(HyperSigError::InvalidComponentParam(format!( - "parameter `{}` is not allowed on derived components", - String::from(param.clone()) + "parameter `{param}` is not allowed on derived components", ))), })?; diff --git a/httpsig/src/lib.rs b/httpsig/src/lib.rs index 9b98fd0..894aeb8 100644 --- a/httpsig/src/lib.rs +++ b/httpsig/src/lib.rs @@ -126,8 +126,8 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let signature_bytes = sk.sign(&signature_base.as_bytes()).unwrap(); - let verification_result = pk.verify(&signature_base.as_bytes(), &signature_bytes); + let signature_bytes = sk.sign(&signature_base.to_vec()).unwrap(); + let verification_result = pk.verify(&signature_base.to_vec(), &signature_bytes); assert!(verification_result.is_ok()); } diff --git a/httpsig/src/message_component/component_param.rs b/httpsig/src/message_component/component_param.rs index 04717ca..9de8c35 100644 --- a/httpsig/src/message_component/component_param.rs +++ b/httpsig/src/message_component/component_param.rs @@ -1,3 +1,5 @@ +use std::fmt; + use crate::error::{HttpSigError, HttpSigResult}; use sfv::{FieldType, Parser}; @@ -24,16 +26,17 @@ pub enum HttpMessageComponentParam { Name(String), } -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(), - HttpMessageComponentParam::Name(v) => format!("name=\"{v}\""), +impl fmt::Display for HttpMessageComponentParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HttpMessageComponentParam::Sf => write!(f, "sf")?, + HttpMessageComponentParam::Key(key) => write!(f, "key=\"{key}\"")?, + HttpMessageComponentParam::Bs => write!(f, "bs")?, + HttpMessageComponentParam::Tr => write!(f, "tr")?, + HttpMessageComponentParam::Req => write!(f, "req")?, + HttpMessageComponentParam::Name(name) => write!(f, "name=\"{name}\"")?, } + Ok(()) } } @@ -70,7 +73,7 @@ pub struct HttpMessageComponentParams(pub IndexSet); impl std::hash::Hash for HttpMessageComponentParams { fn hash(&self, state: &mut H) { - let mut params = self.0.iter().map(|v| v.clone().into()).collect::>(); + let mut params = self.0.iter().map(|param| format!("{param}")).collect::>(); params.sort(); params.hash(state); } @@ -88,15 +91,10 @@ impl TryFrom<&sfv::Parameters> for HttpMessageComponentParams { } impl std::fmt::Display for HttpMessageComponentParams { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if !self.0.is_empty() { - write!( - f, - ";{}", - self.0.iter().map(|v| v.clone().into()).collect::>().join(";") - ) - } else { - Ok(()) + for param in self.0.iter() { + write!(f, ";{param}")?; } + Ok(()) } } diff --git a/httpsig/src/signature_base.rs b/httpsig/src/signature_base.rs index 3abaee2..fe2273d 100644 --- a/httpsig/src/signature_base.rs +++ b/httpsig/src/signature_base.rs @@ -1,3 +1,11 @@ +use std::fmt::{self, Write as _}; +use std::io::Write as _; + +use base64::{Engine as _, engine::general_purpose}; +use indexmap::IndexMap; +use rustc_hash::FxBuildHasher; +use sfv::{BareItem, Item, ListEntry, Parser}; + use crate::{ crypto::SigningKey, error::{HttpSigError, HttpSigResult}, @@ -5,10 +13,6 @@ use crate::{ prelude::{VerifyingKey, message_component::HttpMessageComponentId}, signature_params::HttpSignatureParams, }; -use base64::{Engine as _, engine::general_purpose}; -use indexmap::IndexMap; -use rustc_hash::FxBuildHasher; -use sfv::{BareItem, Item, ListEntry, Parser}; /// IndexMap of signature name and HttpSignatureHeaders pub type HttpSignatureHeadersMap = IndexMap; @@ -110,19 +114,26 @@ impl HttpSignatureHeaders { /// Returns the signature value of "Signature" http header in the form of "=::" pub fn signature_header_value(&self, signature_name: &str) -> String { - format!("{}=:{}:", signature_name, self.signature) + const NON_RSA_SIGNATURE_BUFFER_SIZE: usize = 128; + let mut buf = String::with_capacity(NON_RSA_SIGNATURE_BUFFER_SIZE); + write!(buf, "{}=:{}:", signature_name, self.signature).expect("no allocation error"); + buf } /// Returns the signature input value of "Signature-Input" http header in the form of "=" pub fn signature_input_header_value(&self, signature_name: &str) -> String { - format!("{}={}", signature_name, self.signature_params) + const TYPICAL_SIGNATURE_INPUT_BUFFER_SIZE: usize = 256; + let mut buf = String::with_capacity(TYPICAL_SIGNATURE_INPUT_BUFFER_SIZE); + write!(buf, "{}={}", signature_name, self.signature_params).expect("no allocation error"); + buf } } #[derive(Debug, Clone)] /// Wrapper struct of raw signature bytes pub struct HttpSignature(Vec); -impl std::fmt::Display for HttpSignature { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + +impl fmt::Display for HttpSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let signature_value = general_purpose::STANDARD.encode(&self.0); write!(f, "{}", signature_value) } @@ -166,15 +177,17 @@ impl HttpSignatureBase { }) } - /// Returns the signature base string as bytes to be signed - pub fn as_bytes(&self) -> Vec { - let string = self.to_string(); - string.as_bytes().to_vec() + /// Returns the signature base string as vector of bytes to be signed. + pub fn to_vec(&self) -> Vec { + const TYPICAL_SIGNATURE_BASE_UPPER_BOUND_SIZE: usize = 512; + let mut buf = Vec::with_capacity(TYPICAL_SIGNATURE_BASE_UPPER_BOUND_SIZE); + write!(buf, "{}", self).expect("no allocation error"); + buf } /// Build signature from given signing key pub fn build_raw_signature(&self, signing_key: &impl SigningKey) -> HttpSigResult> { - let bytes = self.as_bytes(); + let bytes = self.to_vec(); signing_key.sign(&bytes) } @@ -195,7 +208,7 @@ impl HttpSignatureBase { )); } let signature_bytes = signature.0.as_slice(); - verifying_key.verify(&self.as_bytes(), signature_bytes) + verifying_key.verify(&self.to_vec(), signature_bytes) } /// Get key id from signature params @@ -219,15 +232,14 @@ impl HttpSignatureBase { } } -impl std::fmt::Display for HttpSignatureBase { - 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'); +impl fmt::Display for HttpSignatureBase { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for component in &self.component_lines { + // writeln appends `\n` on all platforms + writeln!(f, "{}", component)?; } - signature_base.push_str(&format!("\"@signature-params\": {}", self.signature_params)); - write!(f, "{}", signature_base) + // no final newline according to the [Signature Base algorithm](https://www.rfc-editor.org/rfc/rfc9421#section-2.5) + write!(f, "\"@signature-params\": {}", self.signature_params) } }