Skip to content

Commit

Permalink
add verification function for content-digest in hyper extension
Browse files Browse the repository at this point in the history
  • Loading branch information
junkurihara committed Feb 9, 2024
1 parent 5e931f4 commit 77c2cc0
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 5 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ async fn main() {
assert!(res.is_ok())
// receiver verifies the request with a signature
let verification_res = receiver(request_from_sender).await;
let verified_message = receiver(&request_from_sender).await;
assert!(verification_res.is_ok());
assert!(verification_res.unwrap())
// if needed, content-digest can be verified separately
let verified_cd = request_from_sender.verify_content_digest().await.unwrap();
assert!(verified);
}
```
Expand Down
4 changes: 3 additions & 1 deletion httpsig-hyper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ async-trait = { version = "0.1.77" }
indexmap = { version = "2.2.2" }
fxhash = { version = "0.2.1" }

# content digest
# content digest with rfc8941 structured field values
sha2 = { version = "0.10.8", default-features = false }
sfv = "0.9.4"

# encoding
base64 = { version = "0.21.7" }
Expand All @@ -32,6 +33,7 @@ http-body = { version = "1.0.0" }
http-body-util = { version = "0.1.0" }
bytes = { version = "1.5.0" }


[dev-dependencies]
tokio = { version = "1.36.0", default-features = false, features = [
"macros",
Expand Down
16 changes: 15 additions & 1 deletion httpsig-hyper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,18 @@ You can run a basic example in [./examples](./examples/) as follows.

## Caveats

Note that even if `content-digest` header is specified as one of covered component for signature, the verification process of `httpsig-hyper` doesn't validate the message body. Namely, it only check the consistency between the signature and message components.
Note that even if `content-digest` header is specified as one of covered component for signature, the verification process of `httpsig-hyper` doesn't validate the message body automatically. Namely, it only check the consistency between the signature and message components.

If you need to verify the body of a given message when `content-digest` is covered in `signature-input` header, you need to invoke `verify_content_digest()` function as follows.

```:rust:
// first verifies the signature according to `signature-input` header
let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap();
let signature_verification = req.verify_message_signature(&public_key, None).await.unwrap();
assert!(verification_res);
// if needed, content-digest can be verified separately (only if content-digest header is included in the header)
let verified = request_from_sender.verify_content_digest().await.unwrap();
```

In the context of cryptography, the content-digest of *covered components* in signature-input is verified in the process of signature verification. So, hash value of `content-digest` is verified. To check if the `content-digest` is correctly bound with the message body, we need to run the hashing process separately.
8 changes: 6 additions & 2 deletions httpsig-hyper/examples/hyper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async fn sender() -> Request<Full<bytes::Bytes>> {
}

/// Receiver function that verifies a request with a signature
async fn receiver(req: Request<Full<bytes::Bytes>>) -> bool {
async fn receiver(req: &Request<Full<bytes::Bytes>>) -> bool {
let public_key = PublicKey::from_pem(EDDSA_PUBLIC_KEY).unwrap();
let key_id = public_key.key_id();

Expand All @@ -75,6 +75,10 @@ async fn main() {
assert!(signature.starts_with(r##"custom_sig_name=:"##));

// receiver verifies the request with a signature
let verification_res = receiver(request_from_sender).await;
let verification_res = receiver(&request_from_sender).await;
assert!(verification_res);

// if needed, content-digest can be verified separately
let verified = request_from_sender.verify_content_digest().await.unwrap();
assert!(verified);
}
93 changes: 93 additions & 0 deletions httpsig-hyper/src/hyper_content_digest.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use super::{ContentDigestType, CONTENT_DIGEST_HEADER};
use anyhow::ensure;
use async_trait::async_trait;
use base64::{engine::general_purpose, Engine as _};
use bytes::{Buf, Bytes};
use http::{Request, Response};
use http_body::Body;
use http_body_util::{BodyExt, Full};
use sfv::FromStr;
use sha2::Digest;

// hyper's http specific extension to generate and verify http signature
Expand Down Expand Up @@ -45,6 +47,30 @@ pub trait ContentDigest: http_body::Body {

Ok((body_bytes, general_purpose::STANDARD.encode(digest)))
}

/// Verifies the consistency between self and given content-digest in &[u8]
async fn verify_digest(self, cd_type: &ContentDigestType, _cd: &[u8]) -> std::result::Result<bool, Self::Error>
where
Self: Sized,
Self::Data: Send,
{
let body_bytes = self.into_bytes().await?;
let digest = match cd_type {
ContentDigestType::Sha256 => {
let mut hasher = sha2::Sha256::new();
hasher.update(&body_bytes);
hasher.finalize().to_vec()
}

ContentDigestType::Sha512 => {
let mut hasher = sha2::Sha512::new();
hasher.update(&body_bytes);
hasher.finalize().to_vec()
}
};

Ok(matches!(digest, _cd))
}
}

impl<T: ?Sized> ContentDigest for T where T: http_body::Body {}
Expand All @@ -57,6 +83,9 @@ pub trait RequestContentDigest {
async fn set_content_digest(self, cd_type: &ContentDigestType) -> std::result::Result<Request<Full<Bytes>>, Self::Error>
where
Self: Sized;
async fn verify_content_digest(self) -> std::result::Result<bool, Self::Error>
where
Self: Sized;
}

#[async_trait]
Expand All @@ -66,6 +95,9 @@ pub trait ResponseContentDigest {
async fn set_content_digest(self, cd_type: &ContentDigestType) -> std::result::Result<Response<Full<Bytes>>, Self::Error>
where
Self: Sized;
async fn verify_content_digest(self) -> std::result::Result<bool, Self::Error>
where
Self: Sized;
}

#[async_trait]
Expand Down Expand Up @@ -94,6 +126,19 @@ where
let new_req = Request::from_parts(parts, new_body);
Ok(new_req)
}

async fn verify_content_digest(self) -> std::result::Result<bool, Self::Error>
where
Self: Sized,
{
let header_map = self.headers();
let (cd_type, cd) = extract_content_digest(header_map).await?;
let (_, body) = self.into_parts();
body
.verify_digest(&cd_type, &cd)
.await
.map_err(|_e| anyhow::anyhow!("Failed to verify digest"))
}
}

#[async_trait]
Expand Down Expand Up @@ -122,6 +167,48 @@ where
let new_req = Response::from_parts(parts, new_body);
Ok(new_req)
}
async fn verify_content_digest(self) -> std::result::Result<bool, Self::Error>
where
Self: Sized,
{
let header_map = self.headers();
let (cd_type, cd) = extract_content_digest(header_map).await?;
let (_, body) = self.into_parts();
body
.verify_digest(&cd_type, &cd)
.await
.map_err(|_e| anyhow::anyhow!("Failed to verify digest"))
}
}

async fn extract_content_digest(header_map: &http::HeaderMap) -> anyhow::Result<(ContentDigestType, Vec<u8>)> {
let content_digest_header = header_map
.get(CONTENT_DIGEST_HEADER)
.ok_or(anyhow::anyhow!("Content-Digest header not found"))?
.to_str()?;
let indexmap = sfv::Parser::parse_dictionary(content_digest_header.as_bytes())
.map_err(|e| anyhow::anyhow!("Failed to parse Content-Digest header: {e}"))?;
ensure!(indexmap.len() == 1, "Content-Digest header should have only one value");
let (cd_type, cd) = indexmap.iter().next().unwrap();
let cd_type = ContentDigestType::from_str(cd_type).map_err(|e| anyhow::anyhow!("Invalid Content-Digest type: {e}"))?;
ensure!(
matches!(
cd,
sfv::ListEntry::Item(sfv::Item {
bare_item: sfv::BareItem::ByteSeq(_),
..
})
),
"Invalid Content-Digest value"
);
let cd = match cd {
sfv::ListEntry::Item(sfv::Item {
bare_item: sfv::BareItem::ByteSeq(cd),
..
}) => cd,
_ => unreachable!(),
};
Ok((cd_type, cd.to_owned()))
}

/* --------------------------------------- */
Expand Down Expand Up @@ -159,6 +246,9 @@ mod tests {
assert!(req.headers().contains_key(CONTENT_DIGEST_HEADER));
let digest = req.headers().get(CONTENT_DIGEST_HEADER).unwrap().to_str().unwrap();
assert_eq!(digest, format!("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:"));

let verified = req.verify_content_digest().await.unwrap();
assert!(verified);
}

#[tokio::test]
Expand All @@ -176,5 +266,8 @@ mod tests {
assert!(res.headers().contains_key(CONTENT_DIGEST_HEADER));
let digest = res.headers().get(CONTENT_DIGEST_HEADER).unwrap().to_str().unwrap();
assert_eq!(digest, format!("sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:"));

let verified = res.verify_content_digest().await.unwrap();
assert!(verified);
}
}
11 changes: 11 additions & 0 deletions httpsig-hyper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ impl std::fmt::Display for ContentDigestType {
}
}

impl std::str::FromStr for ContentDigestType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sha-256" => Ok(ContentDigestType::Sha256),
"sha-512" => Ok(ContentDigestType::Sha512),
_ => Err(anyhow::anyhow!("Invalid content-digest type")),
}
}
}

pub use httpsig::prelude;
pub use hyper_content_digest::{ContentDigest, RequestContentDigest};
pub use hyper_http::RequestMessageSignature;
Expand Down

0 comments on commit 77c2cc0

Please sign in to comment.