use hmac::{Hmac, KeyInit, Mac};
use sha2::{Sha256, Sha384, Sha512};
use crate::error::{Error, Result};
use crate::jwa::SigningAlg;
use crate::jwk::{Jwk, KeyMaterial};
const SHORT_KEY: &str = "HMAC key is shorter than the hash output";
pub(crate) fn sign(alg: &SigningAlg, key: &Jwk, signing_input: &[u8]) -> Result<Vec<u8>> {
let k = oct_key(key)?;
match alg {
SigningAlg::HS256 => tag::<Hmac<Sha256>>(k, signing_input),
SigningAlg::HS384 => tag::<Hmac<Sha384>>(k, signing_input),
SigningAlg::HS512 => tag::<Hmac<Sha512>>(k, signing_input),
_ => Err(Error::KeyAlgorithmMismatch),
}
}
pub(crate) fn verify(
alg: &SigningAlg,
key: &Jwk,
signing_input: &[u8],
signature: &[u8],
) -> Result<()> {
let k = oct_key(key)?;
match alg {
SigningAlg::HS256 => check::<Hmac<Sha256>>(k, signing_input, signature),
SigningAlg::HS384 => check::<Hmac<Sha384>>(k, signing_input, signature),
SigningAlg::HS512 => check::<Hmac<Sha512>>(k, signing_input, signature),
_ => Err(Error::KeyAlgorithmMismatch),
}
}
fn oct_key(key: &Jwk) -> Result<&[u8]> {
match key.material() {
KeyMaterial::Oct(oct) => Ok(oct.k()),
_ => Err(Error::KeyAlgorithmMismatch),
}
}
fn keyed<M: Mac + KeyInit>(k: &[u8], signing_input: &[u8]) -> Result<M> {
if k.len() < M::output_size() {
return Err(Error::InvalidKey(SHORT_KEY));
}
let mut mac = <M as KeyInit>::new_from_slice(k).map_err(|_| Error::InvalidKey(SHORT_KEY))?;
Mac::update(&mut mac, signing_input);
Ok(mac)
}
fn tag<M: Mac + KeyInit>(k: &[u8], signing_input: &[u8]) -> Result<Vec<u8>> {
Ok(keyed::<M>(k, signing_input)?
.finalize()
.into_bytes()
.to_vec())
}
fn check<M: Mac + KeyInit>(k: &[u8], signing_input: &[u8], signature: &[u8]) -> Result<()> {
keyed::<M>(k, signing_input)?
.verify_slice(signature)
.map_err(|_| Error::InvalidSignature)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::wycheproof::{
Deviation, Expected, Outcome, Summary, assert_conforms, load,
};
use crate::util::{b64url_decode, b64url_encode};
const A1_PROTECTED: &str = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9";
const A1_PAYLOAD: &str = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ";
const A1_KEY: &str = r#"{"kty":"oct","k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"}"#;
const A1_SIGNATURE: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
fn jwk(json: &str) -> Jwk {
Jwk::from_json(json.as_bytes()).unwrap()
}
fn oct(k: &[u8]) -> Jwk {
jwk(&format!(r#"{{"kty":"oct","k":"{}"}}"#, b64url_encode(k)))
}
fn a1_signing_input() -> Vec<u8> {
format!("{A1_PROTECTED}.{A1_PAYLOAD}").into_bytes()
}
fn a1_signature() -> Vec<u8> {
b64url_decode(A1_SIGNATURE).unwrap()
}
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
#[test]
fn rfc7515_a1_signing_input_is_the_crlf_header() {
assert_eq!(
b64url_decode(A1_PROTECTED).unwrap(),
b"{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}"
);
let input = a1_signing_input();
assert_eq!(
input[..20],
[
101, 121, 74, 48, 101, 88, 65, 105, 79, 105, 74, 75, 86, 49, 81, 105, 76, 65, 48,
75
]
);
}
#[test]
fn rfc7515_a1_hs256_signs_to_the_published_signature() {
let sig = sign(&SigningAlg::HS256, &jwk(A1_KEY), &a1_signing_input()).unwrap();
assert_eq!(b64url_encode(&sig), A1_SIGNATURE);
}
#[test]
fn rfc7515_a1_hs256_verifies() {
assert_eq!(
verify(
&SigningAlg::HS256,
&jwk(A1_KEY),
&a1_signing_input(),
&a1_signature()
),
Ok(())
);
}
const RFC4231_TC6_DATA: &[u8] = b"Test Using Larger Than Block-Size Key - Hash Key First";
fn rfc4231_tc6() -> [(SigningAlg, Vec<u8>); 3] {
[
(
SigningAlg::HS256,
hex("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"),
),
(
SigningAlg::HS384,
hex(concat!(
"4ece084485813e9088d2c63a041bc5b44f9ef1012a2b588f",
"3cd11f05033ac4c60c2ef6ab4030fe8296248df163f44952"
)),
),
(
SigningAlg::HS512,
hex(concat!(
"80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f352",
"6b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598"
)),
),
]
}
#[test]
fn rfc4231_tc6_signs_and_verifies_for_every_hs_alg() {
let key = oct(&[0xaa; 131]);
for (alg, tag) in rfc4231_tc6() {
assert_eq!(sign(&alg, &key, RFC4231_TC6_DATA).unwrap(), tag, "{alg}");
assert_eq!(verify(&alg, &key, RFC4231_TC6_DATA, &tag), Ok(()), "{alg}");
}
}
#[test]
fn a_tag_of_the_wrong_length_is_an_invalid_signature() {
let key = jwk(A1_KEY);
let input = a1_signing_input();
let sig = a1_signature();
let mut long = sig.clone();
long.push(0);
for bad in [&sig[..sig.len() - 1], &long[..], &sig[..16], &[][..]] {
assert_eq!(
verify(&SigningAlg::HS256, &key, &input, bad),
Err(Error::InvalidSignature),
"{} octets",
bad.len()
);
}
}
#[test]
fn a_flipped_bit_anywhere_is_an_invalid_signature() {
let key = jwk(A1_KEY);
let input = a1_signing_input();
for at in 0..32 {
for bit in 0..8 {
let mut sig = a1_signature();
sig[at] ^= 1 << bit;
assert_eq!(
verify(&SigningAlg::HS256, &key, &input, &sig),
Err(Error::InvalidSignature),
"octet {at} bit {bit}"
);
}
}
}
#[test]
fn a_tag_over_other_input_or_under_another_hash_is_rejected() {
let key = jwk(A1_KEY);
let mut input = a1_signing_input();
let last = input.len() - 1;
input[last] ^= 1;
assert_eq!(
verify(&SigningAlg::HS256, &key, &input, &a1_signature()),
Err(Error::InvalidSignature)
);
let key = oct(&[0xaa; 131]);
let [(_, hs256), (_, hs384), _] = rfc4231_tc6();
for alg in [SigningAlg::HS384, SigningAlg::HS512] {
assert_eq!(
verify(&alg, &key, RFC4231_TC6_DATA, &hs256),
Err(Error::InvalidSignature),
"{alg}"
);
}
assert_eq!(
verify(&SigningAlg::HS256, &key, RFC4231_TC6_DATA, &hs384[..32]),
Err(Error::InvalidSignature)
);
}
#[test]
fn tags_are_compared_only_by_verify_slice() {
let source = include_str!("hmac.rs");
let production = &source[..source.find("#[cfg(test)]\nmod tests").unwrap()];
assert!(production.contains(".verify_slice(signature)"));
for banned in ["verify_truncated", "==", "!=", "ct_eq", ".verify("] {
assert!(!production.contains(banned), "found {banned:?}");
}
}
#[test]
fn a_key_shorter_than_the_hash_is_an_invalid_key_on_sign_and_verify() {
for (alg, min) in [
(SigningAlg::HS256, 32),
(SigningAlg::HS384, 48),
(SigningAlg::HS512, 64),
] {
for len in [1, min / 2, min - 1] {
let short = oct(&vec![0x42; len]);
assert_eq!(
sign(&alg, &short, b"input"),
Err(Error::InvalidKey(SHORT_KEY)),
"{alg} sign, {len} octets"
);
let tag = vec![0; min];
assert_eq!(
verify(&alg, &short, b"input", &tag),
Err(Error::InvalidKey(SHORT_KEY)),
"{alg} verify, {len} octets"
);
}
let exact = oct(&vec![0x42; min]);
let tag = sign(&alg, &exact, b"input").unwrap();
assert_eq!(tag.len(), min, "{alg}");
assert_eq!(verify(&alg, &exact, b"input", &tag), Ok(()), "{alg}");
}
}
#[test]
fn no_key_bytes_reach_an_error_or_debug() {
let secret = [0x5c_u8; 31];
let key = oct(&secret);
let k_b64 = b64url_encode(&secret);
let k_hex: String = secret.iter().map(|b| format!("{b:02x}")).collect();
let errors = [
sign(&SigningAlg::HS256, &key, b"input").unwrap_err(),
verify(&SigningAlg::HS256, &key, b"input", &[0; 32]).unwrap_err(),
verify(&SigningAlg::HS256, &jwk(A1_KEY), b"input", &[0; 32]).unwrap_err(),
];
let a1_k = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ";
for text in errors
.iter()
.flat_map(|e| [format!("{e}"), format!("{e:?}")])
.chain([format!("{key:?}"), format!("{:?}", jwk(A1_KEY))])
{
assert!(!text.contains(&k_b64), "{text}");
assert!(!text.contains(&k_hex), "{text}");
assert!(!text.contains(a1_k), "{text}");
assert!(!text.contains("5c5c"), "{text}");
}
}
#[test]
fn the_keyed_hmac_state_is_wiped_on_drop() {
use hmac::EagerHash;
fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
assert_zeroize_on_drop::<<Sha256 as EagerHash>::Core>();
assert_zeroize_on_drop::<<Sha384 as EagerHash>::Core>();
assert_zeroize_on_drop::<<Sha512 as EagerHash>::Core>();
}
#[test]
fn a_call_against_the_contract_is_a_mismatch_not_a_panic() {
use crate::jws::alg::test_keys;
let ec = test_keys::p256_private("");
assert_eq!(
sign(&SigningAlg::HS256, &ec, b"x"),
Err(Error::KeyAlgorithmMismatch)
);
assert_eq!(
verify(&SigningAlg::HS256, &ec, b"x", &[0; 32]),
Err(Error::KeyAlgorithmMismatch)
);
let key = jwk(A1_KEY);
for alg in [SigningAlg::RS256, SigningAlg::ES256, SigningAlg::None] {
assert_eq!(sign(&alg, &key, b"x"), Err(Error::KeyAlgorithmMismatch));
assert_eq!(
verify(&alg, &key, b"x", &[0; 32]),
Err(Error::KeyAlgorithmMismatch)
);
}
}
const WYCHEPROOF_HMAC: &[Deviation] = &[
Deviation::range(
82,
108,
Outcome::Reject,
"RFC 7518 §3.2: a JWS HMAC tag is the full hash output, never truncated",
),
Deviation::range(
163,
168,
Outcome::Reject,
"RFC 7518 §3.2: a key shorter than the hash output MUST NOT be used",
),
Deviation::range(
172,
174,
Outcome::Reject,
"RFC 7518 §3.2: a JWS HMAC tag is the full hash output, never truncated",
),
];
fn wycheproof_hmac(name: &str, alg: SigningAlg, hash_bits: u32) -> Summary {
let suite = load(name);
assert_eq!(suite.groups.len(), 6, "{name}: layout of WYCHEPROOF_HMAC");
assert_conforms(&suite, WYCHEPROOF_HMAC, |group, test| {
let (k, msg, tag) = test.mac();
let result = verify(&alg, &oct(k), msg, tag);
let key_bits = group.key_size.unwrap();
let tag_bits = group.tag_size.unwrap();
assert_eq!(key_bits as usize, k.len() * 8, "tcId {}", test.tc_id);
let expected_error = if key_bits < hash_bits {
Some(Error::InvalidKey(SHORT_KEY))
} else if tag_bits < hash_bits || test.result == Expected::Invalid {
Some(Error::InvalidSignature)
} else {
None
};
assert_eq!(result.clone().err(), expected_error, "tcId {}", test.tc_id);
Outcome::of(&result)
})
}
const WYCHEPROOF_HMAC_SUMMARY: Summary = Summary {
tests: 174,
accepted: 30,
rejected: 144,
deviations: 36,
};
#[test]
fn wycheproof_hmac_sha256() {
let summary = wycheproof_hmac("hmac_sha256", SigningAlg::HS256, 256);
assert_eq!(summary, WYCHEPROOF_HMAC_SUMMARY);
}
#[test]
fn wycheproof_hmac_sha384() {
let summary = wycheproof_hmac("hmac_sha384", SigningAlg::HS384, 384);
assert_eq!(summary, WYCHEPROOF_HMAC_SUMMARY);
}
#[test]
fn wycheproof_hmac_sha512() {
let summary = wycheproof_hmac("hmac_sha512", SigningAlg::HS512, 512);
assert_eq!(summary, WYCHEPROOF_HMAC_SUMMARY);
}
mod end_to_end {
use super::*;
use crate::jws::{
ProtectedHeader, SignOptions, VerifyOptions, sign_compact, verify_compact,
};
const HS: &[SigningAlg] = &[SigningAlg::HS256, SigningAlg::HS384, SigningAlg::HS512];
#[test]
fn rfc7515_a1_compact_signs_and_verifies_through_the_public_api() {
let protected = ProtectedHeader::from_encoded(A1_PROTECTED).unwrap();
let payload = b64url_decode(A1_PAYLOAD).unwrap();
let key = jwk(A1_KEY);
let jws = sign_compact(&protected, &payload, &key, &SignOptions::new()).unwrap();
assert_eq!(jws, format!("{A1_PROTECTED}.{A1_PAYLOAD}.{A1_SIGNATURE}"));
let opts = VerifyOptions::new(&[SigningAlg::HS256]).unwrap();
let verified = verify_compact(&jws, &key, &opts).unwrap();
assert_eq!(verified.alg, SigningAlg::HS256);
assert_eq!(*verified.payload, payload);
}
#[test]
fn every_hs_alg_round_trips_and_rejects_a_changed_signature() {
let key = oct(&[0xaa; 131]);
let opts = VerifyOptions::new(HS).unwrap();
for alg in HS {
let header = b64url_encode(format!(r#"{{"alg":"{alg}"}}"#).as_bytes());
let protected = ProtectedHeader::from_encoded(&header).unwrap();
let jws = sign_compact(&protected, b"payload", &key, &SignOptions::new()).unwrap();
let verified = verify_compact(&jws, &key, &opts).unwrap();
assert_eq!(verified.alg, *alg);
assert_eq!(*verified.payload, b"payload");
let (head, sig) = jws.rsplit_once('.').unwrap();
let mut sig = b64url_decode(sig).unwrap();
sig[0] ^= 0x80;
let tampered = format!("{head}.{}", b64url_encode(&sig));
assert_eq!(
verify_compact(&tampered, &key, &opts),
Err(Error::InvalidSignature),
"{alg}"
);
let truncated = format!("{head}.{}", b64url_encode(&sig[1..]));
assert_eq!(
verify_compact(&truncated, &key, &opts),
Err(Error::InvalidSignature),
"{alg}"
);
}
}
#[test]
fn a_short_key_is_refused_by_the_public_api_both_ways() {
let good = oct(&[0x11; 64]);
let short = oct(&[0x11; 47]);
let protected =
ProtectedHeader::from_encoded(&b64url_encode(br#"{"alg":"HS384"}"#)).unwrap();
assert_eq!(
sign_compact(&protected, b"p", &short, &SignOptions::new()).map_err(|e| e.code()),
Err("InvalidKey")
);
let jws = sign_compact(&protected, b"p", &good, &SignOptions::new()).unwrap();
let opts = VerifyOptions::new(HS).unwrap();
assert_eq!(
verify_compact(&jws, &short, &opts).map_err(|e| e.code()),
Err("InvalidKey")
);
}
}
}