From bf365093cf4e7e0865d2e7b30e4c688594edd41a Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 13 Apr 2024 00:08:38 +0900 Subject: [PATCH] feat: support response signature --- Cargo.toml | 2 +- README.md | 45 +- httpsig-hyper/Cargo.toml | 2 +- httpsig-hyper/README.md | 12 +- .../examples/{hyper.rs => hyper-request.rs} | 0 httpsig-hyper/examples/hyper-response.rs | 217 +++++++ httpsig-hyper/src/hyper_http.rs | 552 ++++++++++++------ httpsig-hyper/src/lib.rs | 72 ++- 8 files changed, 727 insertions(+), 175 deletions(-) rename httpsig-hyper/examples/{hyper.rs => hyper-request.rs} (100%) create mode 100644 httpsig-hyper/examples/hyper-response.rs 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 2a2a2a6..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 @@ -28,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; @@ -81,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 a1a8e20..938c092 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -16,10 +16,23 @@ type SignatureName = String; type KeyId = String; /* --------------------------------------- */ -/// A trait to set the http message signature from given http signature params +/// 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,96 +72,112 @@ pub trait MessageSignature { Self: Sized, T: VerifyingKey + Sync; - /// 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>; - /// Extract all signature bases contained in the request headers fn extract_signatures(&self) -> Result, Self::Error>; } -/* --------------------------------------- */ -impl MessageSignature for Request -where - D: Send + Body + Sync, -{ - type Error = HyperSigError; - +/// 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 - async fn set_message_signature( + fn set_message_signature( &mut self, signature_params: &HttpSignatureParams, signing_key: &T, signature_name: Option<&str>, - ) -> HyperSigResult<()> + req_for_param: Option<&Request>, + ) -> impl Future> + Send where Self: Sized, T: SigningKey + Sync, - { - self - .set_message_signatures(&[(&signature_params, signing_key, signature_name)]) - .await - } + B: Sync; + + /// Set the http message signatures from given tuples of (http signature params, signing key, name) + fn set_message_signatures( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + 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 - /// 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 + 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, - { - self - .verify_message_signatures(&[(verifying_key, key_id)]) - .await? - .pop() - .unwrap() - } + 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, + 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 { - self.headers().contains_key("signature") && self.headers().contains_key("signature-input") + has_message_signature_inner(self.headers()) } /// 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) + 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 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::Request(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)| { - let req_or_res = RequestOrResponse::Request(self); - build_signature_base(&req_or_res, headers.signature_params(), None) - .ok() - .map(|base| (name.clone(), (base, headers.clone()))) - }) - .collect(); - Ok(extracted) +impl MessageSignatureReq for Request +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>, + ) -> HyperSigResult<()> + where + Self: Sized, + T: SigningKey + Sync, + { + self + .set_message_signatures(&[(&signature_params, signing_key, signature_name)]) + .await } async fn set_message_signatures( @@ -159,9 +188,10 @@ 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)| { - let req_or_res = RequestOrResponse::Request(self); - build_signature_base(&req_or_res, params, None).map(|base| async move { base.build_signature_headers(*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 @@ -178,6 +208,22 @@ where }) } + /// 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. + /// 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 + where + Self: Sized, + T: VerifyingKey + Sync, + { + self + .verify_message_signatures(&[(verifying_key, key_id)]) + .await? + .pop() + .unwrap() + } + async fn verify_message_signatures( &self, key_and_id: &[(&T, Option<&str>)], @@ -192,41 +238,13 @@ where )); } let map_signature_with_base = self.extract_signatures()?; + 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::>() - } 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(), - )) - } - } - }); - let res = futures::future::join_all(res_fut).await; - Ok(res) + /// Extract all signature bases contained in the request headers + fn extract_signatures(&self) -> Result, Self::Error> { + let req_or_res = RequestOrResponse::Request(self); + extract_signatures_inner(&req_or_res, None as Option<&Request<()>>) } } @@ -237,91 +255,217 @@ where { 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 req_or_res = RequestOrResponse::Response(self); + get_signature_params_inner(&req_or_res) + } +} + +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( + 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)]) + .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, { - todo!() - // let vec_signature_headers_fut = params_key_name.iter().flat_map(|(params, key, name)| { - // build_signature_base_from_response(self, params).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> - // }) + 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() + .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 + /// 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>) -> Result + 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)]) + .verify_message_signatures(&[(verifying_key, key_id)], req_for_param) .await? .pop() .unwrap() } - async fn verify_message_signatures( + async fn verify_message_signatures( &self, key_and_id: &[(&T, Option<&str>)], + req_for_param: Option<&Request>, ) -> Result>, Self::Error> where Self: Sized, T: VerifyingKey + Sync, { - todo!() + if !self.has_message_signature() { + return Err(HyperSigError::NoSignatureHeaders( + "The response does not have signature and signature-input headers".to_string(), + )); + } + let map_signature_with_base = self.extract_signatures(req_for_param)?; + verify_message_signatures_inner(&map_signature_with_base, key_and_id).await } - fn has_message_signature(&self) -> bool { - todo!() + /// 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) } +} - fn get_key_ids(&self) -> Result, Self::Error> { - todo!() - } +/* --------------------------------------- */ +// inner functions +/// has message signature inner function +fn has_message_signature_inner(headers: &HeaderMap) -> bool { + headers.contains_key("signature") && headers.contains_key("signature-input") +} - fn get_signature_params(&self) -> Result, Self::Error> { - todo!() - } +/// 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) +} - fn extract_signatures(&self) -> Result, Self::Error> { - todo!() - } +/// 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 { + Err(HyperSigError::InvalidSignature( + "Invalid signature for the verifying key".to_string(), + )) + } + } + }); + let res = futures::future::join_all(res_fut).await; + Ok(res) } /* --------------------------------------- */ @@ -355,25 +499,33 @@ impl<'a, B> RequestOrResponse<'a, B> { 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 -fn extract_signature_headers_with_name(req: &Request) -> HyperSigResult { - if !(req.headers().contains_key("signature-input") && req.headers().contains_key("signature")) { +/// 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()) @@ -388,10 +540,10 @@ fn extract_signature_headers_with_name(req: &Request) -> HyperSigResult( - req_or_res: &RequestOrResponse, +fn build_signature_base( + req_or_res: &RequestOrResponse, signature_params: &HttpSignatureParams, - req_for_param: Option<&Request>, + req_for_param: Option<&Request>, ) -> HyperSigResult { let component_lines = signature_params .covered_components @@ -450,12 +602,32 @@ fn extract_derived_component( "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_or_res.method()?.as_str().to_string()], DerivedComponentName::TargetUri => vec![req_or_res.uri()?.to_string()], @@ -487,11 +659,7 @@ fn extract_derived_component( .map(|s| s.to_string()) .collect::>() } - DerivedComponentName::Status => { - return Err(HyperSigError::InvalidComponentName( - "`status` is only for response".to_string(), - )) - } + DerivedComponentName::Status => vec![req_or_res.status()?.as_str().to_string()], DerivedComponentName::SignatureParams => req_or_res .headers() .get_all("signature-input") @@ -520,7 +688,11 @@ fn extract_http_message_component( 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; @@ -537,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\"}"[..]); @@ -552,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() @@ -614,7 +806,7 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= 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).unwrap(); + let signature_base = build_signature_base(&req_or_res, &signature_params, None as Option<&Request<()>>).unwrap(); assert_eq!( signature_base.to_string(), r##""@method": GET @@ -640,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!( @@ -650,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(); @@ -666,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); @@ -687,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 @@ -695,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"); @@ -708,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(); @@ -729,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(); @@ -748,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(); @@ -773,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 38d04d4..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::MessageSignature; +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()); + } }