From 381bc66333d3ccd7af2d8b173afee3c523f28dec Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Fri, 12 Sep 2025 00:32:28 +0300 Subject: [PATCH 01/27] security: make HS256 validation constant-time --- httpsig/src/crypto/symmetric.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/httpsig/src/crypto/symmetric.rs b/httpsig/src/crypto/symmetric.rs index afaed16..ad36584 100644 --- a/httpsig/src/crypto/symmetric.rs +++ b/httpsig/src/crypto/symmetric.rs @@ -53,13 +53,14 @@ impl super::SigningKey for SharedKey { impl super::VerifyingKey for SharedKey { /// Verify the mac fn verify(&self, data: &[u8], expected_mac: &[u8]) -> HttpSigResult<()> { - use super::SigningKey; - debug!("Verify HmacSha256"); - let calcurated_mac = self.sign(data)?; - if calcurated_mac == expected_mac { - Ok(()) - } else { - Err(HttpSigError::InvalidSignature("Invalid MAC".to_string())) + match self { + SharedKey::HmacSha256(key) => { + debug!("Verify HmacSha256"); + let mut mac = HmacSha256::new_from_slice(key).unwrap(); + mac.update(data); + mac.verify_slice(expected_mac) + .map_err(|_| HttpSigError::InvalidSignature("Invalid MAC".to_string())) + } } } From 817e35e825e3f9f5057da0533d294624b3a8ecfe Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 12 Sep 2025 15:16:03 +0900 Subject: [PATCH 02/27] bump --- Cargo.toml | 2 +- httpsig-hyper/Cargo.toml | 16 ++++++++-------- httpsig/Cargo.toml | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a082bcd..b645a26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.18" +version = "0.0.19" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 7959140..9ada008 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,32 +13,32 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -httpsig = { path = "../httpsig", version = "0.0.18" } +httpsig = { path = "../httpsig", version = "0.0.19" } -thiserror = { version = "2.0.11" } +thiserror = { version = "2.0.16" } tracing = { version = "0.1.41" } futures = { version = "0.3.31", default-features = false, features = [ "std", "async-await", ] } -indexmap = { version = "2.7.1" } +indexmap = { version = "2.11.1" } # content digest with rfc8941 structured field values -sha2 = { version = "0.10.8", default-features = false } +sha2 = { version = "0.10.9", default-features = false } sfv = { version = "0.10.4" } # encoding base64 = { version = "0.22.1" } # for request and response headers -http = { version = "1.2.0" } +http = { version = "1.3.1" } http-body = { version = "1.0.1" } -http-body-util = { version = "0.1.2" } -bytes = { version = "1.10.0" } +http-body-util = { version = "0.1.3" } +bytes = { version = "1.10.1" } [dev-dependencies] -tokio = { version = "1.43.0", default-features = false, features = [ +tokio = { version = "1.47.1", default-features = false, features = [ "macros", "rt-multi-thread", ] } # testing only diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index 187ead5..7a999a3 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -13,12 +13,12 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -thiserror = { version = "2.0.11" } +thiserror = { version = "2.0.16" } tracing = { version = "0.1.41" } rustc-hash = { version = "2.1.1" } -indexmap = { version = "2.7.1" } +indexmap = { version = "2.11.1" } fxhash = { version = "0.2.1" } -rand = { version = "0.9.0" } +rand = { version = "0.9.2" } # crypto pkcs8 = { version = "0.10.2", default-features = false, features = ["pem"] } @@ -39,8 +39,8 @@ p384 = { version = "0.13.1", default-features = false, features = [ "ecdsa", ] } hmac = { version = "0.12.1" } -sha2 = { version = "0.10.8", default-features = false } -bytes = { version = "1.10.0" } +sha2 = { version = "0.10.9", default-features = false } +bytes = { version = "1.10.1" } # encoding base64 = { version = "0.22.1" } From 30710dbf15b6e252fcb5603c140071e5873a8785 Mon Sep 17 00:00:00 2001 From: Serhij S Date: Tue, 25 Nov 2025 01:27:51 +0100 Subject: [PATCH 03/27] removed unmaintained fxhash crate --- httpsig/Cargo.toml | 1 - httpsig/src/message_component/component.rs | 2 +- httpsig/src/message_component/component_param.rs | 2 +- httpsig/src/signature_base.rs | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index 7a999a3..b6de663 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -17,7 +17,6 @@ thiserror = { version = "2.0.16" } tracing = { version = "0.1.41" } rustc-hash = { version = "2.1.1" } indexmap = { version = "2.11.1" } -fxhash = { version = "0.2.1" } rand = { version = "0.9.2" } # crypto diff --git a/httpsig/src/message_component/component.rs b/httpsig/src/message_component/component.rs index 89dda10..a74ade7 100644 --- a/httpsig/src/message_component/component.rs +++ b/httpsig/src/message_component/component.rs @@ -186,7 +186,7 @@ pub(super) fn build_http_field_component( #[cfg(test)] mod tests { use super::*; - type IndexSet = indexmap::IndexSet; + type IndexSet = indexmap::IndexSet; #[test] fn test_from_serialized_string_derived() { diff --git a/httpsig/src/message_component/component_param.rs b/httpsig/src/message_component/component_param.rs index 736da07..831b603 100644 --- a/httpsig/src/message_component/component_param.rs +++ b/httpsig/src/message_component/component_param.rs @@ -1,7 +1,7 @@ use crate::error::{HttpSigError, HttpSigResult}; use sfv::{Parser, SerializeValue}; -type IndexSet = indexmap::IndexSet; +type IndexSet = indexmap::IndexSet; /* ---------------------------------------------------------------- */ #[derive(PartialEq, Eq, Hash, Debug, Clone)] diff --git a/httpsig/src/signature_base.rs b/httpsig/src/signature_base.rs index 04da34a..0e21a76 100644 --- a/httpsig/src/signature_base.rs +++ b/httpsig/src/signature_base.rs @@ -6,7 +6,7 @@ use crate::{ signature_params::HttpSignatureParams, }; use base64::{engine::general_purpose, Engine as _}; -use fxhash::FxBuildHasher; +use rustc_hash::FxBuildHasher; use indexmap::IndexMap; use sfv::{BareItem, Item, ListEntry, Parser}; From 806c3df2dcc134bc44cda02fa2d1b05b966fce34 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 25 Nov 2025 13:29:24 +0900 Subject: [PATCH 04/27] fix test --- httpsig/src/crypto/asymmetric.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig/src/crypto/asymmetric.rs b/httpsig/src/crypto/asymmetric.rs index c8e3db2..a1e998d 100644 --- a/httpsig/src/crypto/asymmetric.rs +++ b/httpsig/src/crypto/asymmetric.rs @@ -365,7 +365,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= let sk = SecretKey::from_bytes(AlgorithmName::EcdsaP256Sha256, es256_sk.to_bytes().as_ref()).unwrap(); assert!(matches!(sk, SecretKey::EcdsaP256Sha256(_))); let pk_bytes = es256_pk.as_affine().to_bytes(); - let pk = PublicKey::from_bytes(AlgorithmName::EcdsaP256Sha256, pk_bytes.as_slice()).unwrap(); + let pk = PublicKey::from_bytes(AlgorithmName::EcdsaP256Sha256, pk_bytes.as_ref()).unwrap(); assert!(matches!(pk, PublicKey::EcdsaP256Sha256(_))); } From 985566a93bb99fd7e5cd3a01f2ee2430f5a2abfc Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 25 Nov 2025 14:10:10 +0900 Subject: [PATCH 05/27] deps: sfv-0.14 --- httpsig-hyper/Cargo.toml | 2 +- httpsig-hyper/src/hyper_content_digest.rs | 11 +++--- httpsig/Cargo.toml | 2 +- httpsig/src/message_component/component_id.rs | 12 +++++-- .../src/message_component/component_name.rs | 2 +- .../src/message_component/component_param.rs | 36 ++++++++++--------- httpsig/src/signature_base.rs | 20 +++++++---- httpsig/src/signature_params.rs | 26 ++++++++------ 8 files changed, 65 insertions(+), 46 deletions(-) diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 9ada008..aeeec7b 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -25,7 +25,7 @@ indexmap = { version = "2.11.1" } # content digest with rfc8941 structured field values sha2 = { version = "0.10.9", default-features = false } -sfv = { version = "0.10.4" } +sfv = { version = "0.14.0" } # encoding base64 = { version = "0.22.1" } diff --git a/httpsig-hyper/src/hyper_content_digest.rs b/httpsig-hyper/src/hyper_content_digest.rs index 721c641..3c3a9bb 100644 --- a/httpsig-hyper/src/hyper_content_digest.rs +++ b/httpsig-hyper/src/hyper_content_digest.rs @@ -5,9 +5,9 @@ use bytes::Bytes; use http::{Request, Response}; use http_body::Body; use http_body_util::{combinators::BoxBody, BodyExt, Full}; -use sfv::FromStr; use sha2::Digest; use std::future::Future; +use std::str::FromStr; // hyper's http specific extension to generate and verify http signature @@ -209,7 +209,8 @@ async fn extract_content_digest(header_map: &http::HeaderMap) -> HyperDigestResu .get(CONTENT_DIGEST_HEADER) .ok_or(HyperDigestError::NoDigestHeader("No content-digest header".to_string()))? .to_str()?; - let indexmap = sfv::Parser::parse_dictionary(content_digest_header.as_bytes()) + let indexmap = sfv::Parser::new(content_digest_header) + .parse::() .map_err(|e| HyperDigestError::InvalidHeaderValue(e.to_string()))?; if indexmap.len() != 1 { return Err(HyperDigestError::InvalidHeaderValue( @@ -217,12 +218,12 @@ async fn extract_content_digest(header_map: &http::HeaderMap) -> HyperDigestResu )); }; let (cd_type, cd) = indexmap.iter().next().unwrap(); - let cd_type = ContentDigestType::from_str(cd_type) + let cd_type = ContentDigestType::from_str(cd_type.as_str()) .map_err(|e| HyperDigestError::InvalidHeaderValue(format!("Invalid Content-Digest type: {e}")))?; if !matches!( cd, sfv::ListEntry::Item(sfv::Item { - bare_item: sfv::BareItem::ByteSeq(_), + bare_item: sfv::BareItem::ByteSequence(_), .. }) ) { @@ -233,7 +234,7 @@ async fn extract_content_digest(header_map: &http::HeaderMap) -> HyperDigestResu let cd = match cd { sfv::ListEntry::Item(sfv::Item { - bare_item: sfv::BareItem::ByteSeq(cd), + bare_item: sfv::BareItem::ByteSequence(cd), .. }) => cd, _ => unreachable!(), diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index b6de663..e82a4f6 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -45,7 +45,7 @@ bytes = { version = "1.10.1" } base64 = { version = "0.22.1" } # for rfc8941 structured field values -sfv = { version = "0.10.4" } +sfv = { version = "0.14.0" } [dev-dependencies] rand-085 = { package = "rand", version = "0.8.5" } # testing only diff --git a/httpsig/src/message_component/component_id.rs b/httpsig/src/message_component/component_id.rs index c8bebac..c55168c 100644 --- a/httpsig/src/message_component/component_id.rs +++ b/httpsig/src/message_component/component_id.rs @@ -35,11 +35,17 @@ impl TryFrom<&str> for HttpMessageComponentId { /// But accept string in the form of `` (without double quotations) when no param is given fn try_from(val: &str) -> HttpSigResult { let val = val.trim(); - let item = if !val.starts_with('"') && !val.ends_with('"') && !val.is_empty() && !val.contains('"') { + let item: sfv::Item = if !val.starts_with('"') && !val.ends_with('"') && !val.is_empty() && !val.contains('"') { // maybe insufficient, but it's enough for now - Parser::parse_item(format!("\"{val}\"").as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))? + Parser::new(format!("\"{val}\"").as_str()) + .parse() + .map_err(|e| HttpSigError::ParseSFVError(e.to_string()))? + // Parser::parse_item(format!("\"{val}\"").as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))? } else { - Parser::parse_item(val.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))? + Parser::new(val) + .parse() + .map_err(|e| HttpSigError::ParseSFVError(e.to_string()))? + // Parser::parse_item(val.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))? }; let res = Self { diff --git a/httpsig/src/message_component/component_name.rs b/httpsig/src/message_component/component_name.rs index 916f399..7164157 100644 --- a/httpsig/src/message_component/component_name.rs +++ b/httpsig/src/message_component/component_name.rs @@ -16,7 +16,7 @@ impl TryFrom<&BareItem> for HttpMessageComponentName { fn try_from(value: &BareItem) -> HttpSigResult { match value { BareItem::String(name) => { - if name.starts_with('@') { + if name.as_str().starts_with('@') { Ok(Self::Derived(DerivedComponentName::from(name.as_str()))) } else { Ok(Self::HttpField(name.to_string())) diff --git a/httpsig/src/message_component/component_param.rs b/httpsig/src/message_component/component_param.rs index 831b603..04717ca 100644 --- a/httpsig/src/message_component/component_param.rs +++ b/httpsig/src/message_component/component_param.rs @@ -1,5 +1,5 @@ use crate::error::{HttpSigError, HttpSigResult}; -use sfv::{Parser, SerializeValue}; +use sfv::{FieldType, Parser}; type IndexSet = indexmap::IndexSet; @@ -46,13 +46,13 @@ impl TryFrom<(&str, &sfv::BareItem)> for HttpMessageComponentParam { "tr" => Ok(Self::Tr), "req" => Ok(Self::Req), "name" => { - let name = val.as_str().ok_or(HttpSigError::InvalidComponentParam( + let name = val.as_string().ok_or(HttpSigError::InvalidComponentParam( "Invalid http field param: name".to_string(), ))?; Ok(Self::Name(name.to_string())) } "key" => { - let key = val.as_str().ok_or(HttpSigError::InvalidComponentParam( + let key = val.as_string().ok_or(HttpSigError::InvalidComponentParam( "Invalid http field param: key".to_string(), ))?; Ok(Self::Key(key.to_string())) @@ -106,10 +106,10 @@ pub(super) fn handle_params_sf(field_values: &mut [String]) -> HttpSigResult<()> let parsed_list = field_values .iter() .map(|v| { - if let Ok(list) = Parser::parse_list(v.as_bytes()) { - list.serialize_value() - } else if let Ok(dict) = Parser::parse_dictionary(v.as_bytes()) { - dict.serialize_value() + if let Ok(list) = Parser::new(v).parse::() { + list.serialize().ok_or("Failed to serialize structured field value for sf") + } else if let Ok(dict) = Parser::new(v).parse::() { + dict.serialize().ok_or("Failed to serialize structured field value for sf") } else { Err("invalid structured field value for sf") } @@ -129,7 +129,8 @@ pub(super) fn handle_params_sf(field_values: &mut [String]) -> HttpSigResult<()> pub(super) fn handle_params_key_into(field_values: &[String], key: &str) -> HttpSigResult> { let dicts = field_values .iter() - .map(|v| Parser::parse_dictionary(v.as_bytes())) + .map(|v| Parser::new(v.as_str()).parse() as Result) + // Parser::parse_dictionary(v.as_bytes())) .collect::, _>>() .map_err(|e| HttpSigError::InvalidComponentParam(format!("Failed to parse structured field value: {e}")))?; @@ -138,11 +139,12 @@ pub(super) fn handle_params_key_into(field_values: &[String], key: &str) -> Http .filter_map(|dict| { dict.get(key).map(|v| { let sfvalue: sfv::List = vec![v.clone()]; - sfvalue.serialize_value() + // sfvalue.serialize_value() + sfvalue.serialize() }) }) - .collect::, _>>() - .map_err(|e| HttpSigError::InvalidComponentParam(format!("Failed to serialize structured field value: {e}")))?; + .collect::>>() + .ok_or_else(|| HttpSigError::InvalidComponentParam(format!("Failed to serialize structured field value")))?; Ok(found_entries) } @@ -157,19 +159,19 @@ mod tests { fn parser_test() { // Parsing structured field value of Item type. let item_header_input = "12.445;foo=bar"; - let item = Parser::parse_item(item_header_input.as_bytes()).unwrap(); - assert_eq!(item.serialize_value().unwrap(), item_header_input); + let item = Parser::new(item_header_input).parse::().unwrap(); + assert_eq!(item.serialize(), item_header_input); // Parsing structured field value of List type. let list_header_input = " 1; a=tok, (\"foo\" \"bar\" );baz, ( )"; - let list = Parser::parse_list(list_header_input.as_bytes()).unwrap(); - assert_eq!(list.serialize_value().unwrap(), "1;a=tok, (\"foo\" \"bar\");baz, ()"); + let list = Parser::new(list_header_input).parse::().unwrap(); + assert_eq!(list.serialize().unwrap(), "1;a=tok, (\"foo\" \"bar\");baz, ()"); // Parsing structured field value of Dictionary type. let dict_header_input = "a=?0, b, c; foo=bar, rating=1.5, fruits=(apple pear), d"; - let dict = Parser::parse_dictionary(dict_header_input.as_bytes()).unwrap(); + let dict = Parser::new(dict_header_input).parse::().unwrap(); assert_eq!( - dict.serialize_value().unwrap(), + dict.serialize().unwrap(), "a=?0, b, c;foo=bar, rating=1.5, fruits=(apple pear), d" ); } diff --git a/httpsig/src/signature_base.rs b/httpsig/src/signature_base.rs index 0e21a76..125fcea 100644 --- a/httpsig/src/signature_base.rs +++ b/httpsig/src/signature_base.rs @@ -6,8 +6,8 @@ use crate::{ signature_params::HttpSignatureParams, }; use base64::{engine::general_purpose, Engine as _}; -use rustc_hash::FxBuildHasher; use indexmap::IndexMap; +use rustc_hash::FxBuildHasher; use sfv::{BareItem, Item, ListEntry, Parser}; /// IndexMap of signature name and HttpSignatureHeaders @@ -30,10 +30,16 @@ pub struct HttpSignatureHeaders { impl HttpSignatureHeaders { /// Generates (possibly multiple) HttpSignatureHeaders from signature and signature-input header values pub fn try_parse(signature_header: &str, signature_input_header: &str) -> HttpSigResult { - let signature_input = - Parser::parse_dictionary(signature_input_header.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; - let signature = - Parser::parse_dictionary(signature_header.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; + 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) + .parse() + .map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; + // let signature_input = + // Parser::parse_dictionary(signature_input_header.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; + // let signature = + // Parser::parse_dictionary(signature_header.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; if signature.len() != signature_input.len() { return Err(HttpSigError::BuildSignatureHeaderError( @@ -50,7 +56,7 @@ impl HttpSignatureHeaders { matches!( v, ListEntry::Item(Item { - bare_item: BareItem::ByteSeq(_), + bare_item: BareItem::ByteSequence(_), .. }) ) @@ -73,7 +79,7 @@ impl HttpSignatureHeaders { let signature_bytes = match signature.get(k) { Some(ListEntry::Item(Item { - bare_item: BareItem::ByteSeq(v), + bare_item: BareItem::ByteSequence(v), .. })) => v, _ => unreachable!(), diff --git a/httpsig/src/signature_params.rs b/httpsig/src/signature_params.rs index c2827ae..892df06 100644 --- a/httpsig/src/signature_params.rs +++ b/httpsig/src/signature_params.rs @@ -7,7 +7,7 @@ use crate::{ }; use base64::{engine::general_purpose, Engine as _}; use rand::Rng; -use sfv::{ListEntry, Parser, SerializeValue}; +use sfv::{FieldType, ListEntry, Parser}; use std::time::{SystemTime, UNIX_EPOCH}; const DEFAULT_DURATION: u64 = 300; @@ -167,9 +167,10 @@ impl TryFrom<&ListEntry> for HttpSignatureParams { .items .iter() .map(|v| { - v.serialize_value() - .map_err(|e| HttpSigError::ParseSFVError(e.to_string())) - .and_then(|v| HttpMessageComponentId::try_from(v.as_str())) + HttpMessageComponentId::try_from(v.serialize().as_str()) + // v.serialize_value() + // .map_err(|e| HttpSigError::ParseSFVError(e.to_string())) + // .and_then(|v| HttpMessageComponentId::try_from(v.as_str())) }) .collect::, _>>()?; @@ -193,12 +194,12 @@ impl TryFrom<&ListEntry> for HttpSignatureParams { .params .iter() .for_each(|(key, bare_item)| match key.as_str() { - "created" => params.created = bare_item.as_int().map(|v| v as u64), - "expires" => params.expires = bare_item.as_int().map(|v| v as u64), - "nonce" => params.nonce = bare_item.as_str().map(|v| v.to_string()), - "alg" => params.alg = bare_item.as_str().map(|v| v.to_string()), - "keyid" => params.keyid = bare_item.as_str().map(|v| v.to_string()), - "tag" => params.tag = bare_item.as_str().map(|v| v.to_string()), + "created" => params.created = bare_item.as_integer().map(|v| v.try_into().unwrap()), + "expires" => params.expires = bare_item.as_integer().map(|v| v.try_into().unwrap()), + "nonce" => params.nonce = bare_item.as_string().map(|v| v.to_string()), + "alg" => params.alg = bare_item.as_string().map(|v| v.to_string()), + "keyid" => params.keyid = bare_item.as_string().map(|v| v.to_string()), + "tag" => params.tag = bare_item.as_string().map(|v| v.to_string()), _ => { error!("Ignore invalid signature parameter: {}", key) } @@ -211,7 +212,10 @@ impl TryFrom<&str> for HttpSignatureParams { type Error = HttpSigError; /// Convert from string to HttpSignatureParams fn try_from(value: &str) -> HttpSigResult { - let sfv_parsed = Parser::parse_list(value.as_bytes()).map_err(|e| HttpSigError::ParseSFVError(e.to_string()))?; + let sfv_parsed: sfv::List = Parser::new(value) + .parse() + .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())); } From afa6289a26efed97cee07e5fd0c6c865ac187691 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 25 Nov 2025 14:13:20 +0900 Subject: [PATCH 06/27] bump bump --- Cargo.toml | 2 +- httpsig-hyper/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b645a26..d7c24ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.19" +version = "0.0.20" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index aeeec7b..548a6e4 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,7 +13,7 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -httpsig = { path = "../httpsig", version = "0.0.19" } +httpsig = { path = "../httpsig", version = "0.0.20" } thiserror = { version = "2.0.16" } tracing = { version = "0.1.41" } From a13dfb3aec9ce63bae3bc4000022152f863418ed Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Tue, 10 Feb 2026 19:13:33 +0900 Subject: [PATCH 07/27] chore(deps): rand-0.10.0 --- httpsig-hyper/Cargo.toml | 12 ++++++------ httpsig/Cargo.toml | 12 ++++++------ httpsig/src/signature_params.rs | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 548a6e4..ebb786f 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -15,13 +15,13 @@ rust-version.workspace = true [dependencies] httpsig = { path = "../httpsig", version = "0.0.20" } -thiserror = { version = "2.0.16" } -tracing = { version = "0.1.41" } +thiserror = { version = "2.0.18" } +tracing = { version = "0.1.44" } futures = { version = "0.3.31", default-features = false, features = [ "std", "async-await", ] } -indexmap = { version = "2.11.1" } +indexmap = { version = "2.11.4" } # content digest with rfc8941 structured field values sha2 = { version = "0.10.9", default-features = false } @@ -31,14 +31,14 @@ sfv = { version = "0.14.0" } base64 = { version = "0.22.1" } # for request and response headers -http = { version = "1.3.1" } +http = { version = "1.4.0" } http-body = { version = "1.0.1" } http-body-util = { version = "0.1.3" } -bytes = { version = "1.10.1" } +bytes = { version = "1.11.1" } [dev-dependencies] -tokio = { version = "1.47.1", default-features = false, features = [ +tokio = { version = "1.49.0", default-features = false, features = [ "macros", "rt-multi-thread", ] } # testing only diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index e82a4f6..e798575 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -13,17 +13,17 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -thiserror = { version = "2.0.16" } -tracing = { version = "0.1.41" } +thiserror = { version = "2.0.18" } +tracing = { version = "0.1.44" } rustc-hash = { version = "2.1.1" } -indexmap = { version = "2.11.1" } -rand = { version = "0.9.2" } +indexmap = { version = "2.11.4" } +rand = { version = "0.10.0" } # crypto pkcs8 = { version = "0.10.2", default-features = false, features = ["pem"] } spki = { version = "0.7.3", default-features = false, features = ["pem"] } sec1 = { version = "0.7.3", default-features = false, features = ["der"] } -ed25519-compact = { version = "2.1.1", default-features = false, features = [ +ed25519-compact = { version = "2.2.0", default-features = false, features = [ "random", ] } ecdsa = { version = "0.16.9", default-features = false, features = [ @@ -39,7 +39,7 @@ p384 = { version = "0.13.1", default-features = false, features = [ ] } hmac = { version = "0.12.1" } sha2 = { version = "0.10.9", default-features = false } -bytes = { version = "1.10.1" } +bytes = { version = "1.11.1" } # encoding base64 = { version = "0.22.1" } diff --git a/httpsig/src/signature_params.rs b/httpsig/src/signature_params.rs index 892df06..788db68 100644 --- a/httpsig/src/signature_params.rs +++ b/httpsig/src/signature_params.rs @@ -6,7 +6,7 @@ use crate::{ util::has_unique_elements, }; use base64::{engine::general_purpose, Engine as _}; -use rand::Rng; +use rand::RngExt; use sfv::{FieldType, ListEntry, Parser}; use std::time::{SystemTime, UNIX_EPOCH}; From 7db212cb71be59571559bdc52485a4292343dfe1 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Thu, 12 Feb 2026 22:42:11 +0900 Subject: [PATCH 08/27] feat: support blocking apis --- httpsig-hyper/Cargo.toml | 5 + httpsig-hyper/src/hyper_http.rs | 226 ++++++++++++++++++++++++++++++++ httpsig-hyper/src/lib.rs | 50 ++++++- 3 files changed, 280 insertions(+), 1 deletion(-) diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index ebb786f..4185701 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -12,6 +12,11 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["blocking"] +blocking = ["futures/executor"] + + [dependencies] httpsig = { path = "../httpsig", version = "0.0.20" } diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index dd96418..9421b31 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -133,6 +133,89 @@ pub trait MessageSignatureRes { ) -> Result, Self::Error>; } +/* --------------------------------------- */ +#[cfg(feature = "blocking")] +/// A trait about http message signature for request with synchronous signing/verifying key +pub trait MessageSignatureReqSync: MessageSignatureReq { + fn set_message_signature_sync( + &mut self, + signature_params: &HttpSignatureParams, + signing_key: &T, + signature_name: Option<&str>, + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync; + + fn set_message_signatures_sync( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync; + + fn verify_message_signature_sync(&self, verifying_key: &T, key_id: Option<&str>) -> Result + where + Self: Sized, + T: VerifyingKey + Sync; + + fn verify_message_signatures_sync( + &self, + key_and_id: &[(&T, Option<&str>)], + ) -> Result>, Self::Error> + where + Self: Sized, + T: VerifyingKey + Sync; +} + +#[cfg(feature = "blocking")] +/// A trait about http message signature for response with synchronous signing/verifying key +pub trait MessageSignatureResSync: MessageSignatureRes { + fn set_message_signature_sync( + &mut self, + signature_params: &HttpSignatureParams, + signing_key: &T, + signature_name: Option<&str>, + req_for_param: Option<&Request>, + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync, + B: Sync; + + fn set_message_signatures_sync( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + req_for_param: Option<&Request>, + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync, + B: Sync; + + fn verify_message_signature_sync( + &self, + verifying_key: &T, + key_id: Option<&str>, + req_for_param: Option<&Request>, + ) -> Result + where + Self: Sized, + T: VerifyingKey + Sync, + B: Sync; + + fn verify_message_signatures_sync( + &self, + key_and_id: &[(&T, Option<&str>)], + req_for_param: Option<&Request>, + ) -> Result>, Self::Error> + where + Self: Sized, + T: VerifyingKey + Sync, + B: Sync; +} + /* --------------------------------------- */ impl MessageSignature for Request where @@ -378,6 +461,117 @@ where } } +/* --------------------------------------- */ +#[cfg(feature = "blocking")] +impl MessageSignatureReqSync for Request +where + D: Send + Body + Sync, +{ + fn set_message_signature_sync( + &mut self, + signature_params: &HttpSignatureParams, + signing_key: &T, + signature_name: Option<&str>, + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync, + { + 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> + where + Self: Sized, + T: SigningKey + Sync, + { + futures::executor::block_on(self.set_message_signatures(params_key_name)) + } + + fn verify_message_signature_sync(&self, verifying_key: &T, key_id: Option<&str>) -> Result + where + Self: Sized, + T: VerifyingKey + Sync, + { + futures::executor::block_on(self.verify_message_signature(verifying_key, key_id)) + } + + fn verify_message_signatures_sync( + &self, + key_and_id: &[(&T, Option<&str>)], + ) -> Result>, Self::Error> + where + Self: Sized, + T: VerifyingKey + Sync, + { + futures::executor::block_on(self.verify_message_signatures(key_and_id)) + } +} + +#[cfg(feature = "blocking")] +impl MessageSignatureResSync for Response +where + D: Send + Body + Sync, +{ + fn set_message_signature_sync( + &mut self, + signature_params: &HttpSignatureParams, + signing_key: &T, + signature_name: Option<&str>, + req_for_param: Option<&Request>, + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync, + B: Sync, + { + futures::executor::block_on(self.set_message_signature(signature_params, signing_key, signature_name, req_for_param)) + } + + fn set_message_signatures_sync( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + req_for_param: Option<&Request>, + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync, + B: Sync, + { + futures::executor::block_on(self.set_message_signatures(params_key_name, req_for_param)) + } + + fn verify_message_signature_sync( + &self, + verifying_key: &T, + key_id: Option<&str>, + req_for_param: Option<&Request>, + ) -> Result + where + Self: Sized, + T: VerifyingKey + Sync, + B: Sync, + { + futures::executor::block_on(self.verify_message_signature(verifying_key, key_id, req_for_param)) + } + + fn verify_message_signatures_sync( + &self, + key_and_id: &[(&T, Option<&str>)], + req_for_param: Option<&Request>, + ) -> Result>, Self::Error> + where + Self: Sized, + T: VerifyingKey + Sync, + B: Sync, + { + futures::executor::block_on(self.verify_message_signatures(key_and_id, req_for_param)) + } +} + /* --------------------------------------- */ // inner functions /// has message signature inner function @@ -1026,4 +1220,36 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== assert!(verification_res[0].as_ref().unwrap() == "eddsa_sig"); assert!(verification_res[1].as_ref().unwrap() == "p256_sig"); } + + #[cfg(feature = "blocking")] + #[test] + fn test_blocking_set_verify_message_signature_req() { + let mut req = futures::executor::block_on(build_request()); + let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + 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(); + + let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature_sync(&public_key, None); + assert!(verification_res.is_ok()); + } + + #[cfg(feature = "blocking")] + #[test] + fn test_blocking_set_verify_message_signature_res() { + let req = futures::executor::block_on(build_request()); + let mut res = futures::executor::block_on(build_response()); + let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + 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)) + .unwrap(); + + let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); + assert!(verification_res.is_ok()); + } } diff --git a/httpsig-hyper/src/lib.rs b/httpsig-hyper/src/lib.rs index a3fb303..249db5b 100644 --- a/httpsig-hyper/src/lib.rs +++ b/httpsig-hyper/src/lib.rs @@ -42,7 +42,9 @@ impl std::str::FromStr for ContentDigestType { pub use error::{HyperDigestError, HyperDigestResult, HyperSigError, HyperSigResult}; pub use httpsig::prelude; pub use hyper_content_digest::{ContentDigest, RequestContentDigest, ResponseContentDigest}; -pub use hyper_http::{MessageSignature, MessageSignatureReq, MessageSignatureRes}; +pub use hyper_http::{ + MessageSignature, MessageSignatureReq, MessageSignatureReqSync, MessageSignatureRes, MessageSignatureResSync, +}; /* ----------------------------------------------------------------- */ #[cfg(test)] @@ -188,4 +190,50 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= .await; assert!(verification_res.is_err()); } + + #[cfg(feature = "blocking")] + #[test] + fn test_set_verify_request_sync() { + // show usage of set_message_signature_sync and verify_message_signature_sync + + let mut req = futures::executor::block_on(build_request()); + let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let covered_components = COVERED_COMPONENTS_REQ + .iter() + .map(|v| message_component::HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + // 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(); + let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature_sync(&public_key, None); + assert!(verification_res.is_ok()); + } + + #[cfg(feature = "blocking")] + #[test] + fn test_set_verify_response_sync() { + // show usage of set_message_signature_sync and verify_message_signature_sync + let req = futures::executor::block_on(build_request()); + let mut res = futures::executor::block_on(build_response()); + let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let covered_components = COVERED_COMPONENTS_RES + .iter() + .map(|v| message_component::HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + // set key information, alg and keyid + signature_params.set_key_info(&secret_key); + // set signature + res + .set_message_signature_sync(&signature_params, &secret_key, None, Some(&req)) + .unwrap(); + let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); + assert!(verification_res.is_ok()); + } } From 6384e86ed6ed2e3b38c31703dfc825cfd63764ab Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Thu, 12 Feb 2026 22:49:48 +0900 Subject: [PATCH 09/27] chore(docs): update api docs --- httpsig-hyper/src/hyper_http.rs | 18 ++++++++++++++++-- httpsig-hyper/src/lib.rs | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 9421b31..7869bf7 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -135,7 +135,14 @@ pub trait MessageSignatureRes { /* --------------------------------------- */ #[cfg(feature = "blocking")] -/// A trait about http message signature for request with synchronous signing/verifying key +/// Synchronous counterpart of [`MessageSignatureReq`]. +/// +/// Every method delegates to the corresponding async method via `futures::executor::block_on`. +/// +/// # Panics +/// +/// All methods will panic if called from within an async runtime (e.g. a `tokio` task). +/// Use the async [`MessageSignatureReq`] methods instead when you are already in an async context. pub trait MessageSignatureReqSync: MessageSignatureReq { fn set_message_signature_sync( &mut self, @@ -170,7 +177,14 @@ pub trait MessageSignatureReqSync: MessageSignatureReq { } #[cfg(feature = "blocking")] -/// A trait about http message signature for response with synchronous signing/verifying key +/// Synchronous counterpart of [`MessageSignatureRes`]. +/// +/// Every method delegates to the corresponding async method via `futures::executor::block_on`. +/// +/// # Panics +/// +/// All methods will panic if called from within an async runtime (e.g. a `tokio` task). +/// Use the async [`MessageSignatureRes`] methods instead when you are already in an async context. pub trait MessageSignatureResSync: MessageSignatureRes { fn set_message_signature_sync( &mut self, diff --git a/httpsig-hyper/src/lib.rs b/httpsig-hyper/src/lib.rs index 249db5b..b040c21 100644 --- a/httpsig-hyper/src/lib.rs +++ b/httpsig-hyper/src/lib.rs @@ -1,8 +1,24 @@ //! # httpsig-hyper //! //! `httpsig-hyper` is a crate that provides a convenient API for `Hyper` users to handle HTTP signatures. -//! This crate extends hyper's https request and response messages with the ability to generate and verify HTTP signatures. +//! This crate extends hyper's http request and response messages with the ability to generate and verify HTTP signatures. //! Additionally it also provides a way to set and verify content-digest header. +//! +//! ## Async-first design +//! +//! The primary API is fully async, allowing concurrent processing of multiple signatures via +//! [`MessageSignatureReq`] and [`MessageSignatureRes`]. +//! +//! ## Blocking API +//! +//! When the `blocking` feature is enabled (on by default), synchronous wrappers are provided via +//! [`MessageSignatureReqSync`] and [`MessageSignatureResSync`]. These use `futures::executor::block_on` +//! internally and are intended **exclusively for non-async contexts**. +//! +//! # Panics +//! +//! Calling any `*_sync` method from within an async runtime (e.g. inside a `tokio::spawn` task) +//! will panic. If you are already in an async context, use the async methods directly. mod error; mod hyper_content_digest; From cfe86c97f6e56102f94ab651df2d957a3458cd64 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Thu, 12 Feb 2026 22:50:07 +0900 Subject: [PATCH 10/27] chore: bump --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d7c24ab..808636f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.20" +version = "0.0.21" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" From d310194f45246da40cced77611f7f1e400f9f4b4 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Thu, 12 Feb 2026 22:52:01 +0900 Subject: [PATCH 11/27] fix: version --- httpsig-hyper/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 4185701..e58f8a1 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -18,7 +18,7 @@ blocking = ["futures/executor"] [dependencies] -httpsig = { path = "../httpsig", version = "0.0.20" } +httpsig = { path = "../httpsig", version = "0.0.21" } thiserror = { version = "2.0.18" } tracing = { version = "0.1.44" } From 6eeef5fb60e1be5f031ec2ce7a90f8397746eb65 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 13 Feb 2026 22:13:23 +0900 Subject: [PATCH 12/27] feat: support rsassa-pkcs-v1.5-sha256 and rsassa-pss-sha512, braking api changes --- Cargo.toml | 2 +- httpsig-hyper/Cargo.toml | 2 +- httpsig-hyper/README.md | 3 +- httpsig-hyper/examples/hyper-request.rs | 8 +- httpsig-hyper/examples/hyper-response.rs | 8 +- httpsig-hyper/src/hyper_http.rs | 93 ++++--- httpsig-hyper/src/lib.rs | 26 +- httpsig/Cargo.toml | 10 + httpsig/src/crypto/asymmetric.rs | 302 +++++++++++++++++++---- httpsig/src/crypto/mod.rs | 31 ++- httpsig/src/crypto/symmetric.rs | 13 +- httpsig/src/error.rs | 4 + httpsig/src/lib.rs | 14 +- httpsig/src/signature_params.rs | 2 +- 14 files changed, 405 insertions(+), 113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 808636f..06b5f55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.21" +version = "0.0.22" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index e58f8a1..79522e1 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -18,7 +18,7 @@ blocking = ["futures/executor"] [dependencies] -httpsig = { path = "../httpsig", version = "0.0.21" } +httpsig = { path = "../httpsig", version = "0.0.22" } thiserror = { version = "2.0.18" } tracing = { version = "0.1.44" } diff --git a/httpsig-hyper/README.md b/httpsig-hyper/README.md index 2db5ec6..076e0a8 100644 --- a/httpsig-hyper/README.md +++ b/httpsig-hyper/README.md @@ -27,7 +27,8 @@ If you need to verify the body of a given message when `content-digest` is cover ```rust // first verifies the signature according to `signature-input` header -let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); +let alg = AlgorithmName::Ed25519; +let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); let signature_verification = req.verify_message_signature(&public_key, None).await; assert!(verification_res.is_ok()); diff --git a/httpsig-hyper/examples/hyper-request.rs b/httpsig-hyper/examples/hyper-request.rs index ec0ef49..d9243f3 100644 --- a/httpsig-hyper/examples/hyper-request.rs +++ b/httpsig-hyper/examples/hyper-request.rs @@ -43,7 +43,7 @@ async fn sender_ed25519(req: &mut Request) { let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); // set signing/verifying key information, alg and keyid with ed25519 - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); signature_params.set_key_info(&secret_key); // set signature with custom signature name @@ -65,7 +65,7 @@ async fn sender_hs256(req: &mut Request) { let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); // set signing/verifying key information, alg and keyid and random noce with hmac-sha256 - let shared_key = SharedKey::from_base64(HMACSHA256_SECRET_KEY).unwrap(); + let shared_key = SharedKey::from_base64(&AlgorithmName::HmacSha256, HMACSHA256_SECRET_KEY).unwrap(); signature_params.set_key_info(&shared_key); signature_params.set_random_nonce(); @@ -81,7 +81,7 @@ where B: http_body::Body + Send + Sync, { println!("Verifying ED25519 signature"); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let key_id = public_key.key_id(); // verify signature with checking key_id @@ -94,7 +94,7 @@ where B: http_body::Body + Send + Sync, { println!("Verifying HMAC-SHA256 signature"); - let shared_key = SharedKey::from_base64(HMACSHA256_SECRET_KEY).unwrap(); + let shared_key = SharedKey::from_base64(&AlgorithmName::HmacSha256, HMACSHA256_SECRET_KEY).unwrap(); let key_id = VerifyingKey::key_id(&shared_key); // verify signature with checking key_id diff --git a/httpsig-hyper/examples/hyper-response.rs b/httpsig-hyper/examples/hyper-response.rs index 9b12524..e096d29 100644 --- a/httpsig-hyper/examples/hyper-response.rs +++ b/httpsig-hyper/examples/hyper-response.rs @@ -55,7 +55,7 @@ async fn sender_ed25519(res: &mut Response, received_req: &Request, received_req: &Request bool; /// Extract all key ids for signature bases contained in the request headers - fn get_key_ids(&self) -> Result, Self::Error>; + fn get_alg_key_ids(&self) -> Result, Self::Error>; /// Extract all signature params used to generate signature bases contained in the request headers fn get_signature_params(&self) -> Result, Self::Error>; @@ -243,9 +243,9 @@ where } /// Extract all signature bases contained in the request headers - fn get_key_ids(&self) -> HyperSigResult> { + fn get_alg_key_ids(&self) -> HyperSigResult> { let req_or_res = RequestOrResponse::Request(self); - get_key_ids_inner(&req_or_res) + get_alg_key_ids_inner(&req_or_res) } /// Extract all signature params used to generate signature bases contained in the request headers @@ -358,9 +358,9 @@ where } /// Extract all key ids for signature bases contained in the response headers - fn get_key_ids(&self) -> Result, Self::Error> { + fn get_alg_key_ids(&self) -> Result, Self::Error> { let req_or_res = RequestOrResponse::Response(self); - get_key_ids_inner(&req_or_res) + get_alg_key_ids_inner(&req_or_res) } /// Extract all signature params used to generate signature bases contained in the response headers @@ -594,11 +594,28 @@ fn has_message_signature_inner(headers: &HeaderMap) -> bool { } /// get key ids inner function -fn get_key_ids_inner(req_or_res: &RequestOrResponse) -> HyperSigResult> { +fn get_alg_key_ids_inner( + req_or_res: &RequestOrResponse, +) -> HyperSigResult> { let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; let res = signature_headers_map .iter() - .filter_map(|(name, headers)| headers.signature_params().keyid.clone().map(|key_id| (name.clone(), key_id))) + .filter_map(|(name, headers)| { + let alg = headers + .signature_params() + .alg + .clone() + .map(|a| AlgorithmName::from_str(&a)) + .transpose() + .ok() + .flatten(); + let key_id = headers.signature_params().keyid.clone(); + if let (Some(alg), Some(key_id)) = (alg, key_id) { + Some((name.clone(), (alg, key_id))) + } else { + None + } + }) .collect(); Ok(res) } @@ -904,7 +921,7 @@ mod tests { *, }; use http_body_util::Full; - use httpsig::prelude::{PublicKey, SecretKey, SharedKey}; + use httpsig::prelude::{AlgorithmName, PublicKey, SecretKey, SharedKey}; type BoxBody = http_body_util::combinators::BoxBody; @@ -1053,7 +1070,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[tokio::test] async fn test_set_verify_message_signature_req() { let mut req = build_request().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); @@ -1062,7 +1079,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert!(signature_input.starts_with(r##"sig=("@method" "date" "content-type" "content-digest")"##)); // let signature = req.headers().get("signature").unwrap().to_str().unwrap(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature(&public_key, None).await; assert!(verification_res.is_ok()); } @@ -1072,7 +1089,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= let req = build_request().await; let mut res = build_response().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_res()).unwrap(); signature_params.set_key_info(&secret_key); @@ -1090,7 +1107,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert!(signature_input.starts_with(r##"sig=("@status" "@method";req "date" "content-type" "content-digest";req)"##)); // let signature = req.headers().get("signature").unwrap().to_str().unwrap(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; assert!(verification_res.is_ok()); } @@ -1098,7 +1115,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[tokio::test] async fn test_expired_signature() { let mut req = build_request().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); let created = signature_params.created.unwrap(); @@ -1107,7 +1124,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature(&public_key, None).await; assert!(verification_res.is_err()); } @@ -1115,7 +1132,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[tokio::test] async fn test_set_verify_with_signature_name() { let mut req = build_request().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); @@ -1129,7 +1146,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert_eq!(signature_headers_map.len(), 1); assert_eq!(signature_headers_map[0].signature_name(), "custom_sig_name"); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature(&public_key, None).await; assert!(verification_res.is_ok()); } @@ -1137,13 +1154,13 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[tokio::test] async fn test_set_verify_with_key_id() { let mut req = build_request().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); 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(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let key_id = public_key.key_id(); let verification_res = req.verify_message_signature(&public_key, Some(&key_id)).await; assert!(verification_res.is_ok()); @@ -1158,7 +1175,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[tokio::test] async fn test_set_verify_with_key_id_hmac_sha256() { let mut req = build_request().await; - let secret_key = SharedKey::from_base64(HMACSHA256_SECRET_KEY).unwrap(); + let secret_key = SharedKey::from_base64(&AlgorithmName::HmacSha256, HMACSHA256_SECRET_KEY).unwrap(); let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); // Random nonce is highly recommended for HMAC @@ -1166,25 +1183,29 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); - let key_id = VerifyingKey::key_id(&secret_key); - let verification_res = req.verify_message_signature(&secret_key, Some(&key_id)).await; + let org_key_id = VerifyingKey::key_id(&secret_key); + let (alg, key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; + assert_eq!(org_key_id, *key_id); + let verification_key = SharedKey::from_base64(&alg, HMACSHA256_SECRET_KEY).unwrap(); + let verification_res = req.verify_message_signature(&verification_key, Some(&key_id)).await; assert!(verification_res.is_ok()); - let verification_res = req.verify_message_signature(&secret_key, Some("NotFoundKeyId")).await; + let verification_res = req.verify_message_signature(&verification_key, Some("NotFoundKeyId")).await; assert!(verification_res.is_err()); } #[tokio::test] - async fn test_get_key_ids() { + async fn test_get_alg_key_ids() { let mut req = build_request().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); 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(); - let key_ids = req.get_key_ids().unwrap(); + let key_ids = req.get_alg_key_ids().unwrap(); assert_eq!(key_ids.len(), 1); - assert_eq!(key_ids[0], "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); + assert_eq!(key_ids[0].0, AlgorithmName::Ed25519); + assert_eq!(key_ids[0].1, "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); } const P256_SECERT_KEY: &str = r##"-----BEGIN PRIVATE KEY----- @@ -1202,11 +1223,11 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== async fn test_set_verify_multiple_signatures() { let mut req = build_request().await; - let secret_key_eddsa = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key_eddsa = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let mut signature_params_eddsa = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params_eddsa.set_key_info(&secret_key_eddsa); - let secret_key_p256 = SecretKey::from_pem(P256_SECERT_KEY).unwrap(); + let secret_key_p256 = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECERT_KEY).unwrap(); let mut signature_params_hmac = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params_hmac.set_key_info(&secret_key_p256); @@ -1217,8 +1238,8 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== req.set_message_signatures(params_key_name).await.unwrap(); - let public_key_eddsa = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); - let public_key_p256 = PublicKey::from_pem(P256_PUBLIC_KEY).unwrap(); + let public_key_eddsa = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let public_key_p256 = PublicKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_PUBLIC_KEY).unwrap(); let key_id_eddsa = public_key_eddsa.key_id(); let key_id_p256 = public_key_p256.key_id(); @@ -1239,13 +1260,13 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== #[test] fn test_blocking_set_verify_message_signature_req() { let mut req = futures::executor::block_on(build_request()); - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); 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(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature_sync(&public_key, None); assert!(verification_res.is_ok()); } @@ -1255,14 +1276,14 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== fn test_blocking_set_verify_message_signature_res() { let req = futures::executor::block_on(build_request()); let mut res = futures::executor::block_on(build_response()); - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); 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)) .unwrap(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); assert!(verification_res.is_ok()); } diff --git a/httpsig-hyper/src/lib.rs b/httpsig-hyper/src/lib.rs index b040c21..33b4e55 100644 --- a/httpsig-hyper/src/lib.rs +++ b/httpsig-hyper/src/lib.rs @@ -122,7 +122,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= let mut req = build_request().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let covered_components = COVERED_COMPONENTS_REQ .iter() @@ -145,7 +145,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert!(signature.starts_with(r##"custom_sig_name=:"##)); // verify without checking key_id - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + // get algorithm from signature params + let (alg, _key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; + let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature(&public_key, None).await; assert!(verification_res.is_ok()); @@ -165,7 +167,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= let req = build_request().await; let mut res = build_response().await; - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let covered_components = COVERED_COMPONENTS_RES .iter() @@ -187,8 +189,10 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert!(signature_input.starts_with(r##"custom_sig_name=("##)); assert!(signature.starts_with(r##"custom_sig_name=:"##)); - // verify without checking key_id, request must be provided if `req` field param is included - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + // verify without checking key_id, request must be provided if `req` field param is included in signature params + // get algorithm from signature params + let (alg, _key_id) = res.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; + let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; assert!(verification_res.is_ok()); let verification_res = res @@ -213,7 +217,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // show usage of set_message_signature_sync and verify_message_signature_sync let mut req = futures::executor::block_on(build_request()); - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let covered_components = COVERED_COMPONENTS_REQ .iter() .map(|v| message_component::HttpMessageComponentId::try_from(*v)) @@ -224,7 +228,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= signature_params.set_key_info(&secret_key); // set signature req.set_message_signature_sync(&signature_params, &secret_key, None).unwrap(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + + let (alg, _key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; + let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature_sync(&public_key, None); assert!(verification_res.is_ok()); } @@ -235,7 +241,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // show usage of set_message_signature_sync and verify_message_signature_sync let req = futures::executor::block_on(build_request()); let mut res = futures::executor::block_on(build_response()); - let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); let covered_components = COVERED_COMPONENTS_RES .iter() .map(|v| message_component::HttpMessageComponentId::try_from(*v)) @@ -248,7 +254,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= res .set_message_signature_sync(&signature_params, &secret_key, None, Some(&req)) .unwrap(); - let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + + let (alg, _key_id) = res.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; + let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); assert!(verification_res.is_ok()); } diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index e798575..7ed04b4 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -12,6 +12,10 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["rsasig"] +rsasig = ["rsa"] + [dependencies] thiserror = { version = "2.0.18" } tracing = { version = "0.1.44" } @@ -41,6 +45,12 @@ hmac = { version = "0.12.1" } sha2 = { version = "0.10.9", default-features = false } bytes = { version = "1.11.1" } +# rsa is optional +rsa = { version = "0.10.0-rc.15", default-features = false, optional = true, features = [ + "encoding", + "sha2", +] } + # encoding base64 = { version = "0.22.1" } diff --git a/httpsig/src/crypto/asymmetric.rs b/httpsig/src/crypto/asymmetric.rs index a1e998d..ad1bfbd 100644 --- a/httpsig/src/crypto/asymmetric.rs +++ b/httpsig/src/crypto/asymmetric.rs @@ -14,6 +14,14 @@ use pkcs8::{der::Decode, Document, PrivateKeyInfo}; use sha2::{Digest, Sha256, Sha384}; use spki::SubjectPublicKeyInfoRef; +#[cfg(feature = "rsasig")] +use rsa::{ + pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPublicKey}, + pkcs1v15, pss, + signature::{Keypair, RandomizedSigner, SignatureEncoding, Verifier}, + RsaPrivateKey, RsaPublicKey, +}; + #[allow(non_upper_case_globals, dead_code)] /// Algorithm OIDs mod algorithm_oids { @@ -21,6 +29,9 @@ mod algorithm_oids { pub const EC: &str = "1.2.840.10045.2.1"; /// OID for `id-Ed25519`, if you're curious pub const Ed25519: &str = "1.3.101.112"; + #[cfg(feature = "rsasig")] + /// OID for `id-rsaEncryption`, if you're curious + pub const rsaEncryption: &str = "1.2.840.113549.1.1.1"; } #[allow(non_upper_case_globals, dead_code)] /// Params OIDs @@ -42,11 +53,16 @@ pub enum SecretKey { EcdsaP256Sha256(EcSecretKey), /// ed25519 Ed25519(Ed25519SecretKey), + #[cfg(feature = "rsasig")] + /// rsa-v1_5-sha256 + RsaV1_5Sha256(pkcs1v15::SigningKey), + #[cfg(feature = "rsasig")] + RsaPssSha512(pss::SigningKey), } impl SecretKey { /// from plain bytes - pub fn from_bytes(alg: AlgorithmName, bytes: &[u8]) -> HttpSigResult { + pub fn from_bytes(alg: &AlgorithmName, bytes: &[u8]) -> HttpSigResult { match alg { AlgorithmName::EcdsaP256Sha256 => { debug!("Read P256 private key"); @@ -65,15 +81,29 @@ impl SecretKey { let sk = ed25519_compact::KeyPair::from_seed(ed25519_compact::Seed::new(seed)).sk; Ok(Self::Ed25519(sk)) } + #[cfg(feature = "rsasig")] + AlgorithmName::RsaV1_5Sha256 => { + debug!("Read RSA private key"); + // read PrivateKeyInfo.private_key as RsaPrivateKey (RFC 3447), which is DER encoded RSAPrivateKey in PKCS#1 + let sk = RsaPrivateKey::from_pkcs1_der(bytes).map_err(|e| HttpSigError::ParsePrivateKeyError(e.to_string()))?; + Ok(Self::RsaV1_5Sha256(pkcs1v15::SigningKey::::new(sk))) + } + #[cfg(feature = "rsasig")] + AlgorithmName::RsaPssSha512 => { + debug!("Read RSA-PSS private key"); + // read PrivateKeyInfo.private_key as RsaPrivateKey (RFC 3447), which is DER encoded RSAPrivateKey in PKCS#1 + let sk = RsaPrivateKey::from_pkcs1_der(bytes).map_err(|e| HttpSigError::ParsePrivateKeyError(e.to_string()))?; + Ok(Self::RsaPssSha512(pss::SigningKey::::new(sk))) + } _ => Err(HttpSigError::ParsePrivateKeyError("Unsupported algorithm".to_string())), } } /// parse der /// Derive secret key from der bytes - pub fn from_der(der: &[u8]) -> HttpSigResult { + pub fn from_der(alg: &AlgorithmName, der: &[u8]) -> HttpSigResult { let pki = PrivateKeyInfo::from_der(der).map_err(|e| HttpSigError::ParsePrivateKeyError(e.to_string()))?; - let (algorithm_name, sk_bytes) = match pki.algorithm.oid.to_string().as_ref() { + let sk_bytes = match pki.algorithm.oid.to_string().as_ref() { // ec algorithm_oids::EC => { let param = pki @@ -85,26 +115,39 @@ impl SecretKey { params_oids::Secp384r1 => AlgorithmName::EcdsaP384Sha384, _ => return Err(HttpSigError::ParsePrivateKeyError("Unsupported curve".to_string())), }; + // assert algorithm + if algorithm_name != *alg { + return Err(HttpSigError::ParsePrivateKeyError("Algorithm mismatch".to_string())); + } let sk_bytes = sec1::EcPrivateKey::try_from(pki.private_key) .map_err(|e| HttpSigError::ParsePrivateKeyError(format!("Error decoding EcPrivateKey: {e}")))? .private_key; - (algorithm_name, sk_bytes) + sk_bytes } // ed25519 - algorithm_oids::Ed25519 => (AlgorithmName::Ed25519, &pki.private_key[2..]), + algorithm_oids::Ed25519 => { + // assert algorithm + if AlgorithmName::Ed25519 != *alg { + return Err(HttpSigError::ParsePrivateKeyError("Algorithm mismatch".to_string())); + } + &pki.private_key[2..] + } + // rsa + #[cfg(feature = "rsasig")] + algorithm_oids::rsaEncryption => pki.private_key, _ => return Err(HttpSigError::ParsePrivateKeyError("Unsupported algorithm".to_string())), }; - let sk = Self::from_bytes(algorithm_name, sk_bytes)?; + let sk = Self::from_bytes(alg, sk_bytes)?; Ok(sk) } /// Derive secret key from pem string - pub fn from_pem(pem: &str) -> HttpSigResult { + pub fn from_pem(alg: &AlgorithmName, pem: &str) -> HttpSigResult { let (tag, doc) = Document::from_pem(pem).map_err(|e| HttpSigError::ParsePrivateKeyError(e.to_string()))?; if tag != "PRIVATE KEY" { return Err(HttpSigError::ParsePrivateKeyError("Invalid tag".to_string())); }; - Self::from_der(doc.as_bytes()) + Self::from_der(alg, doc.as_bytes()) } /// Get public key from secret key @@ -113,6 +156,10 @@ impl SecretKey { Self::EcdsaP256Sha256(key) => PublicKey::EcdsaP256Sha256(key.public_key()), Self::EcdsaP384Sha384(key) => PublicKey::EcdsaP384Sha384(key.public_key()), Self::Ed25519(key) => PublicKey::Ed25519(key.public_key()), + #[cfg(feature = "rsasig")] + Self::RsaV1_5Sha256(key) => PublicKey::RsaV1_5Sha256(key.verifying_key()), + #[cfg(feature = "rsasig")] + Self::RsaPssSha512(key) => PublicKey::RsaPssSha512(key.verifying_key()), } } } @@ -142,6 +189,18 @@ impl super::SigningKey for SecretKey { let sig = sk.sign(data, Some(ed25519_compact::Noise::default())); Ok(sig.as_ref().to_vec()) } + #[cfg(feature = "rsasig")] + Self::RsaV1_5Sha256(sk) => { + debug!("Sign RsaV1_5Sha256"); + let sig = sk.sign_with_rng(&mut rand::rng(), data); + Ok(sig.to_vec()) + } + #[cfg(feature = "rsasig")] + Self::RsaPssSha512(sk) => { + debug!("Sign RsaPssSha512"); + let sig = sk.sign_with_rng(&mut rand::rng(), data); + Ok(sig.to_vec()) + } } } @@ -181,11 +240,17 @@ pub enum PublicKey { EcdsaP384Sha384(EcPublicKey), /// ed25519 Ed25519(Ed25519PublicKey), + #[cfg(feature = "rsasig")] + /// rsa-v1_5-sha256 + RsaV1_5Sha256(pkcs1v15::VerifyingKey), + #[cfg(feature = "rsasig")] + /// rsa-pss-sha512 + RsaPssSha512(pss::VerifyingKey), } impl PublicKey { /// from plain bytes - pub fn from_bytes(alg: AlgorithmName, bytes: &[u8]) -> HttpSigResult { + pub fn from_bytes(alg: &AlgorithmName, bytes: &[u8]) -> HttpSigResult { match alg { AlgorithmName::EcdsaP256Sha256 => { debug!("Read P256 public key"); @@ -202,13 +267,27 @@ impl PublicKey { let pk = ed25519_compact::PublicKey::from_slice(bytes).map_err(|e| HttpSigError::ParsePublicKeyError(e.to_string()))?; Ok(Self::Ed25519(pk)) } + #[cfg(feature = "rsasig")] + AlgorithmName::RsaV1_5Sha256 => { + debug!("Read RSA public key"); + // read RsaPublicKey in SubjectPublicKeyInfo format in PKCS#8, which is DER encoded RSAPublicKey in PKCS#1 + let pk = RsaPublicKey::from_pkcs1_der(bytes).map_err(|e| HttpSigError::ParsePublicKeyError(e.to_string()))?; + Ok(Self::RsaV1_5Sha256(pkcs1v15::VerifyingKey::new(pk))) + } + #[cfg(feature = "rsasig")] + AlgorithmName::RsaPssSha512 => { + debug!("Read RSA-PSS public key"); + // read RsaPublicKey in SubjectPublicKeyInfo format in PKCS#8, which is DER encoded RSAPublicKey in PKCS#1 + let pk = RsaPublicKey::from_pkcs1_der(bytes).map_err(|e| HttpSigError::ParsePublicKeyError(e.to_string()))?; + Ok(Self::RsaPssSha512(pss::VerifyingKey::new(pk))) + } _ => Err(HttpSigError::ParsePublicKeyError("Unsupported algorithm".to_string())), } } #[allow(dead_code)] /// Convert from pem string - pub fn from_pem(pem: &str) -> HttpSigResult { + pub fn from_pem(alg: &AlgorithmName, pem: &str) -> HttpSigResult { let (tag, doc) = Document::from_pem(pem).map_err(|e| HttpSigError::ParsePublicKeyError(e.to_string()))?; if tag != "PUBLIC KEY" { return Err(HttpSigError::ParsePublicKeyError("Invalid tag".to_string())); @@ -217,7 +296,7 @@ impl PublicKey { let spki_ref = SubjectPublicKeyInfoRef::from_der(doc.as_bytes()) .map_err(|e| HttpSigError::ParsePublicKeyError(format!("Error decoding SubjectPublicKeyInfo: {e}").to_string()))?; - let (algorithm_name, pk_bytes) = match spki_ref.algorithm.oid.to_string().as_ref() { + let pk_bytes = match spki_ref.algorithm.oid.to_string().as_ref() { // ec algorithm_oids::EC => { let param = spki_ref @@ -229,23 +308,35 @@ impl PublicKey { params_oids::Secp384r1 => AlgorithmName::EcdsaP384Sha384, _ => return Err(HttpSigError::ParsePublicKeyError("Unsupported curve".to_string())), }; - let pk_bytes = spki_ref + // assert algorithm + if algorithm_name != *alg { + return Err(HttpSigError::ParsePublicKeyError("Algorithm mismatch".to_string())); + } + spki_ref .subject_public_key .as_bytes() - .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))?; - (algorithm_name, pk_bytes) + .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))? } // ed25519 - algorithm_oids::Ed25519 => ( - AlgorithmName::Ed25519, + algorithm_oids::Ed25519 => { + // assert algorithm + if AlgorithmName::Ed25519 != *alg { + return Err(HttpSigError::ParsePublicKeyError("Algorithm mismatch".to_string())); + } spki_ref .subject_public_key .as_bytes() - .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))?, - ), + .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))? + } + // rsa + #[cfg(feature = "rsasig")] + algorithm_oids::rsaEncryption => spki_ref + .subject_public_key + .as_bytes() + .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))?, _ => return Err(HttpSigError::ParsePublicKeyError("Unsupported algorithm".to_string())), }; - Self::from_bytes(algorithm_name, pk_bytes) + Self::from_bytes(alg, pk_bytes) } } @@ -280,10 +371,27 @@ impl super::VerifyingKey for PublicKey { pk.verify(data, &sig) .map_err(|e| HttpSigError::InvalidSignature(e.to_string())) } + #[cfg(feature = "rsasig")] + Self::RsaV1_5Sha256(pk) => { + debug!("Verify RsaV1_5Sha256"); + let sig = pkcs1v15::Signature::try_from(signature).map_err(|e| HttpSigError::ParseSignatureError(e.to_string()))?; + pk.verify(data, &sig) + .map_err(|e| HttpSigError::InvalidSignature(e.to_string())) + } + #[cfg(feature = "rsasig")] + Self::RsaPssSha512(pk) => { + debug!("Verify RsaPssSha512"); + let sig = pss::Signature::try_from(signature).map_err(|e| HttpSigError::ParseSignatureError(e.to_string()))?; + pk.verify(data, &sig) + .map_err(|e| HttpSigError::InvalidSignature(e.to_string())) + } } } - /// Create key id + /// Create key id, created by SHA-256 hash of the public key bytes, then encoded in base64 + /// - For ECDSA keys, use the uncompressed SEC1 encoding of the public key point as the byte representation. + /// - For Ed25519 keys, use the raw 32-byte public key. + /// - For RSA keys, use the DER encoding of the RSAPublicKey structure (SubjectPublicKeyInfo) as defined in PKCS#1. fn key_id(&self) -> String { use base64::{engine::general_purpose, Engine as _}; @@ -291,6 +399,18 @@ impl super::VerifyingKey for PublicKey { Self::EcdsaP256Sha256(vk) => vk.to_encoded_point(true).as_bytes().to_vec(), Self::EcdsaP384Sha384(vk) => vk.to_encoded_point(true).as_bytes().to_vec(), Self::Ed25519(vk) => vk.as_ref().to_vec(), + #[cfg(feature = "rsasig")] + Self::RsaV1_5Sha256(vk) => vk + .as_ref() + .to_pkcs1_der() + .map(|der| der.as_bytes().to_vec()) + .unwrap_or_default(), + #[cfg(feature = "rsasig")] + Self::RsaPssSha512(vk) => vk + .as_ref() + .to_pkcs1_der() + .map(|der| der.as_bytes().to_vec()) + .unwrap_or_default(), }; let mut hasher = ::new(); hasher.update(&bytes); @@ -304,6 +424,10 @@ impl super::VerifyingKey for PublicKey { Self::EcdsaP256Sha256(_) => AlgorithmName::EcdsaP256Sha256, Self::EcdsaP384Sha384(_) => AlgorithmName::EcdsaP384Sha384, Self::Ed25519(_) => AlgorithmName::Ed25519, + #[cfg(feature = "rsasig")] + Self::RsaV1_5Sha256(_) => AlgorithmName::RsaV1_5Sha256, + #[cfg(feature = "rsasig")] + Self::RsaPssSha512(_) => AlgorithmName::RsaPssSha512, } } } @@ -315,7 +439,7 @@ mod tests { use super::*; use std::matches; - const P256_SECERT_KEY: &str = r##"-----BEGIN PRIVATE KEY----- + const P256_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv7zxW56ojrWwmSo1 4uOdbVhUfj9Jd+5aZIB9u8gtWnihRANCAARGYsMe0CT6pIypwRvoJlLNs4+cTh2K L7fUNb5i6WbKxkpAoO+6T3pMBG5Yw7+8NuGTvvtrZAXduA2giPxQ8zCf @@ -326,7 +450,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERmLDHtAk+qSMqcEb6CZSzbOPnE4d ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== -----END PUBLIC KEY----- "##; - const P384_SECERT_KEY: &str = r##"-----BEGIN PRIVATE KEY----- + const P384_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCPYbeLLlIQKUzVyVGH MeuFp/9o2Lr+4GrI3bsbHuViMMceiuM+8xqzFCSm4Ltl5UyhZANiAARKg3yM+Ltx n4ZptF3hI6Q167crEtPRklCEsRTyWUqy+VrrnM5LU/+fqxVbyniBZHd4vmQVYtjF @@ -349,63 +473,137 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= -----END PUBLIC KEY----- "##; + #[cfg(feature = "rsasig")] + const RSA2048_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCrjtdIxemmmL9V +wfp7qqwytfRDZqQM6XNWcAi3x+j5dHFFIKKWQktJ7eCTRYQrBwjQs5sb0ieNUYwQ +vTIH53z9PqKl1bCIk/6Pago2JNBQAUP9DSs+zcYYC1TYwPM12mxIqz6tHVBabBuG +49OoqWGgU4J5YkXvjyNFPVK1+dePLalm4/jUJb4VpbppN5NQ9qaqRTB3vQPW9i3D +uy2hefxh7FGfKx9BwrtKcV4JmjN9IjPpdjZTex/8GF3eIiePkHIJS88w8lkC4F03 +06EuaMRs6KyWpj1aof+LvMG6iIRAigc0K//4tTwfKALRky4tW9JAYe3cFACDulu2 +VXKGatq1AgMBAAECggEAH/cOb9XIci0VwXHSLQag7RXv/Dr8qBc7UUiwpyWNaCVl +EX9CLAMQKiczZ91VAftejhxY8zcV/YPLODc4QjbEmB76iTGmodwJW0lju7DiS3Xg +6B5zB1Gp7kL2PSi+aDNZZ7TYicLjfOWVv21lu5BLy2aj8d/4rekapkUFyzhRDLEk +E9/mvztCrLjLXMS6SFXY/rjfwckBT/tACbmgHInzRcoyX75FYyGtOc3w1S1tXEM8 +7j/7EHZf+mNcHlpV5OMw+StVfl1Qwx8eJ9ZW1TmZEoysRe4zj/ej7+wTBSAC7AoA +UVB6G8hVU1NP+KD7Z9/6SvfJGvj8yR1HdBE5BZ54JQKBgQDfkpDEH4EH0pRSd867 +nrijAwnqd4xdP11aOwgrrppxavUWAmd3vmki8k4O3ghkz8MtNd+bTcZNKcl4ioS3 +boFA++wZQuzPBu6dbwlM9QX0VyzKAmGITrcnFrxCk3k8d6r9DzTVrzY8oK7nvo+1 +n9QYtlBs/SyJZl4McEOCV0fsowKBgQDEcO6KkwQPt//Qm6Qb1ImrQQ1kgvOu77iG +R5Gn/FkURJvy+I4g9ANRXmHFTcdMSIS3dzY5Zr4kwa0UYJ/ivQ5QYwzYISiW3kgj +jmoLhxfWOXaO+vGNBXoZb5JkKrT2pnLlbbeiHaur6jfg20T2w2whts4vJ8aI6V3k +HagrXuz4xwKBgQDWhMhZFq109w4QTxrTFYmuCAVkr07ETj9hi5DccQ2J1AnUE3x5 +/f7dZEeXpl3BdUSeRboHR0oF0hmZirerVeG5m7+/wWJ9hvY/o0H2UIhlGZxFPKGe +64B7hiofa2eBqIUtiYC1pAfTho4smMFFkVUuXQiwewBX2hxVrQZpsxu1JwKBgEwH +fXuqvPase1ks9A5Fa2cZzWoqeNArPdrS1mAS/hMnHsiiRLgiWSpkAilQGiO/KYas +oBMFXfBx+WAaqacjDugz/eOkqcYCkB8a3pZJmgMyyF08aMLw7LntgdY85T9VWsDL +fzhCjZADHc9sbjunlTFTRGfh2ChjUhCZHd5zZfo/AoGBANk7kXrHZlAsmEEoeA8R +yVpIaTIu64SzCsn4lWzh02zuSB20uNzYdNYBkHT/JHMvV4ctxjAXjDWI8aYzHaHY +KDYy4jUp2TeTPBpqwS24KzFaFx0y2U99TWrzt6sQJr7Y9NlR7S0znc/L7wwFobjr +XVdlU40OaPP7xs0er/tWVAPY +-----END PRIVATE KEY-----"##; + + #[cfg(feature = "rsasig")] + const RSA2048_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq47XSMXpppi/VcH6e6qs +MrX0Q2akDOlzVnAIt8fo+XRxRSCilkJLSe3gk0WEKwcI0LObG9InjVGMEL0yB+d8 +/T6ipdWwiJP+j2oKNiTQUAFD/Q0rPs3GGAtU2MDzNdpsSKs+rR1QWmwbhuPTqKlh +oFOCeWJF748jRT1StfnXjy2pZuP41CW+FaW6aTeTUPamqkUwd70D1vYtw7stoXn8 +YexRnysfQcK7SnFeCZozfSIz6XY2U3sf/Bhd3iInj5ByCUvPMPJZAuBdN9OhLmjE +bOislqY9WqH/i7zBuoiEQIoHNCv/+LU8HygC0ZMuLVvSQGHt3BQAg7pbtlVyhmra +tQIDAQAB +-----END PUBLIC KEY-----"##; + #[test] fn test_from_bytes() { let ed25519_kp = ed25519_compact::KeyPair::from_seed(ed25519_compact::Seed::default()); let ed25519_sk = ed25519_kp.sk.seed().to_vec(); let ed25519_pk = ed25519_kp.pk.as_ref(); - let sk = SecretKey::from_bytes(AlgorithmName::Ed25519, &ed25519_sk).unwrap(); + let sk = SecretKey::from_bytes(&AlgorithmName::Ed25519, &ed25519_sk).unwrap(); assert!(matches!(sk, SecretKey::Ed25519(_))); - let pk = PublicKey::from_bytes(AlgorithmName::Ed25519, ed25519_pk).unwrap(); + let pk = PublicKey::from_bytes(&AlgorithmName::Ed25519, ed25519_pk).unwrap(); assert!(matches!(pk, PublicKey::Ed25519(_))); let mut rng = rand_085::thread_rng(); let es256_sk = p256::ecdsa::SigningKey::random(&mut rng); let es256_pk = es256_sk.verifying_key(); - let sk = SecretKey::from_bytes(AlgorithmName::EcdsaP256Sha256, es256_sk.to_bytes().as_ref()).unwrap(); + let sk = SecretKey::from_bytes(&AlgorithmName::EcdsaP256Sha256, es256_sk.to_bytes().as_ref()).unwrap(); assert!(matches!(sk, SecretKey::EcdsaP256Sha256(_))); let pk_bytes = es256_pk.as_affine().to_bytes(); - let pk = PublicKey::from_bytes(AlgorithmName::EcdsaP256Sha256, pk_bytes.as_ref()).unwrap(); + let pk = PublicKey::from_bytes(&AlgorithmName::EcdsaP256Sha256, pk_bytes.as_ref()).unwrap(); assert!(matches!(pk, PublicKey::EcdsaP256Sha256(_))); } #[test] fn test_from_pem() { - let sk = SecretKey::from_pem(P256_SECERT_KEY).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECRET_KEY).unwrap(); assert!(matches!(sk, SecretKey::EcdsaP256Sha256(_))); - let pk = PublicKey::from_pem(P256_PUBLIC_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_PUBLIC_KEY).unwrap(); assert!(matches!(pk, PublicKey::EcdsaP256Sha256(_))); - let sk = SecretKey::from_pem(P384_SECERT_KEY).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::EcdsaP384Sha384, P384_SECRET_KEY).unwrap(); assert!(matches!(sk, SecretKey::EcdsaP384Sha384(_))); - let pk = PublicKey::from_pem(P384_PUBLIC_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::EcdsaP384Sha384, P384_PUBLIC_KEY).unwrap(); assert!(matches!(pk, PublicKey::EcdsaP384Sha384(_))); - let sk = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); assert!(matches!(sk, SecretKey::Ed25519(_))); - let pk = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); assert!(matches!(pk, PublicKey::Ed25519(_))); } + #[cfg(feature = "rsasig")] + #[test] + fn test_from_pem_rsa() { + let sk = SecretKey::from_pem(&AlgorithmName::RsaV1_5Sha256, RSA2048_SECRET_KEY).unwrap(); + assert!(matches!(sk, SecretKey::RsaV1_5Sha256(_))); + let pk = PublicKey::from_pem(&AlgorithmName::RsaV1_5Sha256, RSA2048_PUBLIC_KEY).unwrap(); + assert!(matches!(pk, PublicKey::RsaV1_5Sha256(_))); + + let sk = SecretKey::from_pem(&AlgorithmName::RsaPssSha512, RSA2048_SECRET_KEY).unwrap(); + assert!(matches!(sk, SecretKey::RsaPssSha512(_))); + let pk = PublicKey::from_pem(&AlgorithmName::RsaPssSha512, RSA2048_PUBLIC_KEY).unwrap(); + assert!(matches!(pk, PublicKey::RsaPssSha512(_))); + } + #[test] fn test_sign_verify() { use super::super::{SigningKey, VerifyingKey}; - let sk = SecretKey::from_pem(P256_SECERT_KEY).unwrap(); - let pk = PublicKey::from_pem(P256_PUBLIC_KEY).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECRET_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_PUBLIC_KEY).unwrap(); let data = b"hello world"; let signature = sk.sign(data).unwrap(); pk.verify(data, &signature).unwrap(); assert!(pk.verify(b"hello", &signature).is_err()); - let sk = SecretKey::from_pem(P384_SECERT_KEY).unwrap(); - let pk = PublicKey::from_pem(P384_PUBLIC_KEY).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::EcdsaP384Sha384, P384_SECRET_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::EcdsaP384Sha384, P384_PUBLIC_KEY).unwrap(); let data = b"hello world"; let signature = sk.sign(data).unwrap(); pk.verify(data, &signature).unwrap(); assert!(pk.verify(b"hello", &signature).is_err()); - let sk = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); - let pk = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let data = b"hello world"; + let signature = sk.sign(data).unwrap(); + pk.verify(data, &signature).unwrap(); + assert!(pk.verify(b"hello", &signature).is_err()); + } + + #[cfg(feature = "rsasig")] + #[test] + fn test_sign_verify_rsa() { + use super::super::{SigningKey, VerifyingKey}; + let sk = SecretKey::from_pem(&AlgorithmName::RsaV1_5Sha256, RSA2048_SECRET_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::RsaV1_5Sha256, RSA2048_PUBLIC_KEY).unwrap(); + let data = b"hello world"; + let signature = sk.sign(data).unwrap(); + pk.verify(data, &signature).unwrap(); + assert!(pk.verify(b"hello", &signature).is_err()); + + let sk = SecretKey::from_pem(&AlgorithmName::RsaPssSha512, RSA2048_SECRET_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::RsaPssSha512, RSA2048_PUBLIC_KEY).unwrap(); let data = b"hello world"; let signature = sk.sign(data).unwrap(); pk.verify(data, &signature).unwrap(); @@ -415,20 +613,36 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[test] fn test_kid() -> HttpSigResult<()> { use super::super::VerifyingKey; - let sk = SecretKey::from_pem(P256_SECERT_KEY)?; - let pk = PublicKey::from_pem(P256_PUBLIC_KEY)?; + let sk = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECRET_KEY)?; + let pk = PublicKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_PUBLIC_KEY)?; assert_eq!(sk.public_key().key_id(), pk.key_id()); assert_eq!(pk.key_id(), "k34r3Nqfak67bhJSXTjTRo5tCIr1Bsre1cPoJ3LJ9xE="); - let sk = SecretKey::from_pem(P384_SECERT_KEY)?; - let pk = PublicKey::from_pem(P384_PUBLIC_KEY)?; + let sk = SecretKey::from_pem(&AlgorithmName::EcdsaP384Sha384, P384_SECRET_KEY)?; + let pk = PublicKey::from_pem(&AlgorithmName::EcdsaP384Sha384, P384_PUBLIC_KEY)?; assert_eq!(sk.public_key().key_id(), pk.key_id()); assert_eq!(pk.key_id(), "JluSJKLaQsbGcgg1Ves4FfP/Kf7qS11RT88TvU0eNSo="); - let sk = SecretKey::from_pem(EDDSA_SECRET_KEY)?; - let pk = PublicKey::from_pem(EDDSA_PUBLIC_KEY)?; + let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY)?; + let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY)?; assert_eq!(sk.public_key().key_id(), pk.key_id()); assert_eq!(pk.key_id(), "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); Ok(()) } + + #[cfg(feature = "rsasig")] + #[test] + fn test_kid_rsa() -> HttpSigResult<()> { + use super::super::VerifyingKey; + let sk = SecretKey::from_pem(&AlgorithmName::RsaV1_5Sha256, RSA2048_SECRET_KEY)?; + let pk = PublicKey::from_pem(&AlgorithmName::RsaV1_5Sha256, RSA2048_PUBLIC_KEY)?; + assert_eq!(sk.public_key().key_id(), pk.key_id()); + assert_eq!(pk.key_id(), "NoJFUyf2XUdhrTK66RlrGEemIlr1tOScYVeNVCv+5Ns="); + + let sk = SecretKey::from_pem(&AlgorithmName::RsaPssSha512, RSA2048_SECRET_KEY)?; + let pk = PublicKey::from_pem(&AlgorithmName::RsaPssSha512, RSA2048_PUBLIC_KEY)?; + assert_eq!(sk.public_key().key_id(), pk.key_id()); + assert_eq!(pk.key_id(), "NoJFUyf2XUdhrTK66RlrGEemIlr1tOScYVeNVCv+5Ns="); // same as above nothing changes for RSA + Ok(()) + } } diff --git a/httpsig/src/crypto/mod.rs b/httpsig/src/crypto/mod.rs index 89b129a..74c1bb4 100644 --- a/httpsig/src/crypto/mod.rs +++ b/httpsig/src/crypto/mod.rs @@ -1,26 +1,35 @@ mod asymmetric; mod symmetric; -use crate::error::HttpSigResult; +use crate::error::{HttpSigError, HttpSigResult}; pub use asymmetric::{PublicKey, SecretKey}; pub use symmetric::SharedKey; +#[derive(Debug, PartialEq, Eq)] /// Algorithm names pub enum AlgorithmName { HmacSha256, EcdsaP256Sha256, EcdsaP384Sha384, Ed25519, + #[cfg(feature = "rsasig")] + RsaV1_5Sha256, + #[cfg(feature = "rsasig")] + RsaPssSha512, } impl AlgorithmName { - pub fn as_str(&self) -> &str { + pub fn as_str(&self) -> &'static str { match self { AlgorithmName::HmacSha256 => "hmac-sha256", AlgorithmName::EcdsaP256Sha256 => "ecdsa-p256-sha256", AlgorithmName::EcdsaP384Sha384 => "ecdsa-p384-sha384", AlgorithmName::Ed25519 => "ed25519", + #[cfg(feature = "rsasig")] + AlgorithmName::RsaV1_5Sha256 => "rsa-v1_5-sha256", + #[cfg(feature = "rsasig")] + AlgorithmName::RsaPssSha512 => "rsa-pss-sha512", } } } @@ -31,6 +40,24 @@ impl std::fmt::Display for AlgorithmName { } } +impl core::str::FromStr for AlgorithmName { + type Err = HttpSigError; + + fn from_str(s: &str) -> Result { + match s { + "hmac-sha256" => Ok(Self::HmacSha256), + "ecdsa-p256-sha256" => Ok(Self::EcdsaP256Sha256), + "ecdsa-p384-sha384" => Ok(Self::EcdsaP384Sha384), + "ed25519" => Ok(Self::Ed25519), + #[cfg(feature = "rsasig")] + "rsa-v1_5-sha256" => Ok(Self::RsaV1_5Sha256), + #[cfg(feature = "rsasig")] + "rsa-pss-sha512" => Ok(Self::RsaPssSha512), + _ => Err(HttpSigError::InvalidAlgorithmName(s.to_string())), + } + } +} + /// SigningKey trait pub trait SigningKey { fn sign(&self, data: &[u8]) -> HttpSigResult>; diff --git a/httpsig/src/crypto/symmetric.rs b/httpsig/src/crypto/symmetric.rs index ad36584..c112cb6 100644 --- a/httpsig/src/crypto/symmetric.rs +++ b/httpsig/src/crypto/symmetric.rs @@ -20,10 +20,16 @@ pub enum SharedKey { impl SharedKey { /// Create a new shared key from base64 encoded string - pub fn from_base64(key: &str) -> HttpSigResult { + pub fn from_base64(alg: &AlgorithmName, key: &str) -> HttpSigResult { debug!("Create SharedKey from base64 string"); let key = general_purpose::STANDARD.decode(key)?; - Ok(SharedKey::HmacSha256(key)) + match alg { + AlgorithmName::HmacSha256 => Ok(SharedKey::HmacSha256(key)), + _ => Err(HttpSigError::InvalidAlgorithmName(format!( + "Unsupported algorithm for SharedKey: {}", + alg + ))), + } } } @@ -58,7 +64,8 @@ impl super::VerifyingKey for SharedKey { debug!("Verify HmacSha256"); let mut mac = HmacSha256::new_from_slice(key).unwrap(); mac.update(data); - mac.verify_slice(expected_mac) + mac + .verify_slice(expected_mac) .map_err(|_| HttpSigError::InvalidSignature("Invalid MAC".to_string())) } } diff --git a/httpsig/src/error.rs b/httpsig/src/error.rs index dda6cae..0a342d0 100644 --- a/httpsig/src/error.rs +++ b/httpsig/src/error.rs @@ -57,6 +57,10 @@ pub enum HttpSigError { #[error("Expired signature params: {0}")] ExpiredSignatureParams(String), + /// Invalid algorithm name + #[error("Invalid algorithm name: {0}")] + InvalidAlgorithmName(String), + /* ----- Other errors ----- */ /// NotYetImplemented #[error("Not yet implemented: {0}")] diff --git a/httpsig/src/lib.rs b/httpsig/src/lib.rs index e45b7ea..2eb8512 100644 --- a/httpsig/src/lib.rs +++ b/httpsig/src/lib.rs @@ -57,8 +57,8 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw #[test] fn test_using_test_vector_ed25519() { - let sk = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); - let pk = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let sk = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let pk = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); assert_eq!(pk.key_id(), sk.public_key().key_id()); let data = EDDSA_SIGNATURE_BASE.as_bytes(); @@ -86,7 +86,7 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw #[test] fn test_using_test_vector_hmac_sha256() { - let sk = SharedKey::from_base64(HMACSHA256_SECRET_KEY).unwrap(); + let sk = SharedKey::from_base64(&AlgorithmName::HmacSha256, HMACSHA256_SECRET_KEY).unwrap(); let data = HMACSHA256_SIGNATURE_BASE.as_bytes(); let binary_signature = general_purpose::STANDARD.decode(HMACSHA256_SIGNATURE_VALUE).unwrap(); @@ -123,8 +123,8 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw .collect::>(); let signature_base = HttpSignatureBase::try_new(&component_lines, &signature_params).unwrap(); - let sk = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); - let pk = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + 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); @@ -141,7 +141,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 sk = SecretKey::from_pem(EDDSA_SECRET_KEY).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(); @@ -154,7 +154,7 @@ Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgw 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 pk = PublicKey::from_pem(EDDSA_PUBLIC_KEY).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/signature_params.rs b/httpsig/src/signature_params.rs index 788db68..7dba407 100644 --- a/httpsig/src/signature_params.rs +++ b/httpsig/src/signature_params.rs @@ -266,7 +266,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[test] fn test_set_key_info() { let mut params = HttpSignatureParams::try_new(&build_covered_components()).unwrap(); - params.set_key_info(&SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap()); + params.set_key_info(&SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap()); assert_eq!(params.keyid, Some(EDDSA_KEY_ID.to_string())); assert_eq!(params.alg, Some("ed25519".to_string())); } From b3e917394899ea90e325f29c1b71acd9929ed9c5 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 13 Feb 2026 22:19:47 +0900 Subject: [PATCH 13/27] update docs --- README.md | 30 +++++++++++++++++++++--------- httpsig-hyper/Cargo.toml | 3 ++- httpsig/Cargo.toml | 2 +- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 92f94e2..acc29fe 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,10 @@ This crates provides a basic library [httpsig](./httpsig) and [its extension](./ - [x] Ed25519 - [x] ECDSA-P256 using SHA-256 - [ ] ECDSA-P384 using SHA-384 +- [x] RSASSA-PSS using SHA-512 +- [x] RSASSA-PKCS1-v1_5 using SHA-256 -~~- [ ] RSASSA-PSS using SHA-512~~ - -~~- [ ] RSASSA-PKCS1-v1_5 using SHA-256~~ - -At this point, we have no plan to support RSA signature due to [the problem related to the non-constant time operation](https://github.com/RustCrypto/RSA/issues/19), i.e., [Mervin Attack](https://people.redhat.com/~hkario/marvin/). +At this point, **RSA signature is non-default** due to [the problem related to the non-constant time operation](https://github.com/RustCrypto/RSA/issues/19), i.e., [Mervin Attack](https://people.redhat.com/~hkario/marvin/). If you want to use RSA signature, please enable the `rsa-signature` feature flag in your `Cargo.toml`. ## Usage of Extension for `hyper` (`httpsig-hyper`) @@ -48,8 +46,11 @@ async fn signer(&mut req: Request) -> HttpSigResult<()> { .unwrap(); let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + // specify algorithm name since we cannot always infer it from key info + let alg = AlgorithmName::Ed25519; + // set signing/verifying key information, alg and keyid - let secret_key = SecretKey::from_pem(SECRET_KEY_STRING).unwrap(); + let secret_key = SecretKey::from_pem(&alg, SECRET_KEY_STRING).unwrap(); signature_params.set_key_info(&secret_key); req @@ -59,7 +60,11 @@ async fn signer(&mut req: Request) -> HttpSigResult<()> { /// Validation function that verifies a request with a signature async fn verifier(req: &Request) -> HttpSigResult { - let public_key = PublicKey::from_pem(PUBLIC_KEY_STRING).unwrap(); + // specify algorithm name since we cannot always infer it from key info + let alg = AlgorithmName::Ed25519; // directly use Ed25519 algorithm + // or else infer it from the request. Find your public key from IndexMap with alg and key_id pairs + // let alg_key_id_map = req.get_alg_key_ids().unwrap(); + let public_key = PublicKey::from_pem(&alg, PUBLIC_KEY_STRING).unwrap(); let key_id = public_key.key_id(); // verify signature with checking key_id @@ -105,8 +110,11 @@ async fn signer(&mut res: Response, corresponding_req: &Request) -> Htt .unwrap(); let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + // specify algorithm name since we cannot always infer it from key info + let alg = AlgorithmName::Ed25519; + // set signing/verifying key information, alg and keyid - let secret_key = SecretKey::from_pem(SECRET_KEY_STRING).unwrap(); + let secret_key = SecretKey::from_pem(&alg, SECRET_KEY_STRING).unwrap(); signature_params.set_key_info(&secret_key); req @@ -116,7 +124,11 @@ async fn signer(&mut res: Response, corresponding_req: &Request) -> Htt /// Validation function that verifies a response with a signature from response itself and sent request async fn verifier(res: &Response, sent_req: &Request) -> HttpSigResult { - let public_key = PublicKey::from_pem(PUBLIC_KEY_STRING).unwrap(); + // specify algorithm name since we cannot always infer it from key info + let alg = AlgorithmName::Ed25519; // directly use Ed25519 algorithm + // or else infer it from the response. Find your public key from IndexMap with alg and key_id pairs + // let alg_key_id_map = res.get_alg_key_ids().unwrap + let public_key = PublicKey::from_pem(&alg, PUBLIC_KEY_STRING).unwrap(); let key_id = public_key.key_id(); // verify signature with checking key_id diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 79522e1..146ee2d 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,8 +13,9 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["blocking"] +default = ["blocking", "rsasig"] blocking = ["futures/executor"] +rsasig = ["httpsig/rsasig"] [dependencies] diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index 7ed04b4..04d4bc4 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -13,7 +13,7 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["rsasig"] +default = [] rsasig = ["rsa"] [dependencies] From ea8a07e33de101f2ccd5b33dc07cc9be1056f830 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 13 Feb 2026 22:21:01 +0900 Subject: [PATCH 14/27] chore: remove rsasig feature from the default --- httpsig-hyper/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 146ee2d..a9d4fc4 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,7 +13,7 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["blocking", "rsasig"] +default = ["blocking"] blocking = ["futures/executor"] rsasig = ["httpsig/rsasig"] From f85034ef977f171d5415c1b13695a4e20d474edc Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 13 Feb 2026 22:36:22 +0900 Subject: [PATCH 15/27] fix: update api signature of get_alg_key_ids --- README.md | 2 +- httpsig-hyper/src/hyper_http.rs | 22 ++++++++++------------ httpsig-hyper/src/lib.rs | 8 ++++---- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index acc29fe..608665e 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ async fn verifier(res: &Response, sent_req: &Request) -> HttpSigResult< // specify algorithm name since we cannot always infer it from key info let alg = AlgorithmName::Ed25519; // directly use Ed25519 algorithm // or else infer it from the response. Find your public key from IndexMap with alg and key_id pairs - // let alg_key_id_map = res.get_alg_key_ids().unwrap + // let alg_key_id_map = res.get_alg_key_ids().unwrap() let public_key = PublicKey::from_pem(&alg, PUBLIC_KEY_STRING).unwrap(); let key_id = public_key.key_id(); diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index ed3587d..67e728f 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -24,7 +24,7 @@ pub trait MessageSignature { fn has_message_signature(&self) -> bool; /// Extract all key ids for signature bases contained in the request headers - fn get_alg_key_ids(&self) -> Result, Self::Error>; + fn get_alg_key_ids(&self) -> Result, Option)>, Self::Error>; /// Extract all signature params used to generate signature bases contained in the request headers fn get_signature_params(&self) -> Result, Self::Error>; @@ -243,7 +243,7 @@ where } /// Extract all signature bases contained in the request headers - fn get_alg_key_ids(&self) -> HyperSigResult> { + fn get_alg_key_ids(&self) -> HyperSigResult, Option)>> { let req_or_res = RequestOrResponse::Request(self); get_alg_key_ids_inner(&req_or_res) } @@ -358,7 +358,7 @@ where } /// Extract all key ids for signature bases contained in the response headers - fn get_alg_key_ids(&self) -> Result, Self::Error> { + fn get_alg_key_ids(&self) -> Result, Option)>, Self::Error> { let req_or_res = RequestOrResponse::Response(self); get_alg_key_ids_inner(&req_or_res) } @@ -596,7 +596,7 @@ fn has_message_signature_inner(headers: &HeaderMap) -> bool { /// get key ids inner function fn get_alg_key_ids_inner( req_or_res: &RequestOrResponse, -) -> HyperSigResult> { +) -> HyperSigResult, Option)>> { let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; let res = signature_headers_map .iter() @@ -610,11 +610,7 @@ fn get_alg_key_ids_inner( .ok() .flatten(); let key_id = headers.signature_params().keyid.clone(); - if let (Some(alg), Some(key_id)) = (alg, key_id) { - Some((name.clone(), (alg, key_id))) - } else { - None - } + Some((name.clone(), (alg, key_id))) }) .collect(); Ok(res) @@ -1185,7 +1181,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= let org_key_id = VerifyingKey::key_id(&secret_key); let (alg, key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; - assert_eq!(org_key_id, *key_id); + let alg = alg.unwrap(); + let key_id = key_id.unwrap(); + assert_eq!(org_key_id, key_id); let verification_key = SharedKey::from_base64(&alg, HMACSHA256_SECRET_KEY).unwrap(); let verification_res = req.verify_message_signature(&verification_key, Some(&key_id)).await; assert!(verification_res.is_ok()); @@ -1204,8 +1202,8 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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, AlgorithmName::Ed25519); - assert_eq!(key_ids[0].1, "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); + assert_eq!(key_ids[0].0.as_ref().unwrap(), &AlgorithmName::Ed25519); + assert_eq!(key_ids[0].1.as_ref().unwrap(), "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); } const P256_SECERT_KEY: &str = r##"-----BEGIN PRIVATE KEY----- diff --git a/httpsig-hyper/src/lib.rs b/httpsig-hyper/src/lib.rs index 33b4e55..691a392 100644 --- a/httpsig-hyper/src/lib.rs +++ b/httpsig-hyper/src/lib.rs @@ -147,7 +147,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // verify without checking key_id // get algorithm from signature params let (alg, _key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; - let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature(&public_key, None).await; assert!(verification_res.is_ok()); @@ -192,7 +192,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // verify without checking key_id, request must be provided if `req` field param is included in signature params // get algorithm from signature params let (alg, _key_id) = res.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; - let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap(); let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; assert!(verification_res.is_ok()); let verification_res = res @@ -230,7 +230,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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, EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap(); let verification_res = req.verify_message_signature_sync(&public_key, None); assert!(verification_res.is_ok()); } @@ -256,7 +256,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= .unwrap(); let (alg, _key_id) = res.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; - let public_key = PublicKey::from_pem(&alg, EDDSA_PUBLIC_KEY).unwrap(); + let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap(); let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); assert!(verification_res.is_ok()); } From c5e161d34c40e0c36d9e82b8f73f169de353899a Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 13 Feb 2026 22:41:08 +0900 Subject: [PATCH 16/27] fix: fix inner process of get_alg_key_ids --- httpsig-hyper/src/hyper_http.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 67e728f..ec041f1 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -600,7 +600,8 @@ fn get_alg_key_ids_inner( let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; let res = signature_headers_map .iter() - .filter_map(|(name, headers)| { + .map(|(name, headers)| { + // Unknown or unsupported algorithm strings are mapped to None let alg = headers .signature_params() .alg @@ -610,7 +611,7 @@ fn get_alg_key_ids_inner( .ok() .flatten(); let key_id = headers.signature_params().keyid.clone(); - Some((name.clone(), (alg, key_id))) + (name.clone(), (alg, key_id)) }) .collect(); Ok(res) From 1a9aa4227acaa8c371b5faa835fbe2e56e7fcbfb Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 14 Feb 2026 01:03:56 +0900 Subject: [PATCH 17/27] refactor: update several internal processes --- README.md | 6 +-- httpsig-hyper/Cargo.toml | 4 +- httpsig-hyper/src/hyper_http.rs | 4 +- httpsig/Cargo.toml | 2 +- httpsig/src/crypto/asymmetric.rs | 87 ++++++++++++++++++-------------- httpsig/src/crypto/mod.rs | 12 ++--- 6 files changed, 64 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 608665e..cb32b3b 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ This crates provides a basic library [httpsig](./httpsig) and [its extension](./ - [x] HMAC using SHA-256 - [x] Ed25519 - [x] ECDSA-P256 using SHA-256 -- [ ] ECDSA-P384 using SHA-384 +- [x] ECDSA-P384 using SHA-384 - [x] RSASSA-PSS using SHA-512 - [x] RSASSA-PKCS1-v1_5 using SHA-256 -At this point, **RSA signature is non-default** due to [the problem related to the non-constant time operation](https://github.com/RustCrypto/RSA/issues/19), i.e., [Mervin Attack](https://people.redhat.com/~hkario/marvin/). If you want to use RSA signature, please enable the `rsa-signature` feature flag in your `Cargo.toml`. +At this point, **RSA signature is non-default** due to [the problem related to the non-constant time operation](https://github.com/RustCrypto/RSA/issues/19), i.e., [Marvin Attack](https://people.redhat.com/~hkario/marvin/). If you want to use RSA signature, please enable the `rsa-signature` feature flag in your `Cargo.toml`. ## Usage of Extension for `hyper` (`httpsig-hyper`) @@ -127,7 +127,7 @@ async fn verifier(res: &Response, sent_req: &Request) -> HttpSigResult< // specify algorithm name since we cannot always infer it from key info let alg = AlgorithmName::Ed25519; // directly use Ed25519 algorithm // or else infer it from the response. Find your public key from IndexMap with alg and key_id pairs - // let alg_key_id_map = res.get_alg_key_ids().unwrap() + // let alg_key_id_map = res.get_alg_key_ids().unwrap(); let public_key = PublicKey::from_pem(&alg, PUBLIC_KEY_STRING).unwrap(); let key_id = public_key.key_id(); diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index a9d4fc4..a5bc42a 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,9 +13,9 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["blocking"] +default = ["blocking", "rsa-signature"] blocking = ["futures/executor"] -rsasig = ["httpsig/rsasig"] +rsa-signature = ["httpsig/rsa-signature"] [dependencies] diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index ec041f1..ad9b2ac 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -1207,7 +1207,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert_eq!(key_ids[0].1.as_ref().unwrap(), "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); } - const P256_SECERT_KEY: &str = r##"-----BEGIN PRIVATE KEY----- + const P256_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv7zxW56ojrWwmSo1 4uOdbVhUfj9Jd+5aZIB9u8gtWnihRANCAARGYsMe0CT6pIypwRvoJlLNs4+cTh2K L7fUNb5i6WbKxkpAoO+6T3pMBG5Yw7+8NuGTvvtrZAXduA2giPxQ8zCf @@ -1226,7 +1226,7 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== let mut signature_params_eddsa = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params_eddsa.set_key_info(&secret_key_eddsa); - let secret_key_p256 = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECERT_KEY).unwrap(); + let secret_key_p256 = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECRET_KEY).unwrap(); let mut signature_params_hmac = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params_hmac.set_key_info(&secret_key_p256); diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index 04d4bc4..b20d835 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -14,7 +14,7 @@ rust-version.workspace = true [features] default = [] -rsasig = ["rsa"] +rsa-signature = ["rsa"] [dependencies] thiserror = { version = "2.0.18" } diff --git a/httpsig/src/crypto/asymmetric.rs b/httpsig/src/crypto/asymmetric.rs index ad1bfbd..f2a6787 100644 --- a/httpsig/src/crypto/asymmetric.rs +++ b/httpsig/src/crypto/asymmetric.rs @@ -14,7 +14,7 @@ use pkcs8::{der::Decode, Document, PrivateKeyInfo}; use sha2::{Digest, Sha256, Sha384}; use spki::SubjectPublicKeyInfoRef; -#[cfg(feature = "rsasig")] +#[cfg(feature = "rsa-signature")] use rsa::{ pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPublicKey}, pkcs1v15, pss, @@ -29,7 +29,7 @@ mod algorithm_oids { pub const EC: &str = "1.2.840.10045.2.1"; /// OID for `id-Ed25519`, if you're curious pub const Ed25519: &str = "1.3.101.112"; - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] /// OID for `id-rsaEncryption`, if you're curious pub const rsaEncryption: &str = "1.2.840.113549.1.1.1"; } @@ -53,10 +53,10 @@ pub enum SecretKey { EcdsaP256Sha256(EcSecretKey), /// ed25519 Ed25519(Ed25519SecretKey), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] /// rsa-v1_5-sha256 RsaV1_5Sha256(pkcs1v15::SigningKey), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] RsaPssSha512(pss::SigningKey), } @@ -81,14 +81,14 @@ impl SecretKey { let sk = ed25519_compact::KeyPair::from_seed(ed25519_compact::Seed::new(seed)).sk; Ok(Self::Ed25519(sk)) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] AlgorithmName::RsaV1_5Sha256 => { debug!("Read RSA private key"); // read PrivateKeyInfo.private_key as RsaPrivateKey (RFC 3447), which is DER encoded RSAPrivateKey in PKCS#1 let sk = RsaPrivateKey::from_pkcs1_der(bytes).map_err(|e| HttpSigError::ParsePrivateKeyError(e.to_string()))?; Ok(Self::RsaV1_5Sha256(pkcs1v15::SigningKey::::new(sk))) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] AlgorithmName::RsaPssSha512 => { debug!("Read RSA-PSS private key"); // read PrivateKeyInfo.private_key as RsaPrivateKey (RFC 3447), which is DER encoded RSAPrivateKey in PKCS#1 @@ -133,8 +133,15 @@ impl SecretKey { &pki.private_key[2..] } // rsa - #[cfg(feature = "rsasig")] - algorithm_oids::rsaEncryption => pki.private_key, + #[cfg(feature = "rsa-signature")] + algorithm_oids::rsaEncryption => { + // assert algorithm + match alg { + AlgorithmName::RsaV1_5Sha256 | AlgorithmName::RsaPssSha512 => {} + _ => return Err(HttpSigError::ParsePrivateKeyError("Algorithm mismatch".to_string())), + } + pki.private_key + } _ => return Err(HttpSigError::ParsePrivateKeyError("Unsupported algorithm".to_string())), }; let sk = Self::from_bytes(alg, sk_bytes)?; @@ -156,9 +163,9 @@ impl SecretKey { Self::EcdsaP256Sha256(key) => PublicKey::EcdsaP256Sha256(key.public_key()), Self::EcdsaP384Sha384(key) => PublicKey::EcdsaP384Sha384(key.public_key()), Self::Ed25519(key) => PublicKey::Ed25519(key.public_key()), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaV1_5Sha256(key) => PublicKey::RsaV1_5Sha256(key.verifying_key()), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaPssSha512(key) => PublicKey::RsaPssSha512(key.verifying_key()), } } @@ -189,13 +196,13 @@ impl super::SigningKey for SecretKey { let sig = sk.sign(data, Some(ed25519_compact::Noise::default())); Ok(sig.as_ref().to_vec()) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaV1_5Sha256(sk) => { debug!("Sign RsaV1_5Sha256"); let sig = sk.sign_with_rng(&mut rand::rng(), data); Ok(sig.to_vec()) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaPssSha512(sk) => { debug!("Sign RsaPssSha512"); let sig = sk.sign_with_rng(&mut rand::rng(), data); @@ -240,10 +247,10 @@ pub enum PublicKey { EcdsaP384Sha384(EcPublicKey), /// ed25519 Ed25519(Ed25519PublicKey), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] /// rsa-v1_5-sha256 RsaV1_5Sha256(pkcs1v15::VerifyingKey), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] /// rsa-pss-sha512 RsaPssSha512(pss::VerifyingKey), } @@ -267,17 +274,17 @@ impl PublicKey { let pk = ed25519_compact::PublicKey::from_slice(bytes).map_err(|e| HttpSigError::ParsePublicKeyError(e.to_string()))?; Ok(Self::Ed25519(pk)) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] AlgorithmName::RsaV1_5Sha256 => { debug!("Read RSA public key"); - // read RsaPublicKey in SubjectPublicKeyInfo format in PKCS#8, which is DER encoded RSAPublicKey in PKCS#1 + // read RsaPublicKey in PKCS#1 DER format let pk = RsaPublicKey::from_pkcs1_der(bytes).map_err(|e| HttpSigError::ParsePublicKeyError(e.to_string()))?; Ok(Self::RsaV1_5Sha256(pkcs1v15::VerifyingKey::new(pk))) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] AlgorithmName::RsaPssSha512 => { debug!("Read RSA-PSS public key"); - // read RsaPublicKey in SubjectPublicKeyInfo format in PKCS#8, which is DER encoded RSAPublicKey in PKCS#1 + // read RsaPublicKey in PKCS#1 DER format let pk = RsaPublicKey::from_pkcs1_der(bytes).map_err(|e| HttpSigError::ParsePublicKeyError(e.to_string()))?; Ok(Self::RsaPssSha512(pss::VerifyingKey::new(pk))) } @@ -329,11 +336,17 @@ impl PublicKey { .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))? } // rsa - #[cfg(feature = "rsasig")] - algorithm_oids::rsaEncryption => spki_ref - .subject_public_key - .as_bytes() - .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))?, + #[cfg(feature = "rsa-signature")] + algorithm_oids::rsaEncryption => { + match alg { + AlgorithmName::RsaV1_5Sha256 | AlgorithmName::RsaPssSha512 => {} + _ => return Err(HttpSigError::ParsePublicKeyError("Algorithm mismatch".to_string())), + } + spki_ref + .subject_public_key + .as_bytes() + .ok_or(HttpSigError::ParsePublicKeyError("Invalid public key".to_string()))? + } _ => return Err(HttpSigError::ParsePublicKeyError("Unsupported algorithm".to_string())), }; Self::from_bytes(alg, pk_bytes) @@ -371,14 +384,14 @@ impl super::VerifyingKey for PublicKey { pk.verify(data, &sig) .map_err(|e| HttpSigError::InvalidSignature(e.to_string())) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaV1_5Sha256(pk) => { debug!("Verify RsaV1_5Sha256"); let sig = pkcs1v15::Signature::try_from(signature).map_err(|e| HttpSigError::ParseSignatureError(e.to_string()))?; pk.verify(data, &sig) .map_err(|e| HttpSigError::InvalidSignature(e.to_string())) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaPssSha512(pk) => { debug!("Verify RsaPssSha512"); let sig = pss::Signature::try_from(signature).map_err(|e| HttpSigError::ParseSignatureError(e.to_string()))?; @@ -391,7 +404,7 @@ impl super::VerifyingKey for PublicKey { /// Create key id, created by SHA-256 hash of the public key bytes, then encoded in base64 /// - For ECDSA keys, use the uncompressed SEC1 encoding of the public key point as the byte representation. /// - For Ed25519 keys, use the raw 32-byte public key. - /// - For RSA keys, use the DER encoding of the RSAPublicKey structure (SubjectPublicKeyInfo) as defined in PKCS#1. + /// - For RSA keys, use the DER encoding of the RSAPublicKey structure in PKCS#1 format. fn key_id(&self) -> String { use base64::{engine::general_purpose, Engine as _}; @@ -399,18 +412,18 @@ impl super::VerifyingKey for PublicKey { Self::EcdsaP256Sha256(vk) => vk.to_encoded_point(true).as_bytes().to_vec(), Self::EcdsaP384Sha384(vk) => vk.to_encoded_point(true).as_bytes().to_vec(), Self::Ed25519(vk) => vk.as_ref().to_vec(), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaV1_5Sha256(vk) => vk .as_ref() .to_pkcs1_der() .map(|der| der.as_bytes().to_vec()) - .unwrap_or_default(), - #[cfg(feature = "rsasig")] + .unwrap_or(b"rsa-der-serialization-failed".to_vec()), + #[cfg(feature = "rsa-signature")] Self::RsaPssSha512(vk) => vk .as_ref() .to_pkcs1_der() .map(|der| der.as_bytes().to_vec()) - .unwrap_or_default(), + .unwrap_or(b"rsa-der-serialization-failed".to_vec()), }; let mut hasher = ::new(); hasher.update(&bytes); @@ -424,9 +437,9 @@ impl super::VerifyingKey for PublicKey { Self::EcdsaP256Sha256(_) => AlgorithmName::EcdsaP256Sha256, Self::EcdsaP384Sha384(_) => AlgorithmName::EcdsaP384Sha384, Self::Ed25519(_) => AlgorithmName::Ed25519, - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaV1_5Sha256(_) => AlgorithmName::RsaV1_5Sha256, - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] Self::RsaPssSha512(_) => AlgorithmName::RsaPssSha512, } } @@ -473,7 +486,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= -----END PUBLIC KEY----- "##; - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] const RSA2048_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCrjtdIxemmmL9V wfp7qqwytfRDZqQM6XNWcAi3x+j5dHFFIKKWQktJ7eCTRYQrBwjQs5sb0ieNUYwQ @@ -503,7 +516,7 @@ KDYy4jUp2TeTPBpqwS24KzFaFx0y2U99TWrzt6sQJr7Y9NlR7S0znc/L7wwFobjr XVdlU40OaPP7xs0er/tWVAPY -----END PRIVATE KEY-----"##; - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] const RSA2048_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq47XSMXpppi/VcH6e6qs MrX0Q2akDOlzVnAIt8fo+XRxRSCilkJLSe3gk0WEKwcI0LObG9InjVGMEL0yB+d8 @@ -552,7 +565,7 @@ tQIDAQAB assert!(matches!(pk, PublicKey::Ed25519(_))); } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] #[test] fn test_from_pem_rsa() { let sk = SecretKey::from_pem(&AlgorithmName::RsaV1_5Sha256, RSA2048_SECRET_KEY).unwrap(); @@ -591,7 +604,7 @@ tQIDAQAB assert!(pk.verify(b"hello", &signature).is_err()); } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] #[test] fn test_sign_verify_rsa() { use super::super::{SigningKey, VerifyingKey}; @@ -630,7 +643,7 @@ tQIDAQAB Ok(()) } - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] #[test] fn test_kid_rsa() -> HttpSigResult<()> { use super::super::VerifyingKey; diff --git a/httpsig/src/crypto/mod.rs b/httpsig/src/crypto/mod.rs index 74c1bb4..ff02cd0 100644 --- a/httpsig/src/crypto/mod.rs +++ b/httpsig/src/crypto/mod.rs @@ -13,9 +13,9 @@ pub enum AlgorithmName { EcdsaP256Sha256, EcdsaP384Sha384, Ed25519, - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] RsaV1_5Sha256, - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] RsaPssSha512, } @@ -26,9 +26,9 @@ impl AlgorithmName { AlgorithmName::EcdsaP256Sha256 => "ecdsa-p256-sha256", AlgorithmName::EcdsaP384Sha384 => "ecdsa-p384-sha384", AlgorithmName::Ed25519 => "ed25519", - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] AlgorithmName::RsaV1_5Sha256 => "rsa-v1_5-sha256", - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] AlgorithmName::RsaPssSha512 => "rsa-pss-sha512", } } @@ -49,9 +49,9 @@ impl core::str::FromStr for AlgorithmName { "ecdsa-p256-sha256" => Ok(Self::EcdsaP256Sha256), "ecdsa-p384-sha384" => Ok(Self::EcdsaP384Sha384), "ed25519" => Ok(Self::Ed25519), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] "rsa-v1_5-sha256" => Ok(Self::RsaV1_5Sha256), - #[cfg(feature = "rsasig")] + #[cfg(feature = "rsa-signature")] "rsa-pss-sha512" => Ok(Self::RsaPssSha512), _ => Err(HttpSigError::InvalidAlgorithmName(s.to_string())), } From 2b0ab651b87d241cf11900b7633b902528162233 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 14 Feb 2026 01:11:30 +0900 Subject: [PATCH 18/27] chore: make rsa-signature non-default --- httpsig-hyper/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index a5bc42a..de941b9 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,7 +13,7 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["blocking", "rsa-signature"] +default = ["blocking"] blocking = ["futures/executor"] rsa-signature = ["httpsig/rsa-signature"] From 0aacd1d9c50be41b1dd821b23a10545758729359 Mon Sep 17 00:00:00 2001 From: Serhij S Date: Sat, 14 Feb 2026 00:20:36 +0100 Subject: [PATCH 19/27] fixed digest verification --- httpsig-hyper/src/hyper_content_digest.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpsig-hyper/src/hyper_content_digest.rs b/httpsig-hyper/src/hyper_content_digest.rs index 3c3a9bb..5564289 100644 --- a/httpsig-hyper/src/hyper_content_digest.rs +++ b/httpsig-hyper/src/hyper_content_digest.rs @@ -141,7 +141,7 @@ where .map_err(|_e| HyperDigestError::HttpBodyError("Failed to get body bytes".to_string()))?; let digest = derive_digest(&body_bytes, &cd_type); - if matches!(digest, _expected_digest) { + if digest == _expected_digest { let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); let res = Request::from_parts(header, new_body); Ok(res) @@ -192,7 +192,7 @@ where .map_err(|_e| HyperDigestError::HttpBodyError("Failed to get body bytes".to_string()))?; let digest = derive_digest(&body_bytes, &cd_type); - if matches!(digest, _expected_digest) { + if digest == _expected_digest { let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); let res = Response::from_parts(header, new_body); Ok(res) From f36de08396f8494e1b4165b8e4045f7405de75ce Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 14 Feb 2026 11:16:12 +0900 Subject: [PATCH 20/27] fix: Security hardening: constant-time digest comparison + regression tests --- httpsig-hyper/Cargo.toml | 1 + httpsig-hyper/src/hyper_content_digest.rs | 129 +++++++++++++++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index de941b9..bd5218d 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -28,6 +28,7 @@ futures = { version = "0.3.31", default-features = false, features = [ "async-await", ] } indexmap = { version = "2.11.4" } +subtle = { version = "2.6.1", default-features = false } # content digest with rfc8941 structured field values sha2 = { version = "0.10.9", default-features = false } diff --git a/httpsig-hyper/src/hyper_content_digest.rs b/httpsig-hyper/src/hyper_content_digest.rs index 5564289..b8657ed 100644 --- a/httpsig-hyper/src/hyper_content_digest.rs +++ b/httpsig-hyper/src/hyper_content_digest.rs @@ -8,6 +8,7 @@ use http_body_util::{combinators::BoxBody, BodyExt, Full}; use sha2::Digest; use std::future::Future; use std::str::FromStr; +use subtle::ConstantTimeEq; // hyper's http specific extension to generate and verify http signature @@ -141,7 +142,8 @@ where .map_err(|_e| HyperDigestError::HttpBodyError("Failed to get body bytes".to_string()))?; let digest = derive_digest(&body_bytes, &cd_type); - if digest == _expected_digest { + // Use constant time equality check to prevent timing attacks + if is_equal_digest(&digest, &_expected_digest) { let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); let res = Request::from_parts(header, new_body); Ok(res) @@ -192,7 +194,8 @@ where .map_err(|_e| HyperDigestError::HttpBodyError("Failed to get body bytes".to_string()))?; let digest = derive_digest(&body_bytes, &cd_type); - if digest == _expected_digest { + // Use constant time equality check to prevent timing attacks + if is_equal_digest(&digest, &_expected_digest) { let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); let res = Response::from_parts(header, new_body); Ok(res) @@ -204,6 +207,16 @@ where } } +// Constant time equality check for digest verification to prevent timing attacks +fn is_equal_digest(digest1: &[u8], digest2: &[u8]) -> bool { + // Early return if the lengths are different to prevent unnecessary computation, + // which is not a security risk in this context since the digest lengths are fixed for each algorithm. + if digest1.len() != digest2.len() { + return false; + } + digest1.ct_eq(digest2).into() +} + async fn extract_content_digest(header_map: &http::HeaderMap) -> HyperDigestResult<(ContentDigestType, Vec)> { let content_digest_header = header_map .get(CONTENT_DIGEST_HEADER) @@ -301,4 +314,116 @@ mod tests { let verified = res.verify_content_digest().await; assert!(verified.is_ok()); } + + #[tokio::test] + async fn hyper_request_digest_mismatch_by_body_tamper_should_fail() { + // 1) Create a request and set a correct Content-Digest for the original body + let body = Full::new(&b"{\"hello\": \"world\"}"[..]); + let req = Request::builder() + .method("GET") + .uri("https://example.com/") + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .header("content-type", "application/json") + .body(body) + .unwrap(); + + let req = req.set_content_digest(&ContentDigestType::Sha256).await.unwrap(); + assert!(req.headers().contains_key(CONTENT_DIGEST_HEADER)); + + // 2) Tamper the body while keeping the digest header unchanged + let (parts, _old_body) = req.into_parts(); + let tampered_body = Full::new(&b"{\"hello\": \"pwned\"}"[..]).boxed(); + let tampered_req = Request::from_parts(parts, tampered_body); + + // 3) Verification must fail + let verified = tampered_req.verify_content_digest().await; + assert!(verified.is_err()); + match verified.err().unwrap() { + HyperDigestError::InvalidContentDigest(_) => {} + e => panic!("unexpected error: {e:?}"), + } + } + + #[tokio::test] + async fn hyper_response_digest_mismatch_by_header_tamper_should_fail() { + // 1) Create a response and set a correct Content-Digest + let body = Full::new(&b"{\"hello\": \"world\"}"[..]); + let res = Response::builder() + .status(200) + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .header("content-type", "application/json") + .body(body) + .unwrap(); + + let res = res.set_content_digest(&ContentDigestType::Sha256).await.unwrap(); + let (mut parts, body) = res.into_parts(); + + // 2) Tamper the Content-Digest header (keep it syntactically valid) + // Expected digest is: X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= + // Change the first character to another valid base64 character. + parts.headers.insert( + CONTENT_DIGEST_HEADER, + "sha-256=:Y48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:".parse().unwrap(), + ); + + let tampered_res = Response::from_parts(parts, body); + + // 3) Verification must fail + let verified = tampered_res.verify_content_digest().await; + assert!(verified.is_err()); + match verified.err().unwrap() { + HyperDigestError::InvalidContentDigest(_) => {} + e => panic!("unexpected error: {e:?}"), + } + } + + #[tokio::test] + async fn hyper_request_missing_content_digest_header_should_fail() { + let body = Full::new(&b"{\"hello\": \"world\"}"[..]); + let req = Request::builder() + .method("GET") + .uri("https://example.com/") + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .header("content-type", "application/json") + .body(body) + .unwrap(); + + // No set_content_digest() call => header missing + let verified = req.verify_content_digest().await; + assert!(verified.is_err()); + match verified.err().unwrap() { + HyperDigestError::NoDigestHeader(_) => {} + e => panic!("unexpected error: {e:?}"), + } + } + + #[tokio::test] + async fn hyper_request_digest_length_mismatch_should_fail() { + // 1) Create a request and attach a valid Content-Digest header + let body = Full::new(&b"{\"hello\": \"world\"}"[..]); + let req = Request::builder() + .method("GET") + .uri("https://example.com/") + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .header("content-type", "application/json") + .body(body) + .unwrap(); + + let req = req.set_content_digest(&ContentDigestType::Sha256).await.unwrap(); + + // 2) Extract parts and replace the Content-Digest header + // with a syntactically valid but length-mismatched base64 value. + // This ensures that length mismatches are properly rejected. + let (mut parts, body) = req.into_parts(); + + parts + .headers + .insert(CONTENT_DIGEST_HEADER, "sha-256=:AAAA=:".parse().unwrap()); + + let tampered_req = Request::from_parts(parts, body); + + // 3) Verification must fail due to digest length mismatch + let verified = tampered_req.verify_content_digest().await; + assert!(verified.is_err()); + } } From 7a7cedc6489c72ac756b5585555cdb2fe3315770 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 14 Feb 2026 11:16:41 +0900 Subject: [PATCH 21/27] bump --- Cargo.toml | 2 +- httpsig-hyper/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 06b5f55..81f2267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.22" +version = "0.0.23" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index bd5218d..0b4a8fd 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -19,7 +19,7 @@ rsa-signature = ["httpsig/rsa-signature"] [dependencies] -httpsig = { path = "../httpsig", version = "0.0.22" } +httpsig = { path = "../httpsig", version = "0.0.23" } thiserror = { version = "2.0.18" } tracing = { version = "0.1.44" } From 669614d008b15f140f0d47bd09d45638c3e21c73 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 14 Feb 2026 11:21:25 +0900 Subject: [PATCH 22/27] chore(refactor) --- httpsig-hyper/src/hyper_content_digest.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/httpsig-hyper/src/hyper_content_digest.rs b/httpsig-hyper/src/hyper_content_digest.rs index b8657ed..51eb75c 100644 --- a/httpsig-hyper/src/hyper_content_digest.rs +++ b/httpsig-hyper/src/hyper_content_digest.rs @@ -134,7 +134,7 @@ where Self: Sized, { let header_map = self.headers(); - let (cd_type, _expected_digest) = extract_content_digest(header_map).await?; + let (cd_type, expected_digest) = extract_content_digest(header_map).await?; let (header, body) = self.into_parts(); let body_bytes = body .into_bytes() @@ -143,7 +143,7 @@ where let digest = derive_digest(&body_bytes, &cd_type); // Use constant time equality check to prevent timing attacks - if is_equal_digest(&digest, &_expected_digest) { + if is_equal_digest(&digest, &expected_digest) { let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); let res = Request::from_parts(header, new_body); Ok(res) @@ -186,7 +186,7 @@ where Self: Sized, { let header_map = self.headers(); - let (cd_type, _expected_digest) = extract_content_digest(header_map).await?; + let (cd_type, expected_digest) = extract_content_digest(header_map).await?; let (header, body) = self.into_parts(); let body_bytes = body .into_bytes() @@ -195,7 +195,7 @@ where let digest = derive_digest(&body_bytes, &cd_type); // Use constant time equality check to prevent timing attacks - if is_equal_digest(&digest, &_expected_digest) { + if is_equal_digest(&digest, &expected_digest) { let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); let res = Response::from_parts(header, new_body); Ok(res) From 4b8a04f1716f8fbc381b81ce8aa9877941372324 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sun, 15 Feb 2026 14:12:16 +0900 Subject: [PATCH 23/27] fix(bug): fix the internal validation logic for parametes of derived-components --- httpsig-hyper/src/hyper_http.rs | 54 ++++++++++++++++++---- httpsig/src/message_component/component.rs | 5 +- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index ad9b2ac..2b31561 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -824,13 +824,37 @@ fn extract_derived_component( "invalid http message component name as derived component".to_string(), )); }; - if !id.params.0.is_empty() - && matches!(req_or_res, RequestOrResponse::Request(_)) - && !(id.params.0.contains(&HttpMessageComponentParam::Req) && id.params.0.len() == 1) - { - return Err(HyperSigError::InvalidComponentParam( - "derived component does not allow parameters for request".to_string(), - )); + // Validate parameters allowed on derived components (RFC 9421). + // - `name`: only valid on `@query-param` + // - `req`: only valid on response messages (to reference request-derived components, §2.4) + // - `sf`, `key`, `bs`, `tr`: only valid on HTTP field components, not derived components + for param in id.params.0.iter() { + match param { + HttpMessageComponentParam::Name(_) => { + if !matches!(derived_id, DerivedComponentName::QueryParam) { + return Err(HyperSigError::InvalidComponentParam( + "`name` parameter is only allowed for `@query-param`".to_string(), + )); + } + } + HttpMessageComponentParam::Req => { + // `req` is only meaningful in response signatures (RFC 9421 §2.4). + // `build_signature_base` already validates this and re-dispatches extraction + // against the original request, so `req_or_res` here should always be + // `Request`. Guard against misuse by callers that bypass `build_signature_base`. + if !matches!(req_or_res, RequestOrResponse::Request(_)) { + return Err(HyperSigError::InvalidComponentParam( + "`req`-tagged component must be extracted from the source request".to_string(), + )); + } + } + _ => { + return Err(HyperSigError::InvalidComponentParam(format!( + "parameter `{}` is not allowed on derived components", + String::from(param.clone()) + ))); + } + } } match req_or_res { @@ -842,9 +866,21 @@ fn extract_derived_component( } } RequestOrResponse::Response(_) => { - if !matches!(derived_id, DerivedComponentName::Status) && !matches!(derived_id, DerivedComponentName::SignatureParams) { + let has_req = id.params.0.contains(&HttpMessageComponentParam::Req); + // Response messages can use `@status` and `@signature-params` directly, + // or any request-derived component with the `req` parameter (RFC 9421 §2.4). + if !matches!(derived_id, DerivedComponentName::Status) + && !matches!(derived_id, DerivedComponentName::SignatureParams) + && !has_req + { return Err(HyperSigError::InvalidComponentName( - "Only `status` and `signature-params` are allowed for response".to_string(), + "derived components other than `@status` and `@signature-params` require `req` parameter for response".to_string(), + )); + } + // `@status` must not have `req` parameter + if matches!(derived_id, DerivedComponentName::Status) && has_req { + return Err(HyperSigError::InvalidComponentParam( + "`@status` does not accept `req` parameter".to_string(), )); } } diff --git a/httpsig/src/message_component/component.rs b/httpsig/src/message_component/component.rs index a74ade7..4fed6e1 100644 --- a/httpsig/src/message_component/component.rs +++ b/httpsig/src/message_component/component.rs @@ -283,9 +283,12 @@ mod tests { #[test] fn test_field_params_derived_component() { // params check - // only req field param is allowed + // only req and query-param field params are allowed let comp = HttpMessageComponent::try_from("\"@method\";req: POST"); assert!(comp.is_ok()); + let comp = HttpMessageComponent::try_from("\"@query-param\";name=\"id\": POST"); + assert!(comp.is_ok()); + let comp = HttpMessageComponent::try_from("\"@method\";bs: POST"); assert!(comp.is_err()); let comp = HttpMessageComponent::try_from("\"@method\";key=\"hoge\": POST"); From 2b2994a1ec92478ede667ca7e329e5afc5cf2b84 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sun, 15 Feb 2026 14:28:01 +0900 Subject: [PATCH 24/27] fix(bug): fix the bug for error propagation --- httpsig-hyper/src/hyper_http.rs | 241 ++++++++++++++++++++++++++------ 1 file changed, 198 insertions(+), 43 deletions(-) diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 2b31561..8bb8433 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -286,14 +286,20 @@ where T: SigningKey + Sync, { let req_or_res = RequestOrResponse::Request(self); - let vec_signature_headers_fut = params_key_name.iter().flat_map(|(params, key, name)| { - build_signature_base(&req_or_res, params, None as Option<&Request<()>>) - .map(|base| async move { base.build_signature_headers(*key, *name) }) - }); - let vec_signature_headers = futures::future::join_all(vec_signature_headers_fut) - .await - .into_iter() + let vec_signature_bases = params_key_name + .iter() + .map(|(params, 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( + vec_signature_bases + .into_iter() + .map(|(base, key, name)| async move { base.build_signature_headers(key, name) }), + ) + .await + .into_iter() + .collect::, _>>()?; vec_signature_headers.iter().try_for_each(|headers| { self .headers_mut() @@ -405,14 +411,18 @@ where { let req_or_res = RequestOrResponse::Response(self); - let vec_signature_headers_fut = params_key_name.iter().flat_map(|(params, key, name)| { - build_signature_base(&req_or_res, params, req_for_param) - .map(|base| async move { base.build_signature_headers(*key, *name) }) - }); - let vec_signature_headers = futures::future::join_all(vec_signature_headers_fut) - .await - .into_iter() + 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))) .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) }), + ) + .await + .into_iter() + .collect::, _>>()?; vec_signature_headers.iter().try_for_each(|headers| { self @@ -828,34 +838,24 @@ fn extract_derived_component( // - `name`: only valid on `@query-param` // - `req`: only valid on response messages (to reference request-derived components, §2.4) // - `sf`, `key`, `bs`, `tr`: only valid on HTTP field components, not derived components - for param in id.params.0.iter() { - match param { - HttpMessageComponentParam::Name(_) => { - if !matches!(derived_id, DerivedComponentName::QueryParam) { - return Err(HyperSigError::InvalidComponentParam( - "`name` parameter is only allowed for `@query-param`".to_string(), - )); - } - } - HttpMessageComponentParam::Req => { - // `req` is only meaningful in response signatures (RFC 9421 §2.4). - // `build_signature_base` already validates this and re-dispatches extraction - // against the original request, so `req_or_res` here should always be - // `Request`. Guard against misuse by callers that bypass `build_signature_base`. - if !matches!(req_or_res, RequestOrResponse::Request(_)) { - return Err(HyperSigError::InvalidComponentParam( - "`req`-tagged component must be extracted from the source request".to_string(), - )); - } - } - _ => { - return Err(HyperSigError::InvalidComponentParam(format!( - "parameter `{}` is not allowed on derived components", - String::from(param.clone()) - ))); - } - } - } + id.params.0.iter().try_for_each(|param| match param { + HttpMessageComponentParam::Name(_) if matches!(derived_id, DerivedComponentName::QueryParam) => Ok(()), + HttpMessageComponentParam::Name(_) => Err(HyperSigError::InvalidComponentParam( + "`name` parameter is only allowed for `@query-param`".to_string(), + )), + // `req` is only meaningful in response signatures (RFC 9421 §2.4). + // `build_signature_base` already validates this and re-dispatches extraction against the + // original request, so `req_or_res` here should always be `Request`. + // Guard against misuse by callers that bypass `build_signature_base`. + HttpMessageComponentParam::Req if matches!(req_or_res, RequestOrResponse::Request(_)) => Ok(()), + HttpMessageComponentParam::Req => Err(HyperSigError::InvalidComponentParam( + "`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()) + ))), + })?; match req_or_res { RequestOrResponse::Request(_) => { @@ -953,7 +953,7 @@ mod tests { }, *, }; - use http_body_util::Full; + use http_body_util::{BodyExt, Full}; use httpsig::prelude::{AlgorithmName, PublicKey, SecretKey, SharedKey}; type BoxBody = http_body_util::combinators::BoxBody; @@ -1322,4 +1322,159 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); assert!(verification_res.is_ok()); } + + // ---- Issue #17: @query-param;name="..." ---- + + /// Helper to build a request with query parameters (no content-digest needed) + fn build_query_request() -> Request { + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + Request::builder() + .method("GET") + .uri("https://example.com/path?foo=bar&id=123&x=y") + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .body(body) + .unwrap() + } + + /// Regression test for issue #17: @query-param;name="id" must produce signature headers (sync) + #[cfg(feature = "blocking")] + #[test] + fn test_query_param_sign_verify_sync() { + let mut req = build_query_request(); + + let covered = ["@method", "\"@query-param\";name=\"id\"", "date"]; + let covered_components = covered + .iter() + .map(|v| HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + signature_params.set_key_info(&secret_key); + + req + .set_message_signature_sync(&signature_params, &secret_key, Some("qp")) + .unwrap(); + + assert!( + req.headers().get("signature-input").is_some(), + "signature-input header is missing" + ); + assert!(req.headers().get("signature").is_some(), "signature header is missing"); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature_sync(&public_key, None); + assert!( + verification_res.is_ok(), + "signature verification failed: {:?}", + verification_res.err() + ); + } + + /// Regression test for issue #17: @query-param;name="id" must produce signature headers (async) + #[tokio::test] + async fn test_query_param_sign_verify_async() { + let mut req = build_query_request(); + + let covered = ["@method", "\"@query-param\";name=\"id\"", "date"]; + let covered_components = covered + .iter() + .map(|v| HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + signature_params.set_key_info(&secret_key); + + req + .set_message_signature(&signature_params, &secret_key, Some("qp")) + .await + .unwrap(); + + assert!( + req.headers().get("signature-input").is_some(), + "signature-input header is missing" + ); + assert!(req.headers().get("signature").is_some(), "signature header is missing"); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature(&public_key, None).await; + assert!( + verification_res.is_ok(), + "signature verification failed: {:?}", + verification_res.err() + ); + } + + // ---- Derived component parameter validation ---- + + #[test] + fn test_extract_derived_component_rejects_name_on_non_query_param() { + let req = build_query_request(); + let req_or_res = RequestOrResponse::Request(&req); + // `@method;name="foo"` is invalid — `name` is only for `@query-param` + let id = HttpMessageComponentId::try_from("\"@method\";name=\"foo\""); + // component_id parsing itself may reject this; if it doesn't, extraction should + if let Ok(id) = id { + let result = extract_derived_component(&req_or_res, &id); + assert!(result.is_err(), "expected error for `name` on `@method`"); + } + } + + #[test] + fn test_extract_derived_component_rejects_sf_on_derived() { + let req = build_query_request(); + let req_or_res = RequestOrResponse::Request(&req); + // `@method;sf` is invalid — `sf` is only for HTTP field components + let id = HttpMessageComponentId::try_from("\"@method\";sf"); + if let Ok(id) = id { + let result = extract_derived_component(&req_or_res, &id); + assert!(result.is_err(), "expected error for `sf` on derived component"); + } + } + + // ---- Error propagation ---- + + #[tokio::test] + async fn test_set_message_signature_propagates_build_error() { + // Use `@status` on a request — this is invalid and must return Err, not Ok + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut req: Request = Request::builder() + .method("GET") + .uri("https://example.com/") + .body(body) + .unwrap(); + + let covered = vec![HttpMessageComponentId::try_from("@status").unwrap()]; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered).unwrap(); + signature_params.set_key_info(&secret_key); + + let result = req + .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"); + } + + #[cfg(feature = "blocking")] + #[test] + fn test_set_message_signature_sync_propagates_build_error() { + // Same as above but for sync path + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut req: Request = Request::builder() + .method("GET") + .uri("https://example.com/") + .body(body) + .unwrap(); + + let covered = vec![HttpMessageComponentId::try_from("@status").unwrap()]; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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); + assert!(result.is_err(), "expected Err when using `@status` on request, got Ok"); + } } From 19ac193788186fa7991fc191a4c1d5037a8d7da9 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sun, 15 Feb 2026 14:31:27 +0900 Subject: [PATCH 25/27] chore(refactor): add tests for validation --- httpsig-hyper/src/hyper_http.rs | 103 ++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 8bb8433..6bca611 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -1477,4 +1477,107 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== 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"); } + + // ---- RFC 9421 §2.2: Derived component extraction values ---- + + #[test] + fn test_extract_derived_components_values() { + let req = build_query_request(); + // URI: https://example.com/path?foo=bar&id=123&x=y + let req_or_res = RequestOrResponse::Request(&req); + + // @method (§2.2.1) + let id = HttpMessageComponentId::try_from("@method").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@method\": GET"); + + // @target-uri (§2.2.2) + let id = HttpMessageComponentId::try_from("@target-uri").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@target-uri\": https://example.com/path?foo=bar&id=123&x=y"); + + // @authority (§2.2.3) + let id = HttpMessageComponentId::try_from("@authority").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@authority\": example.com"); + + // @scheme (§2.2.4) + let id = HttpMessageComponentId::try_from("@scheme").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@scheme\": https"); + + // @path (§2.2.6) + let id = HttpMessageComponentId::try_from("@path").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@path\": /path"); + + // @query (§2.2.7) + let id = HttpMessageComponentId::try_from("@query").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@query\": ?foo=bar&id=123&x=y"); + + // @query-param;name="id" (§2.2.8) + let id = HttpMessageComponentId::try_from("\"@query-param\";name=\"id\"").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@query-param\";name=\"id\": 123"); + } + + // ---- RFC 9421 §2.4: @query-param;name="...";req on response ---- + + #[tokio::test] + async fn test_response_with_query_param_req_sign_verify() { + // Build a request with query params + let req = build_query_request(); + // Build a response + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut res: Response = Response::builder().status(200).body(body).unwrap(); + + // Response signature covering @status + @query-param;name="id";req + let covered = ["@status", "\"@query-param\";name=\"id\";req"]; + let covered_components = covered + .iter() + .map(|v| HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + signature_params.set_key_info(&secret_key); + + res + .set_message_signature(&signature_params, &secret_key, None, Some(&req)) + .await + .unwrap(); + + assert!(req.headers().get("signature-input").is_none(), "request should not be modified"); + assert!(res.headers().get("signature-input").is_some(), "signature-input header is missing on response"); + assert!(res.headers().get("signature").is_some(), "signature header is missing on response"); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; + assert!(verification_res.is_ok(), "signature verification failed: {:?}", verification_res.err()); + } + + // ---- RFC 9421: Response must reject request-derived components without `req` ---- + + #[tokio::test] + async fn test_response_rejects_derived_component_without_req() { + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut res: Response = Response::builder().status(200).body(body).unwrap(); + + // `@method` without `req` on a response — must fail + let covered = vec![ + HttpMessageComponentId::try_from("@status").unwrap(), + HttpMessageComponentId::try_from("@method").unwrap(), + ]; + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered).unwrap(); + signature_params.set_key_info(&secret_key); + + let result = res + .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"); + } } From c14769016efcae80e1259844fadf564140442570 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sun, 15 Feb 2026 14:39:39 +0900 Subject: [PATCH 26/27] chore(refactor): split test file from hyper_http.rs --- httpsig-hyper/src/hyper_http.rs | 640 +------------------------- httpsig-hyper/src/hyper_http_tests.rs | 634 +++++++++++++++++++++++++ 2 files changed, 636 insertions(+), 638 deletions(-) create mode 100644 httpsig-hyper/src/hyper_http_tests.rs diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 6bca611..cea0f08 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -943,641 +943,5 @@ fn extract_http_message_component( /* --------------------------------------- */ #[cfg(test)] -mod tests { - - use super::{ - super::{ - error::HyperDigestError, - hyper_content_digest::{RequestContentDigest, ResponseContentDigest}, - ContentDigestType, - }, - *, - }; - use http_body_util::{BodyExt, Full}; - use httpsig::prelude::{AlgorithmName, PublicKey, SecretKey, SharedKey}; - - type BoxBody = http_body_util::combinators::BoxBody; - - const EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIDSHAE++q1BP7T8tk+mJtS+hLf81B0o6CFyWgucDFN/C ------END PRIVATE KEY----- -"##; - const EDDSA_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY----- -MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= ------END PUBLIC KEY----- -"##; - // const EDDSA_KEY_ID: &str = "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="; - const COVERED_COMPONENTS_REQ: &[&str] = &["@method", "date", "content-type", "content-digest"]; - const COVERED_COMPONENTS_RES: &[&str] = &["@status", "\"@method\";req", "date", "content-type", "\"content-digest\";req"]; - - async fn build_request() -> Request { - let body = Full::new(&b"{\"hello\": \"world\"}"[..]); - let req = Request::builder() - .method("GET") - .uri("https://example.com/parameters?var=this%20is%20a%20big%0Amultiline%20value&bar=with+plus+whitespace&fa%C3%A7ade%22%3A%20=something") - .header("date", "Sun, 09 May 2021 18:30:00 GMT") - .header("content-type", "application/json") - .header("content-type", "application/json-patch+json") - .body(body) - .unwrap(); - req.set_content_digest(&ContentDigestType::Sha256).await.unwrap() - } - - async fn build_response() -> Response { - let body = Full::new(&b"{\"hello\": \"world!!\"}"[..]); - let res = Response::builder() - .status(200) - .header("date", "Sun, 09 May 2021 18:30:00 GMT") - .header("content-type", "application/json") - .header("content-type", "application/json-patch+json") - .body(body) - .unwrap(); - res.set_content_digest(&ContentDigestType::Sha256).await.unwrap() - } - - fn build_covered_components_req() -> Vec { - COVERED_COMPONENTS_REQ - .iter() - .map(|&s| HttpMessageComponentId::try_from(s).unwrap()) - .collect() - } - - fn build_covered_components_res() -> Vec { - COVERED_COMPONENTS_RES - .iter() - .map(|&s| HttpMessageComponentId::try_from(s).unwrap()) - .collect() - } - - #[tokio::test] - async fn test_extract_component_from_request() { - let req = build_request().await; - let req_or_res = RequestOrResponse::Request(&req); - - let component_id_method = HttpMessageComponentId::try_from("\"@method\"").unwrap(); - let component = extract_http_message_component(&req_or_res, &component_id_method).unwrap(); - assert_eq!(component.to_string(), "\"@method\": GET"); - - let component_id = HttpMessageComponentId::try_from("\"date\"").unwrap(); - let component = extract_http_message_component(&req_or_res, &component_id).unwrap(); - assert_eq!(component.to_string(), "\"date\": Sun, 09 May 2021 18:30:00 GMT"); - - let component_id = HttpMessageComponentId::try_from("content-type").unwrap(); - let component = extract_http_field(&req_or_res, &component_id).unwrap(); - assert_eq!( - component.to_string(), - "\"content-type\": application/json, application/json-patch+json" - ); - - let component_id = HttpMessageComponentId::try_from("content-digest").unwrap(); - let component = extract_http_message_component(&req_or_res, &component_id).unwrap(); - assert_eq!( - component.to_string(), - "\"content-digest\": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:" - ); - } - - #[tokio::test] - async fn test_extract_signature_params_from_request() { - let mut req = build_request().await; - let headers = req.headers_mut(); - headers.insert( - "signature-input", - http::HeaderValue::from_static(r##"sig1=("@method" "@authority")"##), - ); - let component_id = HttpMessageComponentId::try_from("@signature-params").unwrap(); - let req_or_res = RequestOrResponse::Request(&req); - 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.as_component_value(), r##"("@method" "@authority")"##); - assert_eq!(component.value.key(), Some("sig1")); - } - - #[tokio::test] - async fn test_build_signature_base_from_request() { - let req = build_request().await; - - const SIGPARA: &str = r##";created=1704972031;alg="ed25519";keyid="gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is=""##; - let values = (r##""@method" "content-type" "date" "content-digest""##, SIGPARA); - 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(); - assert_eq!( - signature_base.to_string(), - r##""@method": GET -"content-type": application/json, application/json-patch+json -"date": Sun, 09 May 2021 18:30:00 GMT -"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: -"@signature-params": ("@method" "content-type" "date" "content-digest");created=1704972031;alg="ed25519";keyid="gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is=""## - ); - } - - #[tokio::test] - async fn test_extract_tuples_from_request() { - let mut req = build_request().await; - let headers = req.headers_mut(); - headers.insert( - "signature-input", - http::HeaderValue::from_static(r##"sig11=("@method" "@authority");created=1704972031"##), - ); - headers.insert( - "signature", - http::HeaderValue::from_static( - r##"sig11=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"##, - ), - ); - - 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"); - assert_eq!( - tuples.get("sig11").unwrap().signature_params().to_string(), - r##"("@method" "@authority");created=1704972031"## - ); - } - - #[tokio::test] - async fn test_set_verify_message_signature_req() { - let mut req = build_request().await; - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - 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(); - 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")"##)); - // let signature = req.headers().get("signature").unwrap().to_str().unwrap(); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = req.verify_message_signature(&public_key, None).await; - assert!(verification_res.is_ok()); - } - - #[tokio::test] - async fn test_set_verify_message_signature_res() { - let req = build_request().await; - let mut res = build_response().await; - - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - - let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_res()).unwrap(); - signature_params.set_key_info(&secret_key); - // let req_or_res = RequestOrResponse::Response(&res); - // let base = build_signature_base(&req_or_res, &signature_params, Some(&req)); - // println!("{}", base.unwrap()); - // // println!("{:#?}", req); - - res - .set_message_signature(&signature_params, &secret_key, None, Some(&req)) - .await - .unwrap(); - // println!("{:#?}", res.headers()); - let signature_input = res.headers().get("signature-input").unwrap().to_str().unwrap(); - assert!(signature_input.starts_with(r##"sig=("@status" "@method";req "date" "content-type" "content-digest";req)"##)); - // let signature = req.headers().get("signature").unwrap().to_str().unwrap(); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; - assert!(verification_res.is_ok()); - } - - #[tokio::test] - async fn test_expired_signature() { - let mut req = build_request().await; - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); - signature_params.set_key_info(&secret_key); - let created = signature_params.created.unwrap(); - signature_params.set_expires(created - 1); - assert!(signature_params.is_expired()); - - 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; - assert!(verification_res.is_err()); - } - - #[tokio::test] - async fn test_set_verify_with_signature_name() { - let mut req = build_request().await; - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - 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, Some("custom_sig_name")) - .await - .unwrap(); - - 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 public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = req.verify_message_signature(&public_key, None).await; - assert!(verification_res.is_ok()); - } - - #[tokio::test] - async fn test_set_verify_with_key_id() { - let mut req = build_request().await; - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - 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(); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let key_id = public_key.key_id(); - let verification_res = req.verify_message_signature(&public_key, Some(&key_id)).await; - assert!(verification_res.is_ok()); - - let verification_res = req.verify_message_signature(&public_key, Some("NotFoundKeyId")).await; - assert!(verification_res.is_err()); - } - - const HMACSHA256_SECRET_KEY: &str = - r##"uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ=="##; - - #[tokio::test] - async fn test_set_verify_with_key_id_hmac_sha256() { - let mut req = build_request().await; - let secret_key = SharedKey::from_base64(&AlgorithmName::HmacSha256, HMACSHA256_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); - signature_params.set_key_info(&secret_key); - // Random nonce is highly recommended for HMAC - signature_params.set_random_nonce(); - - 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; - let alg = alg.unwrap(); - let key_id = key_id.unwrap(); - assert_eq!(org_key_id, key_id); - let verification_key = SharedKey::from_base64(&alg, HMACSHA256_SECRET_KEY).unwrap(); - let verification_res = req.verify_message_signature(&verification_key, Some(&key_id)).await; - assert!(verification_res.is_ok()); - - let verification_res = req.verify_message_signature(&verification_key, Some("NotFoundKeyId")).await; - assert!(verification_res.is_err()); - } - - #[tokio::test] - async fn test_get_alg_key_ids() { - let mut req = build_request().await; - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - 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(); - 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); - assert_eq!(key_ids[0].1.as_ref().unwrap(), "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); - } - - const P256_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv7zxW56ojrWwmSo1 -4uOdbVhUfj9Jd+5aZIB9u8gtWnihRANCAARGYsMe0CT6pIypwRvoJlLNs4+cTh2K -L7fUNb5i6WbKxkpAoO+6T3pMBG5Yw7+8NuGTvvtrZAXduA2giPxQ8zCf ------END PRIVATE KEY----- -"##; - const P256_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERmLDHtAk+qSMqcEb6CZSzbOPnE4d -ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== ------END PUBLIC KEY----- -"##; - #[tokio::test] - async fn test_set_verify_multiple_signatures() { - let mut req = build_request().await; - - let secret_key_eddsa = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let mut signature_params_eddsa = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); - signature_params_eddsa.set_key_info(&secret_key_eddsa); - - let secret_key_p256 = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECRET_KEY).unwrap(); - 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")), - ]; - - req.set_message_signatures(params_key_name).await.unwrap(); - - let public_key_eddsa = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let public_key_p256 = PublicKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_PUBLIC_KEY).unwrap(); - let key_id_eddsa = public_key_eddsa.key_id(); - let key_id_p256 = public_key_p256.key_id(); - - let verification_res = req - .verify_message_signatures(&[ - (&public_key_eddsa, Some(&key_id_eddsa)), - (&public_key_p256, Some(&key_id_p256)), - ]) - .await - .unwrap(); - - assert!(verification_res.len() == 2 && verification_res.iter().all(|r| r.is_ok())); - assert!(verification_res[0].as_ref().unwrap() == "eddsa_sig"); - assert!(verification_res[1].as_ref().unwrap() == "p256_sig"); - } - - #[cfg(feature = "blocking")] - #[test] - fn test_blocking_set_verify_message_signature_req() { - let mut req = futures::executor::block_on(build_request()); - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - 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(); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = req.verify_message_signature_sync(&public_key, None); - assert!(verification_res.is_ok()); - } - - #[cfg(feature = "blocking")] - #[test] - fn test_blocking_set_verify_message_signature_res() { - let req = futures::executor::block_on(build_request()); - let mut res = futures::executor::block_on(build_response()); - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - 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)) - .unwrap(); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); - assert!(verification_res.is_ok()); - } - - // ---- Issue #17: @query-param;name="..." ---- - - /// Helper to build a request with query parameters (no content-digest needed) - fn build_query_request() -> Request { - let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); - Request::builder() - .method("GET") - .uri("https://example.com/path?foo=bar&id=123&x=y") - .header("date", "Sun, 09 May 2021 18:30:00 GMT") - .body(body) - .unwrap() - } - - /// Regression test for issue #17: @query-param;name="id" must produce signature headers (sync) - #[cfg(feature = "blocking")] - #[test] - fn test_query_param_sign_verify_sync() { - let mut req = build_query_request(); - - let covered = ["@method", "\"@query-param\";name=\"id\"", "date"]; - let covered_components = covered - .iter() - .map(|v| HttpMessageComponentId::try_from(*v)) - .collect::, _>>() - .unwrap(); - - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); - signature_params.set_key_info(&secret_key); - - req - .set_message_signature_sync(&signature_params, &secret_key, Some("qp")) - .unwrap(); - - assert!( - req.headers().get("signature-input").is_some(), - "signature-input header is missing" - ); - assert!(req.headers().get("signature").is_some(), "signature header is missing"); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = req.verify_message_signature_sync(&public_key, None); - assert!( - verification_res.is_ok(), - "signature verification failed: {:?}", - verification_res.err() - ); - } - - /// Regression test for issue #17: @query-param;name="id" must produce signature headers (async) - #[tokio::test] - async fn test_query_param_sign_verify_async() { - let mut req = build_query_request(); - - let covered = ["@method", "\"@query-param\";name=\"id\"", "date"]; - let covered_components = covered - .iter() - .map(|v| HttpMessageComponentId::try_from(*v)) - .collect::, _>>() - .unwrap(); - - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); - signature_params.set_key_info(&secret_key); - - req - .set_message_signature(&signature_params, &secret_key, Some("qp")) - .await - .unwrap(); - - assert!( - req.headers().get("signature-input").is_some(), - "signature-input header is missing" - ); - assert!(req.headers().get("signature").is_some(), "signature header is missing"); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = req.verify_message_signature(&public_key, None).await; - assert!( - verification_res.is_ok(), - "signature verification failed: {:?}", - verification_res.err() - ); - } - - // ---- Derived component parameter validation ---- - - #[test] - fn test_extract_derived_component_rejects_name_on_non_query_param() { - let req = build_query_request(); - let req_or_res = RequestOrResponse::Request(&req); - // `@method;name="foo"` is invalid — `name` is only for `@query-param` - let id = HttpMessageComponentId::try_from("\"@method\";name=\"foo\""); - // component_id parsing itself may reject this; if it doesn't, extraction should - if let Ok(id) = id { - let result = extract_derived_component(&req_or_res, &id); - assert!(result.is_err(), "expected error for `name` on `@method`"); - } - } - - #[test] - fn test_extract_derived_component_rejects_sf_on_derived() { - let req = build_query_request(); - let req_or_res = RequestOrResponse::Request(&req); - // `@method;sf` is invalid — `sf` is only for HTTP field components - let id = HttpMessageComponentId::try_from("\"@method\";sf"); - if let Ok(id) = id { - let result = extract_derived_component(&req_or_res, &id); - assert!(result.is_err(), "expected error for `sf` on derived component"); - } - } - - // ---- Error propagation ---- - - #[tokio::test] - async fn test_set_message_signature_propagates_build_error() { - // Use `@status` on a request — this is invalid and must return Err, not Ok - let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); - let mut req: Request = Request::builder() - .method("GET") - .uri("https://example.com/") - .body(body) - .unwrap(); - - let covered = vec![HttpMessageComponentId::try_from("@status").unwrap()]; - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&covered).unwrap(); - signature_params.set_key_info(&secret_key); - - let result = req - .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"); - } - - #[cfg(feature = "blocking")] - #[test] - fn test_set_message_signature_sync_propagates_build_error() { - // Same as above but for sync path - let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); - let mut req: Request = Request::builder() - .method("GET") - .uri("https://example.com/") - .body(body) - .unwrap(); - - let covered = vec![HttpMessageComponentId::try_from("@status").unwrap()]; - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - 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); - assert!(result.is_err(), "expected Err when using `@status` on request, got Ok"); - } - - // ---- RFC 9421 §2.2: Derived component extraction values ---- - - #[test] - fn test_extract_derived_components_values() { - let req = build_query_request(); - // URI: https://example.com/path?foo=bar&id=123&x=y - let req_or_res = RequestOrResponse::Request(&req); - - // @method (§2.2.1) - let id = HttpMessageComponentId::try_from("@method").unwrap(); - let c = extract_derived_component(&req_or_res, &id).unwrap(); - assert_eq!(c.to_string(), "\"@method\": GET"); - - // @target-uri (§2.2.2) - let id = HttpMessageComponentId::try_from("@target-uri").unwrap(); - let c = extract_derived_component(&req_or_res, &id).unwrap(); - assert_eq!(c.to_string(), "\"@target-uri\": https://example.com/path?foo=bar&id=123&x=y"); - - // @authority (§2.2.3) - let id = HttpMessageComponentId::try_from("@authority").unwrap(); - let c = extract_derived_component(&req_or_res, &id).unwrap(); - assert_eq!(c.to_string(), "\"@authority\": example.com"); - - // @scheme (§2.2.4) - let id = HttpMessageComponentId::try_from("@scheme").unwrap(); - let c = extract_derived_component(&req_or_res, &id).unwrap(); - assert_eq!(c.to_string(), "\"@scheme\": https"); - - // @path (§2.2.6) - let id = HttpMessageComponentId::try_from("@path").unwrap(); - let c = extract_derived_component(&req_or_res, &id).unwrap(); - assert_eq!(c.to_string(), "\"@path\": /path"); - - // @query (§2.2.7) - let id = HttpMessageComponentId::try_from("@query").unwrap(); - let c = extract_derived_component(&req_or_res, &id).unwrap(); - assert_eq!(c.to_string(), "\"@query\": ?foo=bar&id=123&x=y"); - - // @query-param;name="id" (§2.2.8) - let id = HttpMessageComponentId::try_from("\"@query-param\";name=\"id\"").unwrap(); - let c = extract_derived_component(&req_or_res, &id).unwrap(); - assert_eq!(c.to_string(), "\"@query-param\";name=\"id\": 123"); - } - - // ---- RFC 9421 §2.4: @query-param;name="...";req on response ---- - - #[tokio::test] - async fn test_response_with_query_param_req_sign_verify() { - // Build a request with query params - let req = build_query_request(); - // Build a response - let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); - let mut res: Response = Response::builder().status(200).body(body).unwrap(); - - // Response signature covering @status + @query-param;name="id";req - let covered = ["@status", "\"@query-param\";name=\"id\";req"]; - let covered_components = covered - .iter() - .map(|v| HttpMessageComponentId::try_from(*v)) - .collect::, _>>() - .unwrap(); - - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); - signature_params.set_key_info(&secret_key); - - res - .set_message_signature(&signature_params, &secret_key, None, Some(&req)) - .await - .unwrap(); - - assert!(req.headers().get("signature-input").is_none(), "request should not be modified"); - assert!(res.headers().get("signature-input").is_some(), "signature-input header is missing on response"); - assert!(res.headers().get("signature").is_some(), "signature header is missing on response"); - - let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; - assert!(verification_res.is_ok(), "signature verification failed: {:?}", verification_res.err()); - } - - // ---- RFC 9421: Response must reject request-derived components without `req` ---- - - #[tokio::test] - async fn test_response_rejects_derived_component_without_req() { - let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); - let mut res: Response = Response::builder().status(200).body(body).unwrap(); - - // `@method` without `req` on a response — must fail - let covered = vec![ - HttpMessageComponentId::try_from("@status").unwrap(), - HttpMessageComponentId::try_from("@method").unwrap(), - ]; - - let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&covered).unwrap(); - signature_params.set_key_info(&secret_key); - - let result = res - .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"); - } -} +#[path = "hyper_http_tests.rs"] +mod tests; diff --git a/httpsig-hyper/src/hyper_http_tests.rs b/httpsig-hyper/src/hyper_http_tests.rs new file mode 100644 index 0000000..ebee294 --- /dev/null +++ b/httpsig-hyper/src/hyper_http_tests.rs @@ -0,0 +1,634 @@ +use super::{ + super::{ + error::HyperDigestError, + hyper_content_digest::{RequestContentDigest, ResponseContentDigest}, + ContentDigestType, + }, + *, +}; +use http_body_util::{BodyExt, Full}; +use httpsig::prelude::{AlgorithmName, PublicKey, SecretKey, SharedKey}; + +type BoxBody = http_body_util::combinators::BoxBody; + +const EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIDSHAE++q1BP7T8tk+mJtS+hLf81B0o6CFyWgucDFN/C +-----END PRIVATE KEY----- +"##; +const EDDSA_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= +-----END PUBLIC KEY----- +"##; +// const EDDSA_KEY_ID: &str = "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="; +const COVERED_COMPONENTS_REQ: &[&str] = &["@method", "date", "content-type", "content-digest"]; +const COVERED_COMPONENTS_RES: &[&str] = &["@status", "\"@method\";req", "date", "content-type", "\"content-digest\";req"]; + +async fn build_request() -> Request { + let body = Full::new(&b"{\"hello\": \"world\"}"[..]); + let req = Request::builder() + .method("GET") + .uri("https://example.com/parameters?var=this%20is%20a%20big%0Amultiline%20value&bar=with+plus+whitespace&fa%C3%A7ade%22%3A%20=something") + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .header("content-type", "application/json") + .header("content-type", "application/json-patch+json") + .body(body) + .unwrap(); + req.set_content_digest(&ContentDigestType::Sha256).await.unwrap() +} + +async fn build_response() -> Response { + let body = Full::new(&b"{\"hello\": \"world!!\"}"[..]); + let res = Response::builder() + .status(200) + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .header("content-type", "application/json") + .header("content-type", "application/json-patch+json") + .body(body) + .unwrap(); + res.set_content_digest(&ContentDigestType::Sha256).await.unwrap() +} + +fn build_covered_components_req() -> Vec { + COVERED_COMPONENTS_REQ + .iter() + .map(|&s| HttpMessageComponentId::try_from(s).unwrap()) + .collect() +} + +fn build_covered_components_res() -> Vec { + COVERED_COMPONENTS_RES + .iter() + .map(|&s| HttpMessageComponentId::try_from(s).unwrap()) + .collect() +} + +/// Helper to build a request with query parameters (no content-digest needed) +fn build_query_request() -> Request { + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + Request::builder() + .method("GET") + .uri("https://example.com/path?foo=bar&id=123&x=y") + .header("date", "Sun, 09 May 2021 18:30:00 GMT") + .body(body) + .unwrap() +} + +// ---- Component extraction ---- + +#[tokio::test] +async fn test_extract_component_from_request() { + let req = build_request().await; + let req_or_res = RequestOrResponse::Request(&req); + + let component_id_method = HttpMessageComponentId::try_from("\"@method\"").unwrap(); + let component = extract_http_message_component(&req_or_res, &component_id_method).unwrap(); + assert_eq!(component.to_string(), "\"@method\": GET"); + + let component_id = HttpMessageComponentId::try_from("\"date\"").unwrap(); + let component = extract_http_message_component(&req_or_res, &component_id).unwrap(); + assert_eq!(component.to_string(), "\"date\": Sun, 09 May 2021 18:30:00 GMT"); + + let component_id = HttpMessageComponentId::try_from("content-type").unwrap(); + let component = extract_http_field(&req_or_res, &component_id).unwrap(); + assert_eq!( + component.to_string(), + "\"content-type\": application/json, application/json-patch+json" + ); + + let component_id = HttpMessageComponentId::try_from("content-digest").unwrap(); + let component = extract_http_message_component(&req_or_res, &component_id).unwrap(); + assert_eq!( + component.to_string(), + "\"content-digest\": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:" + ); +} + +#[tokio::test] +async fn test_extract_signature_params_from_request() { + let mut req = build_request().await; + let headers = req.headers_mut(); + headers.insert( + "signature-input", + http::HeaderValue::from_static(r##"sig1=("@method" "@authority")"##), + ); + let component_id = HttpMessageComponentId::try_from("@signature-params").unwrap(); + let req_or_res = RequestOrResponse::Request(&req); + 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.as_component_value(), r##"("@method" "@authority")"##); + assert_eq!(component.value.key(), Some("sig1")); +} + +#[tokio::test] +async fn test_build_signature_base_from_request() { + let req = build_request().await; + + const SIGPARA: &str = r##";created=1704972031;alg="ed25519";keyid="gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is=""##; + let values = (r##""@method" "content-type" "date" "content-digest""##, SIGPARA); + 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(); + assert_eq!( + signature_base.to_string(), + r##""@method": GET +"content-type": application/json, application/json-patch+json +"date": Sun, 09 May 2021 18:30:00 GMT +"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: +"@signature-params": ("@method" "content-type" "date" "content-digest");created=1704972031;alg="ed25519";keyid="gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is=""## + ); +} + +#[tokio::test] +async fn test_extract_tuples_from_request() { + let mut req = build_request().await; + let headers = req.headers_mut(); + headers.insert( + "signature-input", + http::HeaderValue::from_static(r##"sig11=("@method" "@authority");created=1704972031"##), + ); + headers.insert( + "signature", + http::HeaderValue::from_static( + r##"sig11=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"##, + ), + ); + + 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"); + assert_eq!( + tuples.get("sig11").unwrap().signature_params().to_string(), + r##"("@method" "@authority");created=1704972031"## + ); +} + +// ---- Sign and verify ---- + +#[tokio::test] +async fn test_set_verify_message_signature_req() { + let mut req = build_request().await; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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(); + 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")"##)); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature(&public_key, None).await; + assert!(verification_res.is_ok()); +} + +#[tokio::test] +async fn test_set_verify_message_signature_res() { + let req = build_request().await; + let mut res = build_response().await; + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + + let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_res()).unwrap(); + signature_params.set_key_info(&secret_key); + + res + .set_message_signature(&signature_params, &secret_key, None, Some(&req)) + .await + .unwrap(); + let signature_input = res.headers().get("signature-input").unwrap().to_str().unwrap(); + assert!(signature_input.starts_with(r##"sig=("@status" "@method";req "date" "content-type" "content-digest";req)"##)); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; + assert!(verification_res.is_ok()); +} + +#[tokio::test] +async fn test_expired_signature() { + let mut req = build_request().await; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); + signature_params.set_key_info(&secret_key); + let created = signature_params.created.unwrap(); + signature_params.set_expires(created - 1); + assert!(signature_params.is_expired()); + + 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; + assert!(verification_res.is_err()); +} + +#[tokio::test] +async fn test_set_verify_with_signature_name() { + let mut req = build_request().await; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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, Some("custom_sig_name")) + .await + .unwrap(); + + 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 public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature(&public_key, None).await; + assert!(verification_res.is_ok()); +} + +#[tokio::test] +async fn test_set_verify_with_key_id() { + let mut req = build_request().await; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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(); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let key_id = public_key.key_id(); + let verification_res = req.verify_message_signature(&public_key, Some(&key_id)).await; + assert!(verification_res.is_ok()); + + let verification_res = req.verify_message_signature(&public_key, Some("NotFoundKeyId")).await; + assert!(verification_res.is_err()); +} + +const HMACSHA256_SECRET_KEY: &str = + r##"uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ=="##; + +#[tokio::test] +async fn test_set_verify_with_key_id_hmac_sha256() { + let mut req = build_request().await; + let secret_key = SharedKey::from_base64(&AlgorithmName::HmacSha256, HMACSHA256_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); + signature_params.set_key_info(&secret_key); + // Random nonce is highly recommended for HMAC + signature_params.set_random_nonce(); + + 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; + let alg = alg.unwrap(); + let key_id = key_id.unwrap(); + assert_eq!(org_key_id, key_id); + let verification_key = SharedKey::from_base64(&alg, HMACSHA256_SECRET_KEY).unwrap(); + let verification_res = req.verify_message_signature(&verification_key, Some(&key_id)).await; + assert!(verification_res.is_ok()); + + let verification_res = req.verify_message_signature(&verification_key, Some("NotFoundKeyId")).await; + assert!(verification_res.is_err()); +} + +#[tokio::test] +async fn test_get_alg_key_ids() { + let mut req = build_request().await; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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(); + 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); + assert_eq!(key_ids[0].1.as_ref().unwrap(), "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); +} + +const P256_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv7zxW56ojrWwmSo1 +4uOdbVhUfj9Jd+5aZIB9u8gtWnihRANCAARGYsMe0CT6pIypwRvoJlLNs4+cTh2K +L7fUNb5i6WbKxkpAoO+6T3pMBG5Yw7+8NuGTvvtrZAXduA2giPxQ8zCf +-----END PRIVATE KEY----- +"##; +const P256_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERmLDHtAk+qSMqcEb6CZSzbOPnE4d +ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== +-----END PUBLIC KEY----- +"##; +#[tokio::test] +async fn test_set_verify_multiple_signatures() { + let mut req = build_request().await; + + let secret_key_eddsa = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params_eddsa = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); + signature_params_eddsa.set_key_info(&secret_key_eddsa); + + let secret_key_p256 = SecretKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_SECRET_KEY).unwrap(); + 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")), + ]; + + req.set_message_signatures(params_key_name).await.unwrap(); + + let public_key_eddsa = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let public_key_p256 = PublicKey::from_pem(&AlgorithmName::EcdsaP256Sha256, P256_PUBLIC_KEY).unwrap(); + let key_id_eddsa = public_key_eddsa.key_id(); + let key_id_p256 = public_key_p256.key_id(); + + let verification_res = req + .verify_message_signatures(&[ + (&public_key_eddsa, Some(&key_id_eddsa)), + (&public_key_p256, Some(&key_id_p256)), + ]) + .await + .unwrap(); + + assert!(verification_res.len() == 2 && verification_res.iter().all(|r| r.is_ok())); + assert!(verification_res[0].as_ref().unwrap() == "eddsa_sig"); + assert!(verification_res[1].as_ref().unwrap() == "p256_sig"); +} + +// ---- Blocking (sync) ---- + +#[cfg(feature = "blocking")] +#[test] +fn test_blocking_set_verify_message_signature_req() { + let mut req = futures::executor::block_on(build_request()); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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(); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature_sync(&public_key, None); + assert!(verification_res.is_ok()); +} + +#[cfg(feature = "blocking")] +#[test] +fn test_blocking_set_verify_message_signature_res() { + let req = futures::executor::block_on(build_request()); + let mut res = futures::executor::block_on(build_response()); + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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)) + .unwrap(); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = res.verify_message_signature_sync(&public_key, None, Some(&req)); + assert!(verification_res.is_ok()); +} + +// ---- Issue #17: @query-param;name="..." ---- + +/// Regression test for issue #17: @query-param;name="id" must produce signature headers (sync) +#[cfg(feature = "blocking")] +#[test] +fn test_query_param_sign_verify_sync() { + let mut req = build_query_request(); + + let covered = ["@method", "\"@query-param\";name=\"id\"", "date"]; + let covered_components = covered + .iter() + .map(|v| HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + signature_params.set_key_info(&secret_key); + + req + .set_message_signature_sync(&signature_params, &secret_key, Some("qp")) + .unwrap(); + + assert!( + req.headers().get("signature-input").is_some(), + "signature-input header is missing" + ); + assert!(req.headers().get("signature").is_some(), "signature header is missing"); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature_sync(&public_key, None); + assert!( + verification_res.is_ok(), + "signature verification failed: {:?}", + verification_res.err() + ); +} + +/// Regression test for issue #17: @query-param;name="id" must produce signature headers (async) +#[tokio::test] +async fn test_query_param_sign_verify_async() { + let mut req = build_query_request(); + + let covered = ["@method", "\"@query-param\";name=\"id\"", "date"]; + let covered_components = covered + .iter() + .map(|v| HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + signature_params.set_key_info(&secret_key); + + req + .set_message_signature(&signature_params, &secret_key, Some("qp")) + .await + .unwrap(); + + assert!( + req.headers().get("signature-input").is_some(), + "signature-input header is missing" + ); + assert!(req.headers().get("signature").is_some(), "signature header is missing"); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = req.verify_message_signature(&public_key, None).await; + assert!( + verification_res.is_ok(), + "signature verification failed: {:?}", + verification_res.err() + ); +} + +// ---- Derived component parameter validation ---- + +#[test] +fn test_extract_derived_component_rejects_name_on_non_query_param() { + let req = build_query_request(); + let req_or_res = RequestOrResponse::Request(&req); + // `@method;name="foo"` is invalid — `name` is only for `@query-param` + let id = HttpMessageComponentId::try_from("\"@method\";name=\"foo\""); + // component_id parsing itself may reject this; if it doesn't, extraction should + if let Ok(id) = id { + let result = extract_derived_component(&req_or_res, &id); + assert!(result.is_err(), "expected error for `name` on `@method`"); + } +} + +#[test] +fn test_extract_derived_component_rejects_sf_on_derived() { + let req = build_query_request(); + let req_or_res = RequestOrResponse::Request(&req); + // `@method;sf` is invalid — `sf` is only for HTTP field components + let id = HttpMessageComponentId::try_from("\"@method\";sf"); + if let Ok(id) = id { + let result = extract_derived_component(&req_or_res, &id); + assert!(result.is_err(), "expected error for `sf` on derived component"); + } +} + +// ---- Error propagation (Bug #2 regression) ---- + +#[tokio::test] +async fn test_set_message_signature_propagates_build_error() { + // Use `@status` on a request — this is invalid and must return Err, not Ok + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut req: Request = Request::builder() + .method("GET") + .uri("https://example.com/") + .body(body) + .unwrap(); + + let covered = vec![HttpMessageComponentId::try_from("@status").unwrap()]; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered).unwrap(); + signature_params.set_key_info(&secret_key); + + let result = req + .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"); +} + +#[cfg(feature = "blocking")] +#[test] +fn test_set_message_signature_sync_propagates_build_error() { + // Same as above but for sync path + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut req: Request = Request::builder() + .method("GET") + .uri("https://example.com/") + .body(body) + .unwrap(); + + let covered = vec![HttpMessageComponentId::try_from("@status").unwrap()]; + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + 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); + assert!(result.is_err(), "expected Err when using `@status` on request, got Ok"); +} + +// ---- RFC 9421 §2.2: Derived component extraction values ---- + +#[test] +fn test_extract_derived_components_values() { + let req = build_query_request(); + // URI: https://example.com/path?foo=bar&id=123&x=y + let req_or_res = RequestOrResponse::Request(&req); + + // @method (§2.2.1) + let id = HttpMessageComponentId::try_from("@method").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@method\": GET"); + + // @target-uri (§2.2.2) + let id = HttpMessageComponentId::try_from("@target-uri").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@target-uri\": https://example.com/path?foo=bar&id=123&x=y"); + + // @authority (§2.2.3) + let id = HttpMessageComponentId::try_from("@authority").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@authority\": example.com"); + + // @scheme (§2.2.4) + let id = HttpMessageComponentId::try_from("@scheme").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@scheme\": https"); + + // @path (§2.2.6) + let id = HttpMessageComponentId::try_from("@path").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@path\": /path"); + + // @query (§2.2.7) + let id = HttpMessageComponentId::try_from("@query").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@query\": ?foo=bar&id=123&x=y"); + + // @query-param;name="id" (§2.2.8) + let id = HttpMessageComponentId::try_from("\"@query-param\";name=\"id\"").unwrap(); + let c = extract_derived_component(&req_or_res, &id).unwrap(); + assert_eq!(c.to_string(), "\"@query-param\";name=\"id\": 123"); +} + +// ---- RFC 9421 §2.4: @query-param;name="...";req on response ---- + +#[tokio::test] +async fn test_response_with_query_param_req_sign_verify() { + // Build a request with query params + let req = build_query_request(); + // Build a response + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut res: Response = Response::builder().status(200).body(body).unwrap(); + + // Response signature covering @status + @query-param;name="id";req + let covered = ["@status", "\"@query-param\";name=\"id\";req"]; + let covered_components = covered + .iter() + .map(|v| HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + signature_params.set_key_info(&secret_key); + + res + .set_message_signature(&signature_params, &secret_key, None, Some(&req)) + .await + .unwrap(); + + assert!(req.headers().get("signature-input").is_none(), "request should not be modified"); + assert!(res.headers().get("signature-input").is_some(), "signature-input header is missing on response"); + assert!(res.headers().get("signature").is_some(), "signature header is missing on response"); + + let public_key = PublicKey::from_pem(&AlgorithmName::Ed25519, EDDSA_PUBLIC_KEY).unwrap(); + let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; + assert!(verification_res.is_ok(), "signature verification failed: {:?}", verification_res.err()); +} + +// ---- RFC 9421: Response must reject request-derived components without `req` ---- + +#[tokio::test] +async fn test_response_rejects_derived_component_without_req() { + let body = Full::new(bytes::Bytes::new()).map_err(|never| match never {}).boxed(); + let mut res: Response = Response::builder().status(200).body(body).unwrap(); + + // `@method` without `req` on a response — must fail + let covered = vec![ + HttpMessageComponentId::try_from("@status").unwrap(), + HttpMessageComponentId::try_from("@method").unwrap(), + ]; + + let secret_key = SecretKey::from_pem(&AlgorithmName::Ed25519, EDDSA_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered).unwrap(); + signature_params.set_key_info(&secret_key); + + let result = res + .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"); +} From 0a1c6206ebc6d7cb794cbce835bfe387b997e83f Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sun, 15 Feb 2026 14:57:59 +0900 Subject: [PATCH 27/27] bump --- Cargo.toml | 2 +- httpsig-hyper/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 81f2267..bcafee7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.23" +version = "0.0.24" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 0b4a8fd..0c5b0f1 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -19,7 +19,7 @@ rsa-signature = ["httpsig/rsa-signature"] [dependencies] -httpsig = { path = "../httpsig", version = "0.0.23" } +httpsig = { path = "../httpsig", version = "0.0.24" } thiserror = { version = "2.0.18" } tracing = { version = "0.1.44" }