Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hyper response #3

Merged
merged 5 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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/).
Expand All @@ -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;
Expand Down Expand Up @@ -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<B>(&mut res: Response<B>, corresponding_req: &Request<B>) -> 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::<Result<Vec<_>, _>>()
.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<B>(res: &Response<B>, sent_req: &Request<B>) -> HttpSigResult<SignatureName> {
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.
2 changes: 1 addition & 1 deletion httpsig-hyper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
12 changes: 10 additions & 2 deletions httpsig-hyper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
217 changes: 217 additions & 0 deletions httpsig-hyper/examples/hyper-response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
use http::{Request, Response};
use http_body_util::Full;
use httpsig_hyper::{prelude::*, *};

type BoxBody = http_body_util::combinators::BoxBody<bytes::Bytes, HyperDigestError>;
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<BoxBody> {
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<BoxBody> {
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<BoxBody>, received_req: &Request<BoxBody>) {
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::<Result<Vec<_>, _>>()
.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<BoxBody>, received_req: &Request<BoxBody>) {
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::<Result<Vec<_>, _>>()
.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<B>(res: &Response<B>, sent_req: &Request<BoxBody>) -> HyperSigResult<SignatureName>
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<B>(res: &Response<B>, sent_req: &Request<BoxBody>) -> HyperSigResult<SignatureName>
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::<Result<Vec<_>, _>>()
.unwrap();
let signatures = response_from_sender
.headers()
.get_all("signature")
.iter()
.map(|v| v.to_str())
.collect::<Result<Vec<_>, _>>()
.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::<Result<Vec<_>, _>>()
.unwrap();
let signatures = response_from_sender
.headers()
.get_all("signature")
.iter()
.map(|v| v.to_str())
.collect::<Result<Vec<_>, _>>()
.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!("-------------------------------------------------------------");
}
Loading