Skip to content

Commit

Permalink
feat: support response signature
Browse files Browse the repository at this point in the history
  • Loading branch information
junkurihara committed Apr 12, 2024
1 parent 268be5a commit bf36509
Show file tree
Hide file tree
Showing 8 changed files with 727 additions and 175 deletions.
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
45 changes: 44 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 @@ -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;
Expand Down Expand Up @@ -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<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
File renamed without changes.
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

0 comments on commit bf36509

Please sign in to comment.