diff --git a/httpsig-hyper/examples/hyper.rs b/httpsig-hyper/examples/hyper.rs index 00608cc..709b3e8 100644 --- a/httpsig-hyper/examples/hyper.rs +++ b/httpsig-hyper/examples/hyper.rs @@ -12,6 +12,8 @@ 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] = &["@method", "date", "content-type", "content-digest"]; @@ -29,7 +31,8 @@ async fn build_request() -> anyhow::Result> { } /// Sender function that generates a request with a signature -async fn sender() -> Request { +async fn sender_ed25519(req: &mut Request) { + println!("Signing with ED25519 with key id"); // build signature params that indicates objects to be signed let covered_components = COVERED_COMPONENTS .iter() @@ -38,25 +41,45 @@ async fn sender() -> Request { .unwrap(); let mut signature_params = HttpSignatureParams::try_new(&covered_components).unwrap(); - // set signing/verifying key information, alg and keyid + // 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 - let mut req = build_request().await.unwrap(); req - .set_message_signature(&signature_params, &secret_key, Some("custom_sig_name")) + .set_message_signature(&signature_params, &secret_key, Some("siged25519")) .await .unwrap(); +} + +/// Sender function that generates a request with a signature +async fn sender_hs256(req: &mut 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(); req + .set_message_signature(&signature_params, &shared_key, Some("sighs256")) + .await + .unwrap(); } -/// Receiver function that verifies a request with a signature -async fn receiver(req: &Request) -> Result<(), anyhow::Error> +/// Receiver function that verifies a request with a signature of ed25519 +async fn receiver_ed25519(req: &Request) -> Result<(), anyhow::Error> 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(); @@ -64,26 +87,112 @@ where req.verify_message_signature(&public_key, Some(&key_id)).await } -#[tokio::main] -async fn main() { - // sender generates a request with a signature - let request_from_sender = sender().await; +/// Receiver function that verifies a request with a signature of hmac-sha256 +async fn receiver_hmac_sha256(req: &Request) -> Result<(), anyhow::Error> +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 + req.verify_message_signature(&shared_key, Some(&key_id)).await +} + +async fn scenario_multiple_signatures() { + println!("-------------- Scenario: Multiple signatures --------------"); + + let mut request_from_sender = build_request().await.unwrap(); + println!("Request header before signing:\n{:#?}", request_from_sender.headers()); + + // sender signs a signature of ed25519 and hmac-sha256 + sender_ed25519(&mut request_from_sender).await; + sender_hs256(&mut request_from_sender).await; + + println!( + "Request header separately signed by ED25519 and HS256:\n{:#?}", + request_from_sender.headers() + ); + + let signature_inputs = request_from_sender + .headers() + .get_all("signature-input") + .iter() + .map(|v| v.to_str()) + .collect::, _>>() + .unwrap(); + let signatures = request_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(&request_from_sender).await; + assert!(verification_res_ed25519.is_ok()); + println!("ED25519 signature is verified"); + let verification_res_hs256 = receiver_hmac_sha256(&request_from_sender).await; + assert!(verification_res_hs256.is_ok()); + println!("HMAC-SHA256 signature is verified"); + + // if needed, content-digest can be verified separately + let verified_request = request_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 signature_input = request_from_sender + let mut request_from_sender = build_request().await.unwrap(); + println!("Request header before signing:\n{:#?}", request_from_sender.headers()); + + // sender signs a signature of ed25519 + sender_ed25519(&mut request_from_sender).await; + + println!("Request header signed by ED25519:\n{:#?}", request_from_sender.headers()); + + let signature_inputs = request_from_sender .headers() - .get("signature-input") - .unwrap() - .to_str() + .get_all("signature-input") + .iter() + .map(|v| v.to_str()) + .collect::, _>>() .unwrap(); - let signature = request_from_sender.headers().get("signature").unwrap().to_str().unwrap(); - assert!(signature_input.starts_with(r##"custom_sig_name=("##)); - assert!(signature.starts_with(r##"custom_sig_name=:"##)); + let signatures = request_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 a signature - let verification_res = receiver(&request_from_sender).await; - assert!(verification_res.is_ok()); + // receiver verifies the request with signatures + // every signature is independent and verified separately + let verification_res_ed25519 = receiver_ed25519(&request_from_sender).await; + assert!(verification_res_ed25519.is_ok()); + println!("ED25519 signature is verified"); // if needed, content-digest can be verified separately let verified_request = request_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 f78b784..71d20ab 100644 --- a/httpsig-hyper/src/hyper_http.rs +++ b/httpsig-hyper/src/hyper_http.rs @@ -257,7 +257,7 @@ mod tests { *, }; use http_body_util::Full; - use httpsig::prelude::{PublicKey, SecretKey}; + use httpsig::prelude::{PublicKey, SecretKey, SharedKey}; type BoxBody = http_body_util::combinators::BoxBody; @@ -434,4 +434,26 @@ MCowBQYDK2VwAyEA1ixMQcxO46PLlgQfYS46ivFd+n0CcDHSKUnuhm3i1O0= let verification_res = req.verify_message_signature(&public_key, Some("NotFoundKeyId")).await; assert!(verification_res.is_err()); } + + const HMACSHA256_SECRET_KEY: &str = + r##"uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ=="##; + + #[tokio::test] + async fn test_set_verify_with_key_id_hmac_sha256() { + let mut req = build_request().await.unwrap(); + let secret_key = SharedKey::from_base64(HMACSHA256_SECRET_KEY).unwrap(); + let mut signature_params = HttpSignatureParams::try_new(&build_covered_components()).unwrap(); + signature_params.set_key_info(&secret_key); + // Random nonce is highly recommended for HMAC + signature_params.set_random_nonce(); + + req.set_message_signature(&signature_params, &secret_key, None).await.unwrap(); + + let key_id = VerifyingKey::key_id(&secret_key); + let verification_res = req.verify_message_signature(&secret_key, Some(&key_id)).await; + assert!(verification_res.is_ok()); + + let verification_res = req.verify_message_signature(&secret_key, Some("NotFoundKeyId")).await; + assert!(verification_res.is_err()); + } } diff --git a/httpsig/src/crypto/symmetric.rs b/httpsig/src/crypto/symmetric.rs index e540ac0..cfe6690 100644 --- a/httpsig/src/crypto/symmetric.rs +++ b/httpsig/src/crypto/symmetric.rs @@ -1,5 +1,6 @@ use super::AlgorithmName; use anyhow::Result; +use base64::{engine::general_purpose, Engine as _}; use hmac::{Hmac, Mac}; use sha2::{Digest, Sha256}; @@ -13,6 +14,14 @@ pub enum SharedKey { HmacSha256(Vec), } +impl SharedKey { + /// Create a new shared key from base64 encoded string + pub fn from_base64(key: &str) -> Result { + let key = general_purpose::STANDARD.decode(key)?; + Ok(SharedKey::HmacSha256(key)) + } +} + impl super::SigningKey for SharedKey { /// Sign the data fn sign(&self, data: &[u8]) -> Result> { @@ -49,7 +58,6 @@ impl super::VerifyingKey for SharedKey { /// Get the key id fn key_id(&self) -> String { - use base64::{engine::general_purpose, Engine as _}; match self { SharedKey::HmacSha256(key) => { let mut hasher = ::new(); diff --git a/httpsig/src/lib.rs b/httpsig/src/lib.rs index fce6d83..578a8f1 100644 --- a/httpsig/src/lib.rs +++ b/httpsig/src/lib.rs @@ -22,9 +22,10 @@ pub mod prelude { /* ----------------------------------------------------------------- */ #[cfg(test)] mod tests { - use super::prelude::*; use base64::{engine::general_purpose, Engine as _}; + + /* ----------------------------------------------------------------- */ // params from https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#name-signing-a-request-using-ed2 const EDDSA_SECRET_KEY: &str = r##"-----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF @@ -34,39 +35,65 @@ MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs= -----END PUBLIC KEY----- "##; - const SIGNATURE_BASE: &str = r##""date": Tue, 20 Apr 2021 02:07:55 GMT + const EDDSA_SIGNATURE_BASE: &str = r##""date": Tue, 20 Apr 2021 02:07:55 GMT "@method": POST "@path": /foo "@authority": example.com "content-type": application/json "content-length": 18 "@signature-params": ("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519""##; - const SIGNATURE_VALUE: &str = "wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw=="; - const _SIGNATURE_RESULT: &str = r##"Signature-Input: sig-b26=("date" "@method" "@path" "@authority" \ - "content-type" "content-length");created=1618884473\ - ;keyid="test-key-ed25519" -Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1\ - u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"##; + const EDDSA_SIGNATURE_VALUE: &str = "wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw=="; + const _EDDSA_SIGNATURE_RESULT: &str = r##"Signature-Input: sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519" +Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:"##; #[test] - fn test_using_test_vector() { + fn test_using_test_vector_ed25519() { let sk = SecretKey::from_pem(EDDSA_SECRET_KEY).unwrap(); let pk = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap(); assert_eq!(pk.key_id(), sk.public_key().key_id()); - let data = SIGNATURE_BASE.as_bytes(); - let binary_signature = general_purpose::STANDARD.decode(SIGNATURE_VALUE).unwrap(); + let data = EDDSA_SIGNATURE_BASE.as_bytes(); + let binary_signature = general_purpose::STANDARD.decode(EDDSA_SIGNATURE_VALUE).unwrap(); let verification_result = pk.verify(data, &binary_signature); assert!(verification_result.is_ok()); - let signature = sk.sign(SIGNATURE_BASE.as_bytes()).unwrap(); + let signature = sk.sign(EDDSA_SIGNATURE_BASE.as_bytes()).unwrap(); let signature_value = general_purpose::STANDARD.encode(signature); // println!("{}", signature_value); let signature_bytes = general_purpose::STANDARD.decode(signature_value).unwrap(); - let verification_result = pk.verify(SIGNATURE_BASE.as_bytes(), &signature_bytes); + let verification_result = pk.verify(EDDSA_SIGNATURE_BASE.as_bytes(), &signature_bytes); + assert!(verification_result.is_ok()); + } + + /* ----------------------------------------------------------------- */ + // params from https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#name-signing-a-request-using-hma + const HMACSHA256_SECRET_KEY: &str = + r##"uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ=="##; + const HMACSHA256_SIGNATURE_BASE: &str = r##""date": Tue, 20 Apr 2021 02:07:55 GMT +"@authority": example.com +"content-type": application/json +"@signature-params": ("date" "@authority" "content-type");created=1618884473;keyid="test-shared-secret""##; + const HMACSHA256_SIGNATURE_VALUE: &str = r##"pxcQw6G3AjtMBQjwo8XzkZf/bws5LelbaMk5rGIGtE8="##; + + #[test] + fn test_using_test_vector_hmac_sha256() { + let sk = SharedKey::from_base64(HMACSHA256_SECRET_KEY).unwrap(); + + let data = HMACSHA256_SIGNATURE_BASE.as_bytes(); + let binary_signature = general_purpose::STANDARD.decode(HMACSHA256_SIGNATURE_VALUE).unwrap(); + let verification_result = sk.verify(data, &binary_signature); + assert!(verification_result.is_ok()); + + let signature = sk.sign(HMACSHA256_SIGNATURE_BASE.as_bytes()).unwrap(); + let signature_value = general_purpose::STANDARD.encode(signature); + assert_eq!(signature_value, HMACSHA256_SIGNATURE_VALUE.to_string()); + + let signature_bytes = general_purpose::STANDARD.decode(signature_value).unwrap(); + let verification_result = sk.verify(HMACSHA256_SIGNATURE_BASE.as_bytes(), &signature_bytes); assert!(verification_result.is_ok()); } + /* ----------------------------------------------------------------- */ const COMPONENT_LINES: &[&str] = &[ r##""date": Tue, 20 Apr 2021 02:07:55 GMT"##, r##""@method": POST"##,