diff --git a/Cargo.toml b/Cargo.toml index fe58eb3..3deed0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "0.0.11" +version = "0.0.12" authors = ["Jun Kurihara"] homepage = "https://github.com/junkurihara/httpsig-rs" repository = "https://github.com/junkurihara/httpsig-rs" diff --git a/httpsig-hyper/Cargo.toml b/httpsig-hyper/Cargo.toml index d3d0268..431f599 100644 --- a/httpsig-hyper/Cargo.toml +++ b/httpsig-hyper/Cargo.toml @@ -13,10 +13,14 @@ 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.11" } +httpsig = { path = "../httpsig", version = "0.0.12" } -thiserror = { version = "1.0.57" } +thiserror = { version = "1.0.58" } tracing = { version = "0.1.40" } +futures = { version = "0.3.30", default-features = false, features = [ + "std", + "async-await", +] } # content digest with rfc8941 structured field values sha2 = { version = "0.10.8", default-features = false } @@ -28,7 +32,7 @@ base64 = { version = "0.22.0" } # for request and response headers http = { version = "1.1.0" } http-body = { version = "1.0.0" } -http-body-util = { version = "0.1.0" } +http-body-util = { version = "0.1.1" } bytes = { version = "1.5.0" } diff --git a/httpsig-hyper/src/hyper_http.rs b/httpsig-hyper/src/hyper_http.rs index 2246696..7b92419 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -25,6 +25,15 @@ pub trait RequestMessageSignature { Self: Sized, T: SigningKey + Sync; + /// Set the http message signatures from given tuples of (http signature params, signing key, name) + fn set_message_signatures( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + ) -> impl Future> + Send + where + Self: Sized, + T: SigningKey + Sync; + /// Verify the http message signature with given verifying key if the request has signature and signature-input headers fn verify_message_signature( &self, @@ -35,6 +44,15 @@ pub trait RequestMessageSignature { Self: Sized, T: VerifyingKey + Sync; + /// Verify multiple signatures at once + fn verify_message_signatures( + &self, + key_and_id: &[(&T, Option<&str>)], + ) -> impl Future>, Self::Error>> + Send + where + Self: Sized, + T: VerifyingKey + Sync; + /// Check if the request has signature and signature-input headers fn has_message_signature(&self) -> bool; @@ -65,17 +83,9 @@ where Self: Sized, T: SigningKey + Sync, { - let signature_base = build_signature_base_from_request(self, signature_params)?; - let signature_headers = signature_base.build_signature_headers(signing_key, signature_name)?; - - self - .headers_mut() - .append("signature-input", signature_headers.signature_input_header_value().parse()?); self - .headers_mut() - .append("signature", signature_headers.signature_header_value().parse()?); - - Ok(()) + .set_message_signatures(&[(&signature_params, signing_key, signature_name)]) + .await } /// Verify the http message signature with given verifying key if the request has signature and signature-input headers @@ -87,39 +97,11 @@ 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 vec_signature_with_base = self.extract_signatures()?; - let filtered = if let Some(key_id) = key_id { - vec_signature_with_base - .iter() - .filter(|(base, _)| base.keyid() == Some(key_id)) - .collect::>() - } else { - vec_signature_with_base.iter().collect() - }; - 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 res = filtered - .iter() - .any(|(base, headers)| base.verify_signature_headers(verifying_key, headers).is_ok()); - - if res { - Ok(()) - } else { - Err(HyperSigError::InvalidSignature( - "Invalid signature for the verifying key".to_string(), - )) - } + self + .verify_message_signatures(&[(verifying_key, key_id)]) + .await? + .pop() + .unwrap() } /// Check if the request has signature and signature-input headers @@ -160,6 +142,82 @@ where .collect::>(); Ok(extracted) } + + async fn set_message_signatures( + &mut self, + params_key_name: &[(&HttpSignatureParams, &T, Option<&str>)], + ) -> Result<(), Self::Error> + where + Self: Sized, + T: SigningKey + Sync, + { + 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) }) + }); + 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> + }) + } + + 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 vec_signature_with_base = self.extract_signatures()?; + + // 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 { + vec_signature_with_base + .iter() + .filter(|(base, _)| base.keyid() == Some(key_id)) + .collect::>() + } else { + vec_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 res_each = filtered + .iter() + .any(|(base, headers)| base.verify_signature_headers(*key, headers).is_ok()); + if res_each { + Ok(()) + } else { + Err(HyperSigError::InvalidSignature( + "Invalid signature for the verifying key".to_string(), + )) + } + } + }); + let res = futures::future::join_all(res_fut).await; + Ok(res) + } } /* --------------------------------------- */ @@ -520,4 +578,50 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= assert_eq!(key_ids.len(), 1); assert_eq!(key_ids[0], "gjrE7ACMxgzYfFHgabgf4kLTg1eKIdsJ94AiFTFj1is="); } + + const P256_SECERT_KEY: &str = r##"-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv7zxW56ojrWwmSo1 +4uOdbVhUfj9Jd+5aZIB9u8gtWnihRANCAARGYsMe0CT6pIypwRvoJlLNs4+cTh2K +L7fUNb5i6WbKxkpAoO+6T3pMBG5Yw7+8NuGTvvtrZAXduA2giPxQ8zCf +-----END PRIVATE KEY----- +"##; + const P256_PUBLIC_KEY: &str = r##"-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERmLDHtAk+qSMqcEb6CZSzbOPnE4d +ii+31DW+YulmysZKQKDvuk96TARuWMO/vDbhk777a2QF3bgNoIj8UPMwnw== +-----END PUBLIC KEY----- +"##; + #[tokio::test] + async fn test_set_verify_multiple_signatures() { + let mut req = build_request().await; + + let secret_key_eddsa = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); + let mut signature_params_eddsa = HttpSignatureParams::try_new(&build_covered_components()).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(); + signature_params_hmac.set_key_info(&secret_key_p256); + + let params_key_name = &[ + (&signature_params_eddsa, &secret_key_eddsa, Some("eddsa_sig")), + (&signature_params_hmac, &secret_key_p256, Some("p256_sig")), + ]; + + req.set_message_signatures(params_key_name).await.unwrap(); + + let public_key_eddsa = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); + let public_key_p256 = PublicKey::from_pem(P256_PUBLIC_KEY).unwrap(); + let key_id_eddsa = public_key_eddsa.key_id(); + let key_id_p256 = public_key_p256.key_id(); + + let verification_res = req + .verify_message_signatures(&[ + (&public_key_eddsa, Some(&key_id_eddsa)), + (&public_key_p256, Some(&key_id_p256)), + ]) + .await + .unwrap(); + + assert!(verification_res.len() == 2 && verification_res.iter().all(|r| r.is_ok())); + } } diff --git a/httpsig/Cargo.toml b/httpsig/Cargo.toml index 678b931..ffd17eb 100644 --- a/httpsig/Cargo.toml +++ b/httpsig/Cargo.toml @@ -13,10 +13,10 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -thiserror = { version = "1.0.57" } +thiserror = { version = "1.0.58" } tracing = { version = "0.1.40" } rustc-hash = { version = "1.1.0" } -indexmap = { version = "2.2.3" } +indexmap = { version = "2.2.5" } fxhash = { version = "0.2.1" } rand = { version = "0.8.5" }