use regex::Regex;
use sfv::{
BareItem,
Dictionary,
Item,
ListEntry,
Parser,
SerializeValue,
};
use crate::{
base64,
crypto::hashes::{sha256, sha512},
};
const DIGEST_RE: &str = r"^(?P<algorithm>[\w-]+)=(?P<digest>[^,]+)(,|$)";
#[derive(Clone, Copy, Debug, PartialEq)]
enum Algorithm {
Sha256,
Sha512,
}
impl Algorithm {
fn parse(algorithm_key: &str) -> Option<Self> {
let algorithm = match algorithm_key {
"sha-256" => Self::Sha256,
"sha-512" => Self::Sha512,
_ => return None,
};
Some(algorithm)
}
fn to_str(self) -> &'static str {
match self {
Self::Sha256 => "sha-256",
Self::Sha512 => "sha-512",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDigest {
algorithm: Algorithm,
digest: Vec<u8>,
}
impl ContentDigest {
pub fn new(request_body: &[u8]) -> Self {
let digest = sha256(request_body).to_vec();
Self { algorithm: Algorithm::Sha256, digest }
}
pub fn new_sha512(request_body: &[u8]) -> Self {
let digest = sha512(request_body).to_vec();
Self { algorithm: Algorithm::Sha512, digest }
}
}
pub(crate) fn create_digest_header(digest: &ContentDigest) -> String {
let algorithm = digest.algorithm.to_str().to_uppercase();
let digest_b64 = base64::encode(&digest.digest);
format!("{algorithm}={digest_b64}")
}
pub(crate) fn parse_digest_header(
header_value: &str,
) -> Result<ContentDigest, &'static str> {
let digest_re = Regex::new(DIGEST_RE).expect("regexp should be valid");
let caps = digest_re.captures(header_value)
.ok_or("invalid digest header value")?;
let algorithm = Algorithm::parse(&caps["algorithm"].to_lowercase())
.ok_or("unexpected digest algorithm")?;
let digest_b64 = &caps["digest"];
let digest = base64::decode(digest_b64)
.map_err(|_| "invalid digest encoding")?;
Ok(ContentDigest { algorithm, digest })
}
pub(crate) fn create_content_digest_header(
digest: &ContentDigest,
) -> Result<String, &'static str> {
let digest_item = Item::new(BareItem::ByteSequence(digest.digest.clone()));
let mut digest_dict = Dictionary::new();
digest_dict.insert(
sfv::key_ref(digest.algorithm.to_str()).to_owned(),
ListEntry::Item(digest_item),
);
let digest_header = digest_dict.serialize_value()
.map_err(|_| "invalid structured value")?;
Ok(digest_header)
}
pub(crate) fn parse_content_digest_header(
header_value: &str,
) -> Result<ContentDigest, &'static str> {
let dict = Parser::new(header_value.as_bytes())
.parse_dictionary()
.map_err(|_| "invalid content-digest header")?;
let (label, list_item) = dict.first()
.ok_or("invalid content-digest header")?;
let algorithm = Algorithm::parse(label.as_str())
.ok_or("unexpected digest algorithm")?;
let digest = match list_item {
ListEntry::Item(Item { bare_item: BareItem::ByteSequence(value), .. }) => {
value.clone()
},
_ => return Err("invalid digest encoding"),
};
Ok(ContentDigest { algorithm, digest })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_parse_digest_header() {
let request_body = "test*123";
let digest = ContentDigest::new(request_body.as_bytes());
let header_value = create_digest_header(&digest);
let parsed = parse_digest_header(&header_value).unwrap();
assert_eq!(parsed, digest);
}
#[test]
fn test_create_and_parse_digest_header_sha512() {
let request_body = "test*123";
let digest_sha512 = ContentDigest::new_sha512(request_body.as_bytes());
let header_value = create_digest_header(&digest_sha512);
let parsed = parse_digest_header(&header_value).unwrap();
assert_eq!(parsed, digest_sha512);
}
#[test]
fn test_parse_digest_header_multiple_digests() {
let request_body = "test*123";
let digest = ContentDigest::new(request_body.as_bytes());
let digest_b64 = base64::encode(&digest.digest);
let header_value = format!("sha-256={digest_b64},unixsum=30637");
let parsed = parse_digest_header(&header_value).unwrap();
assert_eq!(parsed, digest);
}
#[test]
fn test_create_and_parse_content_digest_header() {
let request_body = "test*123";
let digest = ContentDigest::new(request_body.as_bytes());
let header_value = create_content_digest_header(&digest).unwrap();
let parsed = parse_content_digest_header(&header_value).unwrap();
assert_eq!(parsed, digest);
}
#[test]
fn test_parse_content_digest_header_sample() {
let request_body = r#"{"hello": "world"}"#;
let expected_digest = ContentDigest::new_sha512(request_body.as_bytes());
let header_value = "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:";
let digest = parse_content_digest_header(header_value).unwrap();
assert_eq!(digest, expected_digest);
}
}