diff --git a/Cargo.toml b/Cargo.toml index 2ccd140..ff54156 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.14" +version = "0.0.15" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" diff --git a/README.md b/README.md index 3a723a8..92f94e2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Implementation of [IETF RFC 9421](https://datatracker.ietf.org/doc/html/rfc9421) of http message signatures. -This crates provides a basic library [httpsig](./httpsig) and [its extension](./httpsig-hyper/) of `hyper`'s http library. At this point, our library can sign and verify only request messages of hyper. (TODO: response message signature) +This crates provides a basic library [httpsig](./httpsig) and [its extension](./httpsig-hyper/) of [`hyper`](https://github.com/hyperium/hyper)'s http library. At this point, our library can sign and verify request and response messages of only `hyper`. ## Supported Signature Algorithms @@ -19,6 +19,7 @@ This crates provides a basic library [httpsig](./httpsig) and [its extension](./ - [ ] ECDSA-P384 using SHA-384 ~~- [ ] 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/). @@ -27,6 +28,8 @@ At this point, we have no plan to support RSA signature due to [the problem rela This is a case signing and verifying a signature generated with asymmetric cryptography (like EdDSA), where `PUBLIC_KEY_STRING` and `SECRET_KEY_STRING` is a public and private keys in PEM format, respectively. Generating and verifying a MAC through symmetric crypto (HMAC-SHA256) is also supported. +### Signing and Verifying a Request + ```rust use http::Request; use http_body_util::Full; @@ -80,6 +83,47 @@ async fn main() { ``` +### Signing and Verifying a Response + +```rust +use http::{Request, Response}; +use http_body_util::Full; +use httpsig_hyper::{prelude::*, *}; + +type SignatureName = String; + +/// This includes the method of the request corresponding to the request (the second element) +const COVERED_COMPONENTS: &[&str] = &["@status", "\"@method\";req", "date", "content-type", "content-digest"]; + +/// Signer function that generates a response with a signature from response itself and corresponding request +async fn signer(&mut res: Response, corresponding_req: &Request) -> HttpSigResult<()> { + // build signature params that indicates objects to be signed + let covered_components = COVERED_COMPONENTS + .iter() + .map(|v| message_component::HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); + + // set signing/verifying key information, alg and keyid + let secret_key = SecretKey::from_pem(SECRET_KEY_STRING).unwrap(); + signature_params.set_key_info(&secret_key); + + req + .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name"), Some(corresponding_req)) + .await +} + +/// 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(); + let key_id = public_key.key_id(); + + // verify signature with checking key_id + res.verify_message_signature(&public_key, Some(&key_id), Some(sent_req)).await +} +``` + ## Examples See [./httpsig-hyper/examples](./httpsig-hyper/examples/) for detailed examples with `hyper` extension. diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index 05ce8f5..c3bdf27 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.14" } +httpsig = { path = "../httpsig", version = "0.0.15" } thiserror = { version = "1.0.58" } tracing = { version = "0.1.40" } diff --git a/httpsig-hyper/README.md b/httpsig-hyper/README.md index 1649348..2db5ec6 100644 --- a/httpsig-hyper/README.md +++ b/httpsig-hyper/README.md @@ -3,12 +3,20 @@ [![httpsig-hyper](https://img.shields.io/crates/v/httpsig-hyper.svg)](https://crates.io/crates/httpsig-hyper) [![httpsig-hyper](https://docs.rs/httpsig-hyper/badge.svg)](https://docs.rs/httpsig-hyper) -## Example +## Examples You can run a basic example in [./examples](./examples/) as follows. +### Sign and Verify a Request + +```sh +% cargo run --example hyper-request +``` + +### Sign and Verify a Response + ```sh -% cargo run --examples hyper +% cargo run --example hyper-response ``` ## Caveats diff --git a/httpsig-hyper/examples/hyper.rs b/httpsig-hyper/examples/hyper-request.rs similarity index 100% rename from httpsig-hyper/examples/hyper.rs rename to httpsig-hyper/examples/hyper-request.rs diff --git a/httpsig-hyper/examples/hyper-response.rs b/httpsig-hyper/examples/hyper-response.rs new file mode 100644 index 0000000..9b12524 --- /dev/null +++ b/httpsig-hyper/examples/hyper-response.rs @@ -0,0 +1,217 @@ +use http::{Request, Response}; +use http_body_util::Full; +use httpsig_hyper::{prelude::*, *}; + +type BoxBody = http_body_util::combinators::BoxBody; +type SignatureName = String; + +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 HMACSHA256_SECRET_KEY: &str = + r##"uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ=="##; + +const COVERED_COMPONENTS: &[&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() +} + +/// Sender function that generates a request with a signature +async fn sender_ed25519(res: &mut Response, received_req: &Request) { + println!("Signing with ED25519 with key id"); + // build signature params that indicates objects to be signed + let covered_components = COVERED_COMPONENTS + .iter() + .map(|v| message_component::HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + 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(); + signature_params.set_key_info(&secret_key); + + // set signature with custom signature name + res + .set_message_signature(&signature_params, &secret_key, Some("siged25519"), Some(received_req)) + .await + .unwrap(); +} + +/// Sender function that generates a request with a signature +async fn sender_hs256(res: &mut Response, received_req: &Request) { + println!("Signing with HS256 with key id and random nonce"); + // build signature params that indicates objects to be signed + let covered_components = COVERED_COMPONENTS + .iter() + .map(|v| message_component::HttpMessageComponentId::try_from(*v)) + .collect::, _>>() + .unwrap(); + 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(); + signature_params.set_key_info(&shared_key); + signature_params.set_random_nonce(); + + res + .set_message_signature(&signature_params, &shared_key, Some("sighs256"), Some(received_req)) + .await + .unwrap(); +} + +/// Receiver function that verifies a request with a signature of ed25519 +async fn receiver_ed25519(res: &Response, sent_req: &Request) -> HyperSigResult +where + B: http_body::Body + Send + Sync, +{ + println!("Verifying ED25519 signature"); + let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let key_id = public_key.key_id(); + + // verify signature with checking key_id + res.verify_message_signature(&public_key, Some(&key_id), Some(sent_req)).await +} + +/// Receiver function that verifies a request with a signature of hmac-sha256 +async fn receiver_hmac_sha256(res: &Response, sent_req: &Request) -> HyperSigResult +where + B: http_body::Body + Send + Sync, +{ + println!("Verifying HMAC-SHA256 signature"); + let shared_key = SharedKey::from_base64(HMACSHA256_SECRET_KEY).unwrap(); + let key_id = VerifyingKey::key_id(&shared_key); + + // verify signature with checking key_id + res.verify_message_signature(&shared_key, Some(&key_id), Some(sent_req)).await +} + +async fn scenario_multiple_signatures() { + println!("-------------- Scenario: Multiple signatures --------------"); + + let sent_req = build_request().await; + println!("Header of request received:\n{:#?}", sent_req.headers()); + + let mut response_from_sender = build_response().await; + println!("Request header before signing:\n{:#?}", response_from_sender.headers()); + + // sender signs a signature of ed25519 and hmac-sha256 + sender_ed25519(&mut response_from_sender, &sent_req).await; + sender_hs256(&mut response_from_sender, &sent_req).await; + + println!( + "Response header separately signed by ED25519 and HS256:\n{:#?}", + response_from_sender.headers() + ); + + let signature_inputs = response_from_sender + .headers() + .get_all("signature-input") + .iter() + .map(|v| v.to_str()) + .collect::, _>>() + .unwrap(); + let signatures = response_from_sender + .headers() + .get_all("signature") + .iter() + .map(|v| v.to_str()) + .collect::, _>>() + .unwrap(); + assert!(signature_inputs.iter().any(|v| v.starts_with(r##"siged25519=("##))); + assert!(signature_inputs.iter().any(|v| v.starts_with(r##"sighs256=("##))); + assert!(signatures.iter().any(|v| v.starts_with(r##"siged25519=:"##))); + assert!(signatures.iter().any(|v| v.starts_with(r##"sighs256=:"##))); + + // receiver verifies the request with signatures + // every signature is independent and verified separately + let verification_res_ed25519 = receiver_ed25519(&response_from_sender, &sent_req).await; + assert!(verification_res_ed25519.is_ok()); + println!("ED25519 signature is verified"); + let verification_res_hs256 = receiver_hmac_sha256(&response_from_sender, &sent_req).await; + assert!(verification_res_hs256.is_ok()); + println!("HMAC-SHA256 signature is verified"); + + // if needed, content-digest can be verified separately + let verified_request = response_from_sender.verify_content_digest().await; + assert!(verified_request.is_ok()); + println!("Content-Digest header is verified"); +} + +async fn scenario_single_signature_ed25519() { + println!("-------------- Scenario: Single signature with Ed25519 --------------"); + + let sent_req = build_request().await; + println!("Header of request received:\n{:#?}", sent_req.headers()); + + let mut response_from_sender = build_response().await; + println!("Response header before signing:\n{:#?}", response_from_sender.headers()); + + // sender signs a signature of ed25519 + sender_ed25519(&mut response_from_sender, &sent_req).await; + + println!("Response header signed by ED25519:\n{:#?}", response_from_sender.headers()); + + let signature_inputs = response_from_sender + .headers() + .get_all("signature-input") + .iter() + .map(|v| v.to_str()) + .collect::, _>>() + .unwrap(); + let signatures = response_from_sender + .headers() + .get_all("signature") + .iter() + .map(|v| v.to_str()) + .collect::, _>>() + .unwrap(); + assert!(signature_inputs.iter().any(|v| v.starts_with(r##"siged25519=("##))); + assert!(signatures.iter().any(|v| v.starts_with(r##"siged25519=:"##))); + + // receiver verifies the request with signatures + // every signature is independent and verified separately + let verification_res_ed25519 = receiver_ed25519(&response_from_sender, &sent_req).await; + assert!(verification_res_ed25519.is_ok()); + println!("ED25519 signature is verified"); + + // if needed, content-digest can be verified separately + let verified_request = response_from_sender.verify_content_digest().await; + assert!(verified_request.is_ok()); + println!("Content-Digest header is verified"); +} + +#[tokio::main] +async fn main() { + scenario_single_signature_ed25519().await; + println!("-------------------------------------------------------------"); + scenario_multiple_signatures().await; + println!("-------------------------------------------------------------"); +} diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 282d051..938c092 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -1,5 +1,5 @@ use crate::error::{HyperSigError, HyperSigResult}; -use http::Request; +use http::{HeaderMap, Request, Response}; use http_body::Body; use httpsig::prelude::{ message_component::{ @@ -16,10 +16,23 @@ type SignatureName = String; type KeyId = String; /* --------------------------------------- */ -/// A trait to set the http message signature from given http signature params -pub trait RequestMessageSignature { +/// A trait about the http message signature common to both request and response +pub trait MessageSignature { type Error; + /// Check if the request has signature and signature-input headers + fn has_message_signature(&self) -> bool; + + /// Extract all key ids for signature bases contained in the request headers + fn get_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>; +} + +/// A trait about http message signature for request +pub trait MessageSignatureReq { + type Error; /// Set the http message signature from given http signature params and signing key fn set_message_signature( &mut self, @@ -59,20 +72,93 @@ pub trait RequestMessageSignature { Self: Sized, T: VerifyingKey + Sync; - /// Check if the request has signature and signature-input headers - fn has_message_signature(&self) -> bool; + /// Extract all signature bases contained in the request headers + fn extract_signatures(&self) -> Result, Self::Error>; +} - /// Extract all key ids for signature bases contained in the request headers - fn get_key_ids(&self) -> Result, Self::Error>; +/// A trait about http message signature for response +pub trait MessageSignatureRes { + type Error; + /// Set the http message signature from given http signature params and signing key + fn set_message_signature( + &mut self, + signature_params: &HttpSignatureParams, + signing_key: &T, + signature_name: Option<&str>, + req_for_param: Option<&Request>, + ) -> impl Future> + Send + where + Self: Sized, + T: SigningKey + Sync, + B: Sync; - /// Extract all signature params used to generate signature bases contained in the request headers - fn get_signature_params(&self) -> Result, Self::Error>; + /// Set the http message signatures from given tuples of (http signature params, signing key, name) + fn set_message_signatures( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + req_for_param: Option<&Request>, + ) -> impl Future> + Send + where + Self: Sized, + T: SigningKey + Sync, + B: Sync; + + /// Verify the http message signature with given verifying key if the request has signature and signature-input headers + fn verify_message_signature( + &self, + verifying_key: &T, + key_id: Option<&str>, + req_for_param: Option<&Request>, + ) -> impl Future> + Send + where + Self: Sized, + T: VerifyingKey + Sync, + B: Sync; + + /// Verify multiple signatures at once + fn verify_message_signatures( + &self, + key_and_id: &[(&T, Option<&str>)], + req_for_param: Option<&Request>, + ) -> impl Future>, Self::Error>> + Send + where + Self: Sized, + T: VerifyingKey + Sync, + B: Sync; /// Extract all signature bases contained in the request headers - fn extract_signatures(&self) -> Result, Self::Error>; + fn extract_signatures( + &self, + req_for_param: Option<&Request>, + ) -> Result, Self::Error>; +} + +/* --------------------------------------- */ +impl MessageSignature for Request +where + D: Send + Body + Sync, +{ + type Error = HyperSigError; + + /// Check if the request has signature and signature-input headers + fn has_message_signature(&self) -> bool { + has_message_signature_inner(self.headers()) + } + + /// Extract all signature bases contained in the request headers + fn get_key_ids(&self) -> HyperSigResult> { + let req_or_res = RequestOrResponse::Request(self); + get_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> { + let req_or_res = RequestOrResponse::Request(self); + get_signature_params_inner(&req_or_res) + } } -impl RequestMessageSignature for Request +impl MessageSignatureReq for Request where D: Send + Body + Sync, { @@ -94,6 +180,34 @@ where .await } + async fn set_message_signatures( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + ) -> Result<(), Self::Error> + where + Self: Sized, + 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() + .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", headers.signature_header_value().parse()?); + Ok(()) as Result<(), HyperSigError> + }) + } + /// Verify the http message signature with given verifying key if the request has signature and signature-input headers /// 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. @@ -110,60 +224,99 @@ where .unwrap() } - /// Check if the request has signature and signature-input headers - fn has_message_signature(&self) -> bool { - self.headers().contains_key("signature") && self.headers().contains_key("signature-input") + async fn verify_message_signatures( + &self, + key_and_id: &[(&T, Option<&str>)], + ) -> Result>, Self::Error> + where + Self: Sized, + T: VerifyingKey + Sync, + { + if !self.has_message_signature() { + return Err(HyperSigError::NoSignatureHeaders( + "The request does not have signature and signature-input headers".to_string(), + )); + } + let map_signature_with_base = self.extract_signatures()?; + verify_message_signatures_inner(&map_signature_with_base, key_and_id).await } /// Extract all signature bases contained in the request headers - fn get_key_ids(&self) -> HyperSigResult> { - let signature_headers_map = extract_signature_headers_with_name(self)?; - let res = signature_headers_map - .iter() - .filter_map(|(name, headers)| headers.signature_params().keyid.clone().map(|key_id| (name.clone(), key_id))) - .collect(); - Ok(res) + fn extract_signatures(&self) -> Result, Self::Error> { + let req_or_res = RequestOrResponse::Request(self); + extract_signatures_inner(&req_or_res, None as Option<&Request<()>>) } +} - /// Extract all signature params used to generate signature bases contained in the request headers +/* --------------------------------------- */ +impl MessageSignature for Response +where + D: Send + Body + Sync, +{ + type Error = HyperSigError; + + /// Check if the response has signature and signature-input headers + fn has_message_signature(&self) -> bool { + has_message_signature_inner(self.headers()) + } + + /// Extract all key ids for signature bases contained in the response headers + fn get_key_ids(&self) -> Result, Self::Error> { + let req_or_res = RequestOrResponse::Response(self); + get_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> { - let signature_headers_map = extract_signature_headers_with_name(self)?; - let res = signature_headers_map - .iter() - .map(|(name, headers)| (name.clone(), headers.signature_params().clone())) - .collect(); - Ok(res) + let req_or_res = RequestOrResponse::Response(self); + get_signature_params_inner(&req_or_res) } +} - /// Extract all signature bases contained in the request headers - fn extract_signatures(&self) -> Result, Self::Error> { - let signature_headers_map = extract_signature_headers_with_name(self)?; - let extracted = signature_headers_map - .iter() - .filter_map(|(name, headers)| { - build_signature_base_from_request(self, headers.signature_params()) - .ok() - .map(|base| (name.clone(), (base, headers.clone()))) - }) - .collect(); - Ok(extracted) +impl MessageSignatureRes for Response +where + D: Send + Body + Sync, +{ + type Error = HyperSigError; + + /// Set the http message signature from given http signature params and signing key + async fn set_message_signature( + &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, + { + self + .set_message_signatures(&[(&signature_params, signing_key, signature_name)], req_for_param) + .await } - async fn set_message_signatures( + async fn set_message_signatures( &mut self, params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + req_for_param: Option<&Request>, ) -> Result<(), Self::Error> where Self: Sized, T: SigningKey + Sync, { + let req_or_res = RequestOrResponse::Response(self); + let vec_signature_headers_fut = params_key_name.iter().flat_map(|(params, key, name)| { - build_signature_base_from_request(self, params).map(|base| async move { base.build_signature_headers(*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() .collect::, _>>()?; + vec_signature_headers.iter().try_for_each(|headers| { self .headers_mut() @@ -175,9 +328,32 @@ where }) } - async fn verify_message_signatures( + /// Verify the http message signature with given verifying key if the response has signature and signature-input headers + /// 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>, + req_for_param: Option<&Request>, + ) -> Result + where + Self: Sized, + T: VerifyingKey + Sync, + B: Sync, + { + self + .verify_message_signatures(&[(verifying_key, key_id)], req_for_param) + .await? + .pop() + .unwrap() + } + + async fn verify_message_signatures( &self, key_and_id: &[(&T, Option<&str>)], + req_for_param: Option<&Request>, ) -> Result>, Self::Error> where Self: Sized, @@ -185,67 +361,171 @@ where { if !self.has_message_signature() { return Err(HyperSigError::NoSignatureHeaders( - "The request does not have signature and signature-input headers".to_string(), + "The response does not have signature and signature-input headers".to_string(), )); } - let map_signature_with_base = self.extract_signatures()?; + let map_signature_with_base = self.extract_signatures(req_for_param)?; + verify_message_signatures_inner(&map_signature_with_base, key_and_id).await + } - // verify for each key_and_id tuple - let res_fut = key_and_id.iter().map(|(key, key_id)| { - let filtered = if let Some(key_id) = key_id { - map_signature_with_base - .iter() - .filter(|(_, (base, _))| base.keyid() == Some(key_id)) - .collect::>() + /// Extract all signature bases contained in the response headers + fn extract_signatures( + &self, + req_for_param: Option<&Request>, + ) -> Result, Self::Error> { + let req_or_res = RequestOrResponse::Response(self); + extract_signatures_inner(&req_or_res, req_for_param) + } +} + +/* --------------------------------------- */ +// inner functions +/// has message signature inner function +fn has_message_signature_inner(headers: &HeaderMap) -> bool { + headers.contains_key("signature") && headers.contains_key("signature-input") +} + +/// get key ids inner function +fn get_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))) + .collect(); + Ok(res) +} + +/// get signature params inner function +fn get_signature_params_inner( + req_or_res: &RequestOrResponse, +) -> HyperSigResult> { + let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; + let res = signature_headers_map + .iter() + .map(|(name, headers)| (name.clone(), headers.signature_params().clone())) + .collect(); + Ok(res) +} + +/// extract signatures inner function +fn extract_signatures_inner( + req_or_res: &RequestOrResponse, + req_for_param: Option<&Request>, +) -> HyperSigResult> { + let signature_headers_map = extract_signature_headers_with_name(req_or_res)?; + let extracted = signature_headers_map + .iter() + .filter_map(|(name, headers)| { + build_signature_base(req_or_res, headers.signature_params(), req_for_param) + .ok() + .map(|base| (name.clone(), (base, headers.clone()))) + }) + .collect(); + Ok(extracted) +} + +/// Verify multiple signatures inner function +async fn verify_message_signatures_inner( + map_signature_with_base: &IndexMap, + key_and_id: &[(&T, Option<&str>)], +) -> HyperSigResult>> +where + T: VerifyingKey + Sync, +{ + // verify for each key_and_id tuple + let res_fut = key_and_id.iter().map(|(key, key_id)| { + let filtered = if let Some(key_id) = key_id { + map_signature_with_base + .iter() + .filter(|(_, (base, _))| base.keyid() == Some(key_id)) + .collect::>() + } else { + map_signature_with_base.iter().collect() + }; + + // check if any one of the signature headers is valid in async manner + async move { + if filtered.is_empty() { + return Err(HyperSigError::NoSignatureHeaders( + "No signature as appropriate target for verification".to_string(), + )); + } + // 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())) + .collect::>(); + if !successful_sig_names.is_empty() { + Ok(successful_sig_names.first().unwrap().clone()) } else { - map_signature_with_base.iter().collect() - }; - - // check if any one of the signature headers is valid in async manner - async move { - if filtered.is_empty() { - return Err(HyperSigError::NoSignatureHeaders( - "No signature as appropriate target for verification".to_string(), - )); - } - // 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())) - .collect::>(); - if !successful_sig_names.is_empty() { - Ok(successful_sig_names.first().unwrap().clone()) - } else { - Err(HyperSigError::InvalidSignature( - "Invalid signature for the verifying key".to_string(), - )) - } + Err(HyperSigError::InvalidSignature( + "Invalid signature for the verifying key".to_string(), + )) } - }); - let res = futures::future::join_all(res_fut).await; - Ok(res) - } + } + }); + let res = futures::future::join_all(res_fut).await; + Ok(res) } /* --------------------------------------- */ -/// Extract signature and signature-input with signature-name indication from http request -fn extract_signature_headers_with_name(req: &Request) -> HyperSigResult { - if !(req.headers().contains_key("signature-input") && req.headers().contains_key("signature")) { +/// A type to represent either http request or response +enum RequestOrResponse<'a, B> { + Request(&'a Request), + Response(&'a Response), +} + +impl<'a, B> RequestOrResponse<'a, B> { + fn method(&self) -> HyperSigResult<&http::Method> { + match self { + RequestOrResponse::Request(req) => Ok(req.method()), + _ => Err(HyperSigError::InvalidComponentName( + "`method` is only for request".to_string(), + )), + } + } + + fn uri(&self) -> HyperSigResult<&http::Uri> { + match self { + RequestOrResponse::Request(req) => Ok(req.uri()), + _ => Err(HyperSigError::InvalidComponentName("`uri` is only for request".to_string())), + } + } + + fn headers(&self) -> &HeaderMap { + match self { + RequestOrResponse::Request(req) => req.headers(), + RequestOrResponse::Response(res) => res.headers(), + } + } + + fn status(&self) -> HyperSigResult { + match self { + RequestOrResponse::Response(res) => Ok(res.status()), + _ => Err(HyperSigError::InvalidComponentName( + "`status` is only for response".to_string(), + )), + } + } +} + +/// 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 { + let headers = req_or_res.headers(); + if !(headers.contains_key("signature-input") && headers.contains_key("signature")) { return Err(HyperSigError::NoSignatureHeaders( "The request does not have signature and signature-input headers".to_string(), )); }; - let signature_input_strings = req - .headers() + let signature_input_strings = headers .get_all("signature-input") .iter() .map(|v| v.to_str()) .collect::, _>>()? .join(", "); - let signature_strings = req - .headers() + let signature_strings = headers .get_all("signature") .iter() .map(|v| v.to_str()) @@ -256,35 +536,54 @@ fn extract_signature_headers_with_name(req: &Request) -> HyperSigResult( - req: &Request, +/// Build signature base from hyper http request/response and signature params +/// - req_or_res: the hyper http request or response +/// - signature_params: the http signature params +/// - req_for_param: corresponding request to be considered in the signature base in response +fn build_signature_base( + req_or_res: &RequestOrResponse, signature_params: &HttpSignatureParams, + req_for_param: Option<&Request>, ) -> HyperSigResult { let component_lines = signature_params .covered_components .iter() - .map(|component_id| extract_http_message_component_from_request(req, component_id)) + .map(|component_id| { + 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(), + )); + } + if req_for_param.is_none() { + return Err(HyperSigError::InvalidComponentParam( + "`req` is required for the param".to_string(), + )); + } + let req = RequestOrResponse::Request(req_for_param.unwrap()); + extract_http_message_component(&req, component_id) + } else { + extract_http_message_component(req_or_res, component_id) + } + }) .collect::, _>>()?; HttpSignatureBase::try_new(&component_lines, signature_params).map_err(|e| e.into()) } -/// Extract http field from hyper http request -fn extract_http_field_from_request(req: &Request, id: &HttpMessageComponentId) -> HyperSigResult { +/// Extract http field from hyper http request/response +fn extract_http_field(req_or_res: &RequestOrResponse, id: &HttpMessageComponentId) -> HyperSigResult { let HttpMessageComponentName::HttpField(header_name) = &id.name else { return Err(HyperSigError::InvalidComponentName( "invalid http message component name as http field".to_string(), )); }; - if id.params.0.contains(&HttpMessageComponentParam::Req) { - return Err(HyperSigError::InvalidComponentParam( - "`req` is not allowed in request".to_string(), - )); - } + let headers = match req_or_res { + RequestOrResponse::Request(req) => req.headers(), + RequestOrResponse::Response(res) => res.headers(), + }; - let field_values = req - .headers() + let field_values = headers .get_all(header_name) .iter() .map(|v| v.to_str().map(|s| s.to_owned())) @@ -293,9 +592,9 @@ fn extract_http_field_from_request(req: &Request, id: &HttpMessageComponen HttpMessageComponent::try_from((id, field_values.as_slice())).map_err(|e| e.into()) } -/// Extract derived component from hyper http request -fn extract_derived_component_from_request( - req: &Request, +/// Extract derived component from hyper http request/response +fn extract_derived_component( + req_or_res: &RequestOrResponse, id: &HttpMessageComponentId, ) -> HyperSigResult { let HttpMessageComponentName::Derived(derived_id) = &id.name else { @@ -303,45 +602,65 @@ fn extract_derived_component_from_request( "invalid http message component name as derived component".to_string(), )); }; - if !id.params.0.is_empty() { + 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(), )); } + match req_or_res { + RequestOrResponse::Request(_) => { + if matches!(derived_id, DerivedComponentName::Status) { + return Err(HyperSigError::InvalidComponentName( + "`status` is only for response".to_string(), + )); + } + } + RequestOrResponse::Response(_) => { + if !matches!(derived_id, DerivedComponentName::Status) && !matches!(derived_id, DerivedComponentName::SignatureParams) { + return Err(HyperSigError::InvalidComponentName( + "Only `status` and `signature-params` are allowed for response".to_string(), + )); + } + } + } + let field_values: Vec = match derived_id { - DerivedComponentName::Method => vec![req.method().as_str().to_string()], - DerivedComponentName::TargetUri => vec![req.uri().to_string()], - DerivedComponentName::Authority => vec![req.uri().authority().map(|s| s.to_string()).unwrap_or("".to_string())], - DerivedComponentName::Scheme => vec![req.uri().scheme_str().unwrap_or("").to_string()], - DerivedComponentName::RequestTarget => match *req.method() { - http::Method::CONNECT => vec![req.uri().authority().map(|s| s.to_string()).unwrap_or("".to_string())], + 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::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::OPTIONS => vec!["*".to_string()], - _ => vec![req.uri().path_and_query().map(|s| s.to_string()).unwrap_or("".to_string())], + _ => vec![req_or_res + .uri()? + .path_and_query() + .map(|s| s.to_string()) + .unwrap_or("".to_string())], }, DerivedComponentName::Path => vec![{ - let p = req.uri().path(); + let p = req_or_res.uri()?.path(); if p.is_empty() { "/".to_string() } else { p.to_string() } }], - DerivedComponentName::Query => vec![req.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.uri().query().unwrap_or(""); + let query = req_or_res.uri()?.query().unwrap_or(""); query .split('&') .filter(|s| !s.is_empty()) .map(|s| s.to_string()) .collect::>() } - DerivedComponentName::Status => { - return Err(HyperSigError::InvalidComponentName( - "`status` is only for response".to_string(), - )) - } - DerivedComponentName::SignatureParams => req + DerivedComponentName::Status => vec![req_or_res.status()?.as_str().to_string()], + DerivedComponentName::SignatureParams => req_or_res .headers() .get_all("signature-input") .iter() @@ -354,13 +673,13 @@ fn extract_derived_component_from_request( /* --------------------------------------- */ /// Extract http message component from hyper http request -fn extract_http_message_component_from_request( - req: &Request, +fn extract_http_message_component( + req_or_res: &RequestOrResponse, target_component_id: &HttpMessageComponentId, ) -> HyperSigResult { match &target_component_id.name { - HttpMessageComponentName::HttpField(_) => extract_http_field_from_request(req, target_component_id), - HttpMessageComponentName::Derived(_) => extract_derived_component_from_request(req, target_component_id), + HttpMessageComponentName::HttpField(_) => extract_http_field(req_or_res, target_component_id), + HttpMessageComponentName::Derived(_) => extract_derived_component(req_or_res, target_component_id), } } @@ -369,7 +688,11 @@ fn extract_http_message_component_from_request( mod tests { use super::{ - super::{error::HyperDigestError, hyper_content_digest::RequestContentDigest, ContentDigestType}, + super::{ + error::HyperDigestError, + hyper_content_digest::{RequestContentDigest, ResponseContentDigest}, + ContentDigestType, + }, *, }; use http_body_util::Full; @@ -386,7 +709,8 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= -----END PUBLIC KEY----- "##; // const EDDSA_KEY_ID: &str = "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="; - const COVERED_COMPONENTS: &[&str] = &["@method", "date", "content-type", "content-digest"]; + 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\"}"[..]); @@ -401,8 +725,27 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= req.set_content_digest(&ContentDigestType::Sha256).await.unwrap() } - fn build_covered_components() -> Vec { - COVERED_COMPONENTS + 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() @@ -411,24 +754,25 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= #[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_from_request(&req, &component_id_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_from_request(&req, &component_id).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_from_request(&req, &component_id).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_from_request(&req, &component_id).unwrap(); + let component = extract_http_message_component(&req_or_res, &component_id).unwrap(); assert_eq!( component.to_string(), "\"content-digest\": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:" @@ -444,7 +788,8 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= http::HeaderValue::from_static(r##"sig1=("@method" "@authority")"##), ); let component_id = HttpMessageComponentId::try_from("@signature-params").unwrap(); - let component = extract_http_message_component_from_request(&req, &component_id).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")"##); @@ -460,7 +805,8 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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 signature_base = build_signature_base_from_request(&req, &signature_params).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 @@ -486,7 +832,8 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= ), ); - let tuples = extract_signature_headers_with_name(&req).unwrap(); + 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!( @@ -496,10 +843,10 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= } #[tokio::test] - async fn test_set_verify_message_signature() { + 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 mut signature_params = HttpSignatureParams::try_new(&build_covered_components()).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(); @@ -512,11 +859,39 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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(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(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(EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&build_covered_components()).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); @@ -533,7 +908,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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 mut signature_params = HttpSignatureParams::try_new(&build_covered_components()).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&build_covered_components_req()).unwrap(); signature_params.set_key_info(&secret_key); req @@ -541,7 +916,8 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= .await .unwrap(); - let signature_headers_map = extract_signature_headers_with_name(&req).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"); @@ -554,7 +930,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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 mut signature_params = HttpSignatureParams::try_new(&build_covered_components()).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(); @@ -575,7 +951,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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 mut signature_params = HttpSignatureParams::try_new(&build_covered_components()).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(); @@ -594,7 +970,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= async fn test_get_key_ids() { let mut req = build_request().await; let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); - let mut signature_params = HttpSignatureParams::try_new(&build_covered_components()).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(); @@ -619,11 +995,11 @@ ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== let mut req = build_request().await; let secret_key_eddsa = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); - let mut signature_params_eddsa = HttpSignatureParams::try_new(&build_covered_components()).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 mut signature_params_hmac = HttpSignatureParams::try_new(&build_covered_components()).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 = &[ diff --git a/httpsig-hyper/src/lib.rs b/httpsig-hyper/src/lib.rs index 95e9dd0..6fdb012 100644 --- a/httpsig-hyper/src/lib.rs +++ b/httpsig-hyper/src/lib.rs @@ -36,13 +36,13 @@ 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::RequestMessageSignature; +pub use hyper_http::{MessageSignature, MessageSignatureReq, MessageSignatureRes}; /* ----------------------------------------------------------------- */ #[cfg(test)] mod tests { use super::{prelude::*, *}; - use http::Request; + use http::{Request, Response}; use http_body_util::Full; use httpsig::prelude::{PublicKey, SecretKey}; @@ -58,7 +58,8 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= "##; // const EDDSA_KEY_ID: &str = "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is"; - const COVERED_COMPONENTS: &[&str] = &["@method", "date", "content-type", "content-digest"]; + 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\"}"[..]); @@ -73,6 +74,18 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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() + } + #[test] fn test_content_digest_type() { assert_eq!(ContentDigestType::Sha256.to_string(), "sha-256"); @@ -80,14 +93,14 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= } #[tokio::test] - async fn test_set_verify() { + async fn test_set_verify_request() { // show usage of set_message_signature and verify_message_signature let mut req = build_request().await; let secret_key = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); - let covered_components = COVERED_COMPONENTS + let covered_components = COVERED_COMPONENTS_REQ .iter() .map(|v| message_component::HttpMessageComponentId::try_from(*v)) .collect::, _>>() @@ -120,4 +133,53 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= let verification_res = req.verify_message_signature(&public_key, Some("NotFoundKeyId")).await; assert!(verification_res.is_err()); } + + #[tokio::test] + async fn test_set_verify_response() { + // show usage of set_message_signature and verify_message_signature + + let req = build_request().await; + let mut res = build_response().await; + + 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 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)) + .await + .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=:"##)); + + // 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(); + 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<()>>) + .await; + assert!(verification_res.is_err()); + + // 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; + assert!(verification_res.is_ok()); + + let verification_res = res + .verify_message_signature(&public_key, Some("NotFoundKeyId"), Some(&req)) + .await; + assert!(verification_res.is_err()); + } }