diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a4b824b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..2bc17ab --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2021" +tab_spaces = 2 diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 0c5b0f1..7d1b8f1 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,7 +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", "digest-sha256", "digest-sha512"] +digest-sha256 = [] +digest-sha512 = [] blocking = ["futures/executor"] rsa-signature = ["httpsig/rsa-signature"] @@ -49,3 +51,12 @@ tokio = { version = "1.49.0", default-features = false, features = [ "macros", "rt-multi-thread", ] } # testing only + +[[example]] +name = "hyper-response" +required-features = ["digest-sha256"] + + +[[example]] +name = "hyper-request" +required-features = ["digest-sha256"] diff --git a/httpsig-hyper/src/hyper_content_digest.rs b/httpsig-hyper/src/hyper_content_digest.rs index 51eb75c..4f84c90 100644 --- a/httpsig-hyper/src/hyper_content_digest.rs +++ b/httpsig-hyper/src/hyper_content_digest.rs @@ -44,12 +44,14 @@ pub trait ContentDigest: http_body::Body { /// Returns the digest of the given body in Vec fn derive_digest(body_bytes: &Bytes, cd_type: &ContentDigestType) -> Vec { match cd_type { + #[cfg(feature = "digest-sha256")] ContentDigestType::Sha256 => { let mut hasher = sha2::Sha256::new(); hasher.update(body_bytes); hasher.finalize().to_vec() } + #[cfg(feature = "digest-sha512")] ContentDigestType::Sha512 => { let mut hasher = sha2::Sha512::new(); hasher.update(body_bytes); @@ -75,7 +77,9 @@ pub trait RequestContentDigest { Self: Sized; /// Verify the content digest in the request and returns self if it's valid otherwise returns error - fn verify_content_digest(self) -> impl Future> + Send + fn verify_content_digest( + self, + ) -> impl Future> + Send where Self: Sized; } @@ -94,7 +98,9 @@ pub trait ResponseContentDigest { Self: Sized; /// Verify the content digest in the response and returns self if it's valid otherwise returns error - fn verify_content_digest(self) -> impl Future> + Send + fn verify_content_digest( + self, + ) -> impl Future> + Send where Self: Sized; } @@ -108,7 +114,10 @@ where type PassthroughRequest = Request>; /// Set the content digest in the request - async fn set_content_digest(self, cd_type: &ContentDigestType) -> HyperDigestResult + async fn set_content_digest( + self, + cd_type: &ContentDigestType, + ) -> HyperDigestResult where Self: Sized, { @@ -117,11 +126,14 @@ where .into_bytes_with_digest(cd_type) .await .map_err(|_e| HyperDigestError::HttpBodyError("Failed to generate digest".to_string()))?; - let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); + let new_body = Full::new(body_bytes) + .map_err(|never| match never {}) + .boxed(); - parts - .headers - .insert(CONTENT_DIGEST_HEADER, format!("{cd_type}=:{digest}:").parse().unwrap()); + parts.headers.insert( + CONTENT_DIGEST_HEADER, + format!("{cd_type}=:{digest}:").parse().unwrap(), + ); let new_req = Request::from_parts(parts, new_body); Ok(new_req) @@ -144,7 +156,9 @@ where // 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 new_body = Full::new(body_bytes) + .map_err(|never| match never {}) + .boxed(); let res = Request::from_parts(header, new_body); Ok(res) } else { @@ -163,7 +177,10 @@ where type Error = HyperDigestError; type PassthroughResponse = Response>; - async fn set_content_digest(self, cd_type: &ContentDigestType) -> HyperDigestResult + async fn set_content_digest( + self, + cd_type: &ContentDigestType, + ) -> HyperDigestResult where Self: Sized, { @@ -172,11 +189,14 @@ where .into_bytes_with_digest(cd_type) .await .map_err(|_e| HyperDigestError::HttpBodyError("Failed to generate digest".to_string()))?; - let new_body = Full::new(body_bytes).map_err(|never| match never {}).boxed(); + let new_body = Full::new(body_bytes) + .map_err(|never| match never {}) + .boxed(); - parts - .headers - .insert(CONTENT_DIGEST_HEADER, format!("{cd_type}=:{digest}:").parse().unwrap()); + parts.headers.insert( + CONTENT_DIGEST_HEADER, + format!("{cd_type}=:{digest}:").parse().unwrap(), + ); let new_req = Response::from_parts(parts, new_body); Ok(new_req) @@ -196,7 +216,9 @@ where // 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 new_body = Full::new(body_bytes) + .map_err(|never| match never {}) + .boxed(); let res = Response::from_parts(header, new_body); Ok(res) } else { @@ -217,10 +239,14 @@ fn is_equal_digest(digest1: &[u8], digest2: &[u8]) -> bool { digest1.ct_eq(digest2).into() } -async fn extract_content_digest(header_map: &http::HeaderMap) -> HyperDigestResult<(ContentDigestType, Vec)> { +async fn extract_content_digest( + header_map: &http::HeaderMap, +) -> HyperDigestResult<(ContentDigestType, Vec)> { let content_digest_header = header_map .get(CONTENT_DIGEST_HEADER) - .ok_or(HyperDigestError::NoDigestHeader("No content-digest header".to_string()))? + .ok_or(HyperDigestError::NoDigestHeader( + "No content-digest header".to_string(), + ))? .to_str()?; let indexmap = sfv::Parser::new(content_digest_header) .parse::() @@ -231,8 +257,9 @@ 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.as_str()) - .map_err(|e| HyperDigestError::InvalidHeaderValue(format!("Invalid Content-Digest type: {e}")))?; + 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 { @@ -263,17 +290,30 @@ mod tests { #[tokio::test] async fn content_digest() { let body = Full::new(&b"{\"hello\": \"world\"}"[..]); - let (_body_bytes, digest) = body.into_bytes_with_digest(&ContentDigestType::Sha256).await.unwrap(); - - assert_eq!(digest, "X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="); + #[cfg(feature = "digest-sha256")] + { + let (_body_bytes, digest) = body + .into_bytes_with_digest(&ContentDigestType::Sha256) + .await + .unwrap(); + + assert_eq!(digest, "X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="); + } - let (_body_bytes, digest) = body.into_bytes_with_digest(&ContentDigestType::Sha512).await.unwrap(); - assert_eq!( - digest, - "WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==" - ); + #[cfg(feature = "digest-sha512")] + { + let (_body_bytes, digest) = body + .into_bytes_with_digest(&ContentDigestType::Sha512) + .await + .unwrap(); + assert_eq!( + digest, + "WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==" + ); + } } + #[cfg(feature = "digest-sha256")] #[tokio::test] async fn hyper_request_test() { let body = Full::new(&b"{\"hello\": \"world\"}"[..]); @@ -285,16 +325,28 @@ mod tests { .header("content-type", "application/json") .body(body) .unwrap(); - let req = req.set_content_digest(&ContentDigestType::Sha256).await.unwrap(); + let req = req + .set_content_digest(&ContentDigestType::Sha256) + .await + .unwrap(); assert!(req.headers().contains_key(CONTENT_DIGEST_HEADER)); - let digest = req.headers().get(CONTENT_DIGEST_HEADER).unwrap().to_str().unwrap(); - assert_eq!(digest, format!("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:")); + let digest = req + .headers() + .get(CONTENT_DIGEST_HEADER) + .unwrap() + .to_str() + .unwrap(); + assert_eq!( + digest, + format!("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:") + ); let verified = req.verify_content_digest().await; assert!(verified.is_ok()); } + #[cfg(feature = "digest-sha256")] #[tokio::test] async fn hyper_response_test() { let body = Full::new(&b"{\"hello\": \"world\"}"[..]); @@ -305,16 +357,28 @@ mod tests { .header("content-type", "application/json") .body(body) .unwrap(); - let res = res.set_content_digest(&ContentDigestType::Sha256).await.unwrap(); + let res = res + .set_content_digest(&ContentDigestType::Sha256) + .await + .unwrap(); assert!(res.headers().contains_key(CONTENT_DIGEST_HEADER)); - let digest = res.headers().get(CONTENT_DIGEST_HEADER).unwrap().to_str().unwrap(); - assert_eq!(digest, format!("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:")); + let digest = res + .headers() + .get(CONTENT_DIGEST_HEADER) + .unwrap() + .to_str() + .unwrap(); + assert_eq!( + digest, + format!("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:") + ); let verified = res.verify_content_digest().await; assert!(verified.is_ok()); } + #[cfg(feature = "digest-sha256")] #[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 @@ -327,7 +391,10 @@ mod tests { .body(body) .unwrap(); - let req = req.set_content_digest(&ContentDigestType::Sha256).await.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 @@ -344,6 +411,7 @@ mod tests { } } + #[cfg(feature = "digest-sha256")] #[tokio::test] async fn hyper_response_digest_mismatch_by_header_tamper_should_fail() { // 1) Create a response and set a correct Content-Digest @@ -355,7 +423,10 @@ mod tests { .body(body) .unwrap(); - let res = res.set_content_digest(&ContentDigestType::Sha256).await.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) @@ -363,7 +434,9 @@ mod tests { // Change the first character to another valid base64 character. parts.headers.insert( CONTENT_DIGEST_HEADER, - "sha-256=:Y48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:".parse().unwrap(), + "sha-256=:Y48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:" + .parse() + .unwrap(), ); let tampered_res = Response::from_parts(parts, body); @@ -397,6 +470,7 @@ mod tests { } } + #[cfg(feature = "digest-sha256")] #[tokio::test] async fn hyper_request_digest_length_mismatch_should_fail() { // 1) Create a request and attach a valid Content-Digest header @@ -409,7 +483,10 @@ mod tests { .body(body) .unwrap(); - let req = req.set_content_digest(&ContentDigestType::Sha256).await.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. diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index cea0f08..897da19 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -3,9 +3,11 @@ use http::{HeaderMap, Request, Response}; use http_body::Body; use httpsig::prelude::{ message_component::{ - DerivedComponentName, HttpMessageComponent, HttpMessageComponentId, HttpMessageComponentName, HttpMessageComponentParam, + DerivedComponentName, HttpMessageComponent, HttpMessageComponentId, HttpMessageComponentName, + HttpMessageComponentParam, }, - AlgorithmName, HttpSignatureBase, HttpSignatureHeaders, HttpSignatureHeadersMap, HttpSignatureParams, SigningKey, VerifyingKey, + AlgorithmName, HttpSignatureBase, HttpSignatureHeaders, HttpSignatureHeadersMap, + HttpSignatureParams, SigningKey, VerifyingKey, }; use indexmap::{IndexMap, IndexSet}; use std::{future::Future, str::FromStr}; @@ -24,10 +26,14 @@ 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, Option)>, 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>; + fn get_signature_params( + &self, + ) -> Result, Self::Error>; } /// A trait about http message signature for request @@ -73,7 +79,9 @@ pub trait MessageSignatureReq { T: VerifyingKey + Sync; /// Extract all signature bases contained in the request headers - fn extract_signatures(&self) -> Result, Self::Error>; + fn extract_signatures( + &self, + ) -> Result, Self::Error>; } /// A trait about http message signature for response @@ -162,7 +170,11 @@ pub trait MessageSignatureReqSync: MessageSignatureReq { Self: Sized, T: SigningKey + Sync; - fn verify_message_signature_sync(&self, verifying_key: &T, key_id: Option<&str>) -> Result + fn verify_message_signature_sync( + &self, + verifying_key: &T, + key_id: Option<&str>, + ) -> Result where Self: Sized, T: VerifyingKey + Sync; @@ -243,13 +255,17 @@ where } /// Extract all signature bases contained in the request headers - fn get_alg_key_ids(&self) -> HyperSigResult, Option)>> { + fn get_alg_key_ids( + &self, + ) -> HyperSigResult, Option)>> { let req_or_res = RequestOrResponse::Request(self); get_alg_key_ids_inner(&req_or_res) } /// Extract all signature params used to generate signature bases contained in the request headers - fn get_signature_params(&self) -> Result, Self::Error> { + fn get_signature_params( + &self, + ) -> Result, Self::Error> { let req_or_res = RequestOrResponse::Request(self); get_signature_params_inner(&req_or_res) } @@ -289,7 +305,8 @@ where 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)) + 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( @@ -301,9 +318,10 @@ where .into_iter() .collect::, _>>()?; vec_signature_headers.iter().try_for_each(|headers| { - self - .headers_mut() - .append("signature-input", headers.signature_input_header_value().parse()?); + self.headers_mut().append( + "signature-input", + headers.signature_input_header_value().parse()?, + ); self .headers_mut() .append("signature", headers.signature_header_value().parse()?); @@ -315,7 +333,11 @@ where /// Return Ok(()) if the signature is valid. /// If invalid for the given key or error occurs (like the case where the request does not have signature and/or signature-input headers), return Err. /// If key_id is given, it is used to match the key id in signature params - async fn verify_message_signature(&self, verifying_key: &T, key_id: Option<&str>) -> HyperSigResult + async fn verify_message_signature( + &self, + verifying_key: &T, + key_id: Option<&str>, + ) -> HyperSigResult where Self: Sized, T: VerifyingKey + Sync, @@ -345,7 +367,9 @@ where } /// Extract all signature bases contained in the request headers - fn extract_signatures(&self) -> Result, Self::Error> { + fn extract_signatures( + &self, + ) -> Result, Self::Error> { let req_or_res = RequestOrResponse::Request(self); extract_signatures_inner(&req_or_res, None as Option<&Request<()>>) } @@ -364,13 +388,17 @@ where } /// Extract all key ids for signature bases contained in the response headers - fn get_alg_key_ids(&self) -> Result, Option)>, 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) } /// Extract all signature params used to generate signature bases contained in the response headers - fn get_signature_params(&self) -> Result, Self::Error> { + fn get_signature_params( + &self, + ) -> Result, Self::Error> { let req_or_res = RequestOrResponse::Response(self); get_signature_params_inner(&req_or_res) } @@ -396,7 +424,10 @@ where B: Sync, { self - .set_message_signatures(&[(signature_params, signing_key, signature_name)], req_for_param) + .set_message_signatures( + &[(signature_params, signing_key, signature_name)], + req_for_param, + ) .await } @@ -413,7 +444,9 @@ where 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))) + .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 @@ -425,9 +458,10 @@ where .collect::, _>>()?; vec_signature_headers.iter().try_for_each(|headers| { - self - .headers_mut() - .append("signature-input", headers.signature_input_header_value().parse()?); + self.headers_mut().append( + "signature-input", + headers.signature_input_header_value().parse()?, + ); self .headers_mut() .append("signature", headers.signature_header_value().parse()?); @@ -501,7 +535,11 @@ where Self: Sized, T: SigningKey + Sync, { - futures::executor::block_on(self.set_message_signature(signature_params, signing_key, signature_name)) + futures::executor::block_on(self.set_message_signature( + signature_params, + signing_key, + signature_name, + )) } fn set_message_signatures_sync( @@ -515,7 +553,11 @@ where 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 + fn verify_message_signature_sync( + &self, + verifying_key: &T, + key_id: Option<&str>, + ) -> Result where Self: Sized, T: VerifyingKey + Sync, @@ -552,7 +594,12 @@ where T: SigningKey + Sync, B: Sync, { - futures::executor::block_on(self.set_message_signature(signature_params, signing_key, signature_name, req_for_param)) + futures::executor::block_on(self.set_message_signature( + signature_params, + signing_key, + signature_name, + req_for_param, + )) } fn set_message_signatures_sync( @@ -685,7 +732,12 @@ where // check if any one of the signature headers is valid let successful_sig_names = filtered .iter() - .filter_map(|(&name, (base, headers))| base.verify_signature_headers(*key, headers).ok().map(|_| name.clone())) + .filter_map(|(&name, (base, headers))| { + base + .verify_signature_headers(*key, headers) + .ok() + .map(|_| name.clone()) + }) .collect::>(); if !successful_sig_names.is_empty() { Ok(successful_sig_names.first().unwrap().clone()) @@ -721,7 +773,9 @@ impl RequestOrResponse<'_, B> { fn uri(&self) -> HyperSigResult<&http::Uri> { match self { RequestOrResponse::Request(req) => Ok(req.uri()), - _ => Err(HyperSigError::InvalidComponentName("`uri` is only for request".to_string())), + _ => Err(HyperSigError::InvalidComponentName( + "`uri` is only for request".to_string(), + )), } } @@ -743,7 +797,9 @@ impl RequestOrResponse<'_, B> { } /// Extract signature and signature-input with signature-name indication from http request and response -fn extract_signature_headers_with_name(req_or_res: &RequestOrResponse) -> HyperSigResult { +fn extract_signature_headers_with_name( + req_or_res: &RequestOrResponse, +) -> HyperSigResult { let headers = req_or_res.headers(); if !(headers.contains_key("signature-input") && headers.contains_key("signature")) { return Err(HyperSigError::NoSignatureHeaders( @@ -764,7 +820,8 @@ fn extract_signature_headers_with_name(req_or_res: &RequestOrResponse) -> .collect::, _>>()? .join(", "); - let signature_headers = HttpSignatureHeaders::try_parse(&signature_strings, &signature_input_strings)?; + let signature_headers = + HttpSignatureHeaders::try_parse(&signature_strings, &signature_input_strings)?; Ok(signature_headers) } @@ -781,7 +838,11 @@ fn build_signature_base( .covered_components .iter() .map(|component_id| { - if component_id.params.0.contains(&HttpMessageComponentParam::Req) { + if component_id + .params + .0 + .contains(&HttpMessageComponentParam::Req) + { if matches!(req_or_res, RequestOrResponse::Request(_)) { return Err(HyperSigError::InvalidComponentParam( "`req` is not allowed in request".to_string(), @@ -804,7 +865,10 @@ fn build_signature_base( } /// Extract http field from hyper http request/response -fn extract_http_field(req_or_res: &RequestOrResponse, id: &HttpMessageComponentId) -> HyperSigResult { +fn extract_http_field( + req_or_res: &RequestOrResponse, + id: &HttpMessageComponentId, +) -> HyperSigResult { let HttpMessageComponentName::HttpField(header_name) = &id.name else { return Err(HyperSigError::InvalidComponentName( "invalid http message component name as http field".to_string(), @@ -839,7 +903,11 @@ fn extract_derived_component( // - `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 id.params.0.iter().try_for_each(|param| match param { - HttpMessageComponentParam::Name(_) if matches!(derived_id, DerivedComponentName::QueryParam) => Ok(()), + HttpMessageComponentParam::Name(_) + if matches!(derived_id, DerivedComponentName::QueryParam) => + { + Ok(()) + } HttpMessageComponentParam::Name(_) => Err(HyperSigError::InvalidComponentParam( "`name` parameter is only allowed for `@query-param`".to_string(), )), @@ -889,10 +957,18 @@ fn extract_derived_component( let field_values: Vec = match derived_id { DerivedComponentName::Method => vec![req_or_res.method()?.as_str().to_string()], DerivedComponentName::TargetUri => vec![req_or_res.uri()?.to_string()], - DerivedComponentName::Authority => vec![req_or_res.uri()?.authority().map(|s| s.to_string()).unwrap_or("".to_string())], + DerivedComponentName::Authority => vec![req_or_res + .uri()? + .authority() + .map(|s| s.to_string()) + .unwrap_or("".to_string())], DerivedComponentName::Scheme => vec![req_or_res.uri()?.scheme_str().unwrap_or("").to_string()], DerivedComponentName::RequestTarget => match *req_or_res.method()? { - http::Method::CONNECT => vec![req_or_res.uri()?.authority().map(|s| s.to_string()).unwrap_or("".to_string())], + http::Method::CONNECT => vec![req_or_res + .uri()? + .authority() + .map(|s| s.to_string()) + .unwrap_or("".to_string())], http::Method::OPTIONS => vec!["*".to_string()], _ => vec![req_or_res .uri()? @@ -908,7 +984,11 @@ fn extract_derived_component( p.to_string() } }], - DerivedComponentName::Query => vec![req_or_res.uri()?.query().map(|v| format!("?{v}")).unwrap_or("?".to_string())], + DerivedComponentName::Query => vec![req_or_res + .uri()? + .query() + .map(|v| format!("?{v}")) + .unwrap_or("?".to_string())], DerivedComponentName::QueryParam => { let query = req_or_res.uri()?.query().unwrap_or(""); query @@ -937,11 +1017,13 @@ fn extract_http_message_component( ) -> HyperSigResult { match &target_component_id.name { HttpMessageComponentName::HttpField(_) => extract_http_field(req_or_res, target_component_id), - HttpMessageComponentName::Derived(_) => extract_derived_component(req_or_res, target_component_id), + HttpMessageComponentName::Derived(_) => { + extract_derived_component(req_or_res, target_component_id) + } } } /* --------------------------------------- */ -#[cfg(test)] +#[cfg(all(test, feature = "digest-sha256"))] #[path = "hyper_http_tests.rs"] mod tests; diff --git a/httpsig-hyper/src/lib.rs b/httpsig-hyper/src/lib.rs index 691a392..1f7bcc8 100644 --- a/httpsig-hyper/src/lib.rs +++ b/httpsig-hyper/src/lib.rs @@ -21,49 +21,63 @@ //! will panic. If you are already in an async context, use the async methods directly. mod error; +#[cfg(any(feature = "digest-sha256", feature = "digest-sha512"))] mod hyper_content_digest; mod hyper_http; // hyper's http specific extension to generate and verify http signature /// content-digest header name +#[cfg(any(feature = "digest-sha256", feature = "digest-sha512"))] const CONTENT_DIGEST_HEADER: &str = "content-digest"; /// content-digest header type +#[cfg(any(feature = "digest-sha256", feature = "digest-sha512"))] pub enum ContentDigestType { + #[cfg(feature = "digest-sha256")] Sha256, + #[cfg(feature = "digest-sha512")] Sha512, } +#[cfg(any(feature = "digest-sha256", feature = "digest-sha512"))] impl std::fmt::Display for ContentDigestType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + #[cfg(feature = "digest-sha256")] ContentDigestType::Sha256 => write!(f, "sha-256"), + #[cfg(feature = "digest-sha512")] ContentDigestType::Sha512 => write!(f, "sha-512"), } } } +#[cfg(any(feature = "digest-sha256", feature = "digest-sha512"))] impl std::str::FromStr for ContentDigestType { type Err = error::HyperDigestError; fn from_str(s: &str) -> Result { match s { + #[cfg(feature = "digest-sha256")] "sha-256" => Ok(ContentDigestType::Sha256), + #[cfg(feature = "digest-sha512")] "sha-512" => Ok(ContentDigestType::Sha512), - _ => Err(error::HyperDigestError::InvalidContentDigestType(s.to_string())), + _ => Err(error::HyperDigestError::InvalidContentDigestType( + s.to_string(), + )), } } } pub use error::{HyperDigestError, HyperDigestResult, HyperSigError, HyperSigResult}; pub use httpsig::prelude; +#[cfg(any(feature = "digest-sha256", feature = "digest-sha512"))] pub use hyper_content_digest::{ContentDigest, RequestContentDigest, ResponseContentDigest}; -pub use hyper_http::{ - MessageSignature, MessageSignatureReq, MessageSignatureReqSync, MessageSignatureRes, MessageSignatureResSync, -}; +pub use hyper_http::{MessageSignature, MessageSignatureReq, MessageSignatureRes}; +#[cfg(feature = "blocking")] +pub use hyper_http::{MessageSignatureReqSync, MessageSignatureResSync}; /* ----------------------------------------------------------------- */ -#[cfg(test)] +#[cfg(all(test, feature = "digest-sha256"))] mod tests { use super::{prelude::*, *}; use http::{Request, Response}; @@ -83,7 +97,13 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // 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"]; + 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\"}"[..]); @@ -95,7 +115,10 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= .header("content-type", "application/json-patch+json") .body(body) .unwrap(); - req.set_content_digest(&ContentDigestType::Sha256).await.unwrap() + req + .set_content_digest(&ContentDigestType::Sha256) + .await + .unwrap() } async fn build_response() -> Response { @@ -107,12 +130,16 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= .header("content-type", "application/json-patch+json") .body(body) .unwrap(); - res.set_content_digest(&ContentDigestType::Sha256).await.unwrap() + res + .set_content_digest(&ContentDigestType::Sha256) + .await + .unwrap() } #[test] fn test_content_digest_type() { assert_eq!(ContentDigestType::Sha256.to_string(), "sha-256"); + #[cfg(feature = "digest-sha512")] assert_eq!(ContentDigestType::Sha512.to_string(), "sha-512"); } @@ -139,7 +166,12 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name")) .await .unwrap(); - let signature_input = req.headers().get("signature-input").unwrap().to_str().unwrap(); + let signature_input = req + .headers() + .get("signature-input") + .unwrap() + .to_str() + .unwrap(); let signature = req.headers().get("signature").unwrap().to_str().unwrap(); assert!(signature_input.starts_with(r##"custom_sig_name=("##)); assert!(signature.starts_with(r##"custom_sig_name=:"##)); @@ -153,10 +185,14 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // verify with checking key_id let key_id = public_key.key_id(); - let verification_res = req.verify_message_signature(&public_key, Some(&key_id)).await; + 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; + let verification_res = req + .verify_message_signature(&public_key, Some("NotFoundKeyId")) + .await; assert!(verification_res.is_err()); } @@ -181,10 +217,20 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // set custom signature name, and `req` field param if needed (e.g., request method, uri, content-digest, etc.) included only in response res - .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name"), Some(&req)) + .set_message_signature( + &signature_params, + &secret_key, + Some("custom_sig_name"), + Some(&req), + ) .await .unwrap(); - let signature_input = res.headers().get("signature-input").unwrap().to_str().unwrap(); + let signature_input = res + .headers() + .get("signature-input") + .unwrap() + .to_str() + .unwrap(); let signature = res.headers().get("signature").unwrap().to_str().unwrap(); assert!(signature_input.starts_with(r##"custom_sig_name=("##)); assert!(signature.starts_with(r##"custom_sig_name=:"##)); @@ -193,7 +239,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // 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.unwrap(), EDDSA_PUBLIC_KEY).unwrap(); - let verification_res = res.verify_message_signature(&public_key, None, Some(&req)).await; + let verification_res = res + .verify_message_signature(&public_key, None, Some(&req)) + .await; assert!(verification_res.is_ok()); let verification_res = res .verify_message_signature(&public_key, None, None as Option<&Request<()>>) @@ -202,7 +250,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // verify with checking key_id let key_id = public_key.key_id(); - let verification_res = res.verify_message_signature(&public_key, Some(&key_id), Some(&req)).await; + let verification_res = res + .verify_message_signature(&public_key, Some(&key_id), Some(&req)) + .await; assert!(verification_res.is_ok()); let verification_res = res @@ -227,7 +277,9 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= // set key information, alg and keyid signature_params.set_key_info(&secret_key); // set signature - req.set_message_signature_sync(&signature_params, &secret_key, None).unwrap(); + req + .set_message_signature_sync(&signature_params, &secret_key, None) + .unwrap(); let (alg, _key_id) = req.get_alg_key_ids().unwrap().into_iter().next().unwrap().1; let public_key = PublicKey::from_pem(&alg.unwrap(), EDDSA_PUBLIC_KEY).unwrap();