diff --git a/Cargo.lock b/Cargo.lock index c94dd75..5061ef1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,7 +173,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cas-lib" -version = "0.2.86" +version = "0.2.87" dependencies = [ "aes-gcm", "aes-gcm-siv", diff --git a/Cargo.toml b/Cargo.toml index 7b88825..c66d7cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cas-lib" -version = "0.2.86" +version = "0.2.87" edition = "2021" description = "A function wrapper layer for RustCrypto and Dalek-Cryptography. Intended to be used in FFI situations with a global heap deallactor at the top level project." license = "Apache-2.0" diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index eccaa9e..c50dcc5 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -309,22 +309,29 @@ fn main() { The message module provides HMAC. `sign` produces a tag over a message using a shared key; `verify` recomputes the tag and confirms it matches. A correct -key + message + signature triple verifies as `true`. +key + message + signature triple verifies as `true`. `HMAC` uses SHA2-256 +(32-byte tag) and `HMACSha512` uses SHA2-512 (64-byte tag); both implement +the same `CASHMAC` trait. ```rust -use cas_lib::message::{cas_hmac::CASHMAC, hmac::HMAC}; +use cas_lib::message::{cas_hmac::CASHMAC, hmac::{HMAC, HMACSha512}}; fn main() { // Shared secret key and the message bytes (read a file here in practice). let key: Vec = vec![1, 2, 3, 4, 5]; let message: Vec = std::fs::read("message.txt").unwrap(); - // Produce an authentication tag. + // Produce an authentication tag (HMAC-SHA256). let signature = HMAC::sign(key.clone(), message.clone()).unwrap(); // Verify the tag with the same key and message -> true. - let is_valid = HMAC::verify(key, message, signature).unwrap(); - println!("hmac valid: {}", is_valid); // true + let is_valid = HMAC::verify(key.clone(), message.clone(), signature).unwrap(); + println!("hmac-sha256 valid: {}", is_valid); // true + + // Same API with HMAC-SHA512. + let signature = HMACSha512::sign(key.clone(), message.clone()).unwrap(); + let is_valid = HMACSha512::verify(key, message, signature).unwrap(); + println!("hmac-sha512 valid: {}", is_valid); // true } ``` diff --git a/src/message/hmac.rs b/src/message/hmac.rs index 9148db7..88a7287 100644 --- a/src/message/hmac.rs +++ b/src/message/hmac.rs @@ -2,10 +2,12 @@ use crate::error::{CasError, CasResult}; use super::cas_hmac::CASHMAC; use hmac::{Hmac, Mac}; -use sha2::Sha256; +use sha2::{Sha256, Sha512}; type HmacSha256 = Hmac; +type HmacSha512 = Hmac; pub struct HMAC; +pub struct HMACSha512; impl CASHMAC for HMAC { /// Signs a message using HMAC with SHA-256. @@ -29,3 +31,22 @@ impl CASHMAC for HMAC { } + +impl CASHMAC for HMACSha512 { + /// Signs a message using HMAC with SHA2-512. + /// Returns the signature as a vector of bytes. + fn sign(key: Vec, message: Vec) -> CasResult> { + let mut mac = HmacSha512::new_from_slice(&key).map_err(|_| CasError::InvalidKey)?; + mac.update(&message); + Ok(mac.finalize().into_bytes().to_vec()) + } + + /// Verifies a signature using HMAC with SHA2-512. + /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if it is not, and + /// an error if the key could not be used. + fn verify(key: Vec, message: Vec, signature: Vec) -> CasResult { + let mut mac = HmacSha512::new_from_slice(&key).map_err(|_| CasError::InvalidKey)?; + mac.update(&message); + Ok(mac.verify_slice(&signature).is_ok()) + } +} diff --git a/tests/data/hmac/HMAC-SHA512-RFC4231.txt b/tests/data/hmac/HMAC-SHA512-RFC4231.txt new file mode 100644 index 0000000..d962519 --- /dev/null +++ b/tests/data/hmac/HMAC-SHA512-RFC4231.txt @@ -0,0 +1,55 @@ +# HMAC-SHA-512 test vectors from RFC 4231, Section 4. +# https://www.rfc-editor.org/rfc/rfc4231 +# Mac is the full HMAC-SHA-512 tag truncated to Tlen bytes (Test Case 5 is the +# 128-bit truncation case; all others are full-length). + +[L=64] + +Count = 1 +Klen = 20 +Tlen = 64 +Key = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b +Msg = 4869205468657265 +Mac = 87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854 + +Count = 2 +Klen = 4 +Tlen = 64 +Key = 4a656665 +Msg = 7768617420646f2079612077616e7420666f72206e6f7468696e673f +Mac = 164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737 + +Count = 3 +Klen = 20 +Tlen = 64 +Key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +Msg = dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd +Mac = fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb + +Count = 4 +Klen = 25 +Tlen = 64 +Key = 0102030405060708090a0b0c0d0e0f10111213141516171819 +Msg = cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd +Mac = b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd + +Count = 5 +Klen = 20 +Tlen = 16 +Key = 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c +Msg = 546573742057697468205472756e636174696f6e +Mac = 415fad6271580a531d4179bc891d87a6 + +Count = 6 +Klen = 131 +Tlen = 64 +Key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +Msg = 54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374 +Mac = 80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598 + +Count = 7 +Klen = 131 +Tlen = 64 +Key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +Msg = 5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e +Mac = e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58 diff --git a/tests/message.rs b/tests/message.rs index c9f079e..8f2216d 100644 --- a/tests/message.rs +++ b/tests/message.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod message { - use cas_lib::message::{cas_hmac::CASHMAC, hmac::HMAC}; + use cas_lib::message::{cas_hmac::CASHMAC, hmac::{HMAC, HMACSha512}}; use std::fs; #[test] @@ -21,7 +21,7 @@ mod message { assert!(is_valid); } - /// A single HMACVS known-answer record from the `[L=32]` (SHA-256) block. + /// A single known-answer record from an HMACVS-format vector file. #[derive(Debug)] struct HmacCase { /// Tag length in bytes. `mac` is the full HMAC truncated to this length. @@ -43,12 +43,12 @@ mod message { .collect() } - /// Parses the NIST HMACVS file (`HMACVS.pdf` format), returning only the - /// records under the `[L=32]` / SHA-256 section, which is all the current - /// SHA-256-only `HMAC` implementation can validate. - fn parse_hmac_vectors(path: &str) -> Vec { + /// Parses an HMACVS-format vector file (`HMACVS.pdf` format), returning only + /// the records under the given section header (e.g. `[L=32]` for SHA-256 or + /// `[L=64]` for SHA-512). + fn parse_hmac_vectors(path: &str, section: &str) -> Vec { let contents = fs::read_to_string(path).unwrap(); - let mut in_l32 = false; + let mut in_section = false; let mut klen: Option = None; let mut tlen: Option = None; let mut key: Option> = None; @@ -59,11 +59,11 @@ mod message { let line = line.trim(); if line.starts_with('[') { - in_l32 = line == "[L=32]"; + in_section = line == section; continue; } - if !in_l32 || line.is_empty() || line.starts_with('#') { + if !in_section || line.is_empty() || line.starts_with('#') { continue; } @@ -97,7 +97,7 @@ mod message { /// the (possibly truncated) expected tag, plus a full-length `verify`. #[test] fn test_hmac_sha256_nist_vectors() { - let cases = parse_hmac_vectors("tests/data/hmac/HMAC.txt"); + let cases = parse_hmac_vectors("tests/data/hmac/HMAC.txt", "[L=32]"); assert!(!cases.is_empty(), "no [L=32] vectors were parsed"); for case in &cases { @@ -118,7 +118,7 @@ mod message { /// A tampered tag must not verify. #[test] fn test_hmac_sha256_nist_vectors_negative() { - let cases = parse_hmac_vectors("tests/data/hmac/HMAC.txt"); + let cases = parse_hmac_vectors("tests/data/hmac/HMAC.txt", "[L=32]"); let case = cases .iter() .find(|c| c.tlen == 32) @@ -129,4 +129,58 @@ mod message { assert!(!HMAC::verify(case.key.clone(), case.msg.clone(), tampered).unwrap()); } + + #[test] + pub fn hmac_sha512_sign() { + let key = vec![1, 2, 3, 4, 5]; + let message = vec![6, 7, 8, 9, 10]; + let signature = HMACSha512::sign(key.clone(), message.clone()).unwrap(); + assert!(!signature.is_empty()); + } + + #[test] + pub fn hmac_sha512_verify() { + let key = vec![1, 2, 3, 4, 5]; + let message = vec![6, 7, 8, 9, 10]; + let signature = HMACSha512::sign(key.clone(), message.clone()).unwrap(); + let is_valid = HMACSha512::verify(key, message, signature).unwrap(); + assert!(is_valid); + } + + /// Validates every RFC 4231 HMAC-SHA-512 vector by signing and comparing + /// against the (possibly truncated) expected tag, plus a full-length `verify`. + #[test] + fn test_hmac_sha512_rfc4231_vectors() { + let cases = parse_hmac_vectors("tests/data/hmac/HMAC-SHA512-RFC4231.txt", "[L=64]"); + assert!(!cases.is_empty(), "no [L=64] vectors were parsed"); + + for case in &cases { + let tag = HMACSha512::sign(case.key.clone(), case.msg.clone()).unwrap(); + assert_eq!(tag.len(), 64, "SHA-512 HMAC must produce a 64-byte tag"); + + // `Mac` is the full tag truncated to `Tlen` bytes. + assert_eq!(&tag[..case.tlen], &case.mac[..]); + + // `verify` requires a full-length tag, so it only applies when the + // vector is not truncated. + if case.tlen == 64 { + assert!(HMACSha512::verify(case.key.clone(), case.msg.clone(), case.mac.clone()).unwrap()); + } + } + } + + /// A tampered tag must not verify. + #[test] + fn test_hmac_sha512_rfc4231_vectors_negative() { + let cases = parse_hmac_vectors("tests/data/hmac/HMAC-SHA512-RFC4231.txt", "[L=64]"); + let case = cases + .iter() + .find(|c| c.tlen == 64) + .expect("expected at least one full-length (Tlen=64) vector"); + + let mut tampered = case.mac.clone(); + tampered[0] ^= 0x01; + + assert!(!HMACSha512::verify(case.key.clone(), case.msg.clone(), tampered).unwrap()); + } }