use core::fmt;
use zeroize::Zeroizing;
use crate::error::{Error, Result};
use crate::header::Header;
use crate::jwa::SigningAlg;
use crate::jwk::Jwk;
use crate::util::{b64url_decode, b64url_encode};
use super::alg::{FAMILIES, Families};
use super::process::{
JoseHeader, allowed_alg, prepare_sign, signing_input, signing_input_from_part, verify_with_key,
};
use super::{ProtectedHeader, SignOptions, Verified, VerifyOptions};
pub fn sign_compact(
protected: &ProtectedHeader,
payload: &[u8],
key: &Jwk,
opts: &SignOptions,
) -> Result<String> {
sign_compact_with(&FAMILIES, protected, payload, key, opts)
}
pub(crate) fn sign_compact_with(
families: &Families,
protected: &ProtectedHeader,
payload: &[u8],
key: &Jwk,
opts: &SignOptions,
) -> Result<String> {
let header = JoseHeader::new(Some(protected.header().clone()), None)?;
let ready = prepare_sign(&header, key)?;
let b64 = header.b64();
if !b64 && !opts.detached && !payload.iter().all(|&b| is_unencoded_octet(b)) {
return Err(Error::InvalidFormat(
"b64:false compact payload must be printable ASCII without '.'",
));
}
let encoded = b64.then(|| Zeroizing::new(b64url_encode(payload)));
let payload_part = encoded.as_ref().map_or(payload, |e| e.as_bytes());
let input = signing_input_from_part(protected.encoded(), payload_part);
let signature = b64url_encode(&ready.sign(families, &input)?);
let carried = if opts.detached { &[][..] } else { payload_part };
let mut jws =
String::with_capacity(protected.encoded().len() + carried.len() + signature.len() + 2);
jws.push_str(protected.encoded());
jws.push('.');
jws.extend(carried.iter().copied().map(char::from));
jws.push('.');
jws.push_str(&signature);
Ok(jws)
}
fn is_unencoded_octet(octet: u8) -> bool {
matches!(octet, 0x20..=0x7e) && octet != b'.'
}
pub fn verify_compact(jws: &str, key: &Jwk, opts: &VerifyOptions<'_>) -> Result<Verified> {
verify_compact_with(&FAMILIES, jws, key, opts)
}
pub(crate) fn verify_compact_with(
families: &Families,
jws: &str,
key: &Jwk,
opts: &VerifyOptions<'_>,
) -> Result<Verified> {
let pre = precheck_compact(jws, opts)?;
finish_compact(families, &pre, key)
}
pub(crate) struct CompactPrechecked<'t> {
alg: SigningAlg,
protected: Header,
signing_input: Zeroizing<Vec<u8>>,
signature: Vec<u8>,
payload: PrecheckedPayload<'t>,
}
enum PrecheckedPayload<'t> {
Decoded(Zeroizing<Vec<u8>>),
Borrowed(&'t [u8]),
}
impl PrecheckedPayload<'_> {
fn as_bytes(&self) -> &[u8] {
match self {
Self::Decoded(decoded) => decoded,
Self::Borrowed(octets) => octets,
}
}
}
impl CompactPrechecked<'_> {
pub(crate) fn header(&self) -> &Header {
&self.protected
}
pub(crate) fn alg(&self) -> &SigningAlg {
&self.alg
}
}
impl fmt::Debug for CompactPrechecked<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompactPrechecked")
.field("alg", &self.alg)
.field("protected", &self.protected)
.field("payload_len", &self.payload.as_bytes().len())
.field("signature_len", &self.signature.len())
.finish_non_exhaustive()
}
}
pub(crate) fn precheck_compact<'t>(
jws: &'t str,
opts: &VerifyOptions<'t>,
) -> Result<CompactPrechecked<'t>> {
if jws.len() > opts.max_input_len {
return Err(Error::InvalidFormat("compact JWS exceeds max_input_len"));
}
let (protected_b64, payload_part, signature_b64) = split(jws)?;
if opts.detached_payload.is_some() && !payload_part.is_empty() {
return Err(Error::InvalidFormat(
"compact payload part must be empty with a detached payload",
));
}
let protected_json = Zeroizing::new(b64url_decode(protected_b64)?);
let signature = b64url_decode(signature_b64)?;
let protected = Header::from_json(&protected_json)?;
let b64 = protected.b64 != Some(false);
let payload = match opts.detached_payload {
Some(detached) => PrecheckedPayload::Borrowed(detached),
None if b64 => PrecheckedPayload::Decoded(Zeroizing::new(b64url_decode(payload_part)?)),
None => PrecheckedPayload::Borrowed(payload_part.as_bytes()),
};
let header = JoseHeader::new(Some(protected), None)?;
let alg = allowed_alg(&header, opts)?;
let signing_input = match opts.detached_payload {
Some(detached) => signing_input(protected_b64, detached, header.b64()),
None => signing_input_from_part(protected_b64, payload_part.as_bytes()),
};
Ok(CompactPrechecked {
alg,
protected: header.into_union(),
signing_input,
signature,
payload,
})
}
pub(crate) fn finish_compact(
families: &Families,
pre: &CompactPrechecked<'_>,
key: &Jwk,
) -> Result<Verified> {
verify_with_key(families, pre.alg(), key, &pre.signing_input, &pre.signature)?;
Ok(Verified {
alg: pre.alg().clone(),
protected: Some(pre.header().clone()),
unprotected: None,
payload: Zeroizing::new(pre.payload.as_bytes().to_vec()),
signature_index: 0,
})
}
fn split(jws: &str) -> Result<(&str, &str, &str)> {
let mut parts = jws.split('.');
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(protected), Some(payload), Some(signature), None) => {
Ok((protected, payload, signature))
}
_ => Err(Error::InvalidFormat("compact JWS must have three parts")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jwa::SigningAlg;
use crate::jws::alg::fake::{FAKE, REJECTING, fake_signature};
use crate::jws::alg::test_keys as keys;
use crate::jws::process::signing_input;
use crate::util::b64url_encode;
use SigningAlg as A;
const HS256: &str = r#"{"alg":"HS256"}"#;
const HS256_B64_FALSE: &str = r#"{"alg":"HS256","b64":false,"crit":["b64"]}"#;
const ALLOW_HS256: &[SigningAlg] = &[A::HS256];
fn forge(alg: &SigningAlg, header_json: &str, payload: &[u8], detached: bool) -> String {
let b64 = !header_json.contains(r#""b64":false"#);
let protected = b64url_encode(header_json.as_bytes());
let sig = fake_signature(alg, &signing_input(&protected, payload, b64));
let payload_part = match (detached, b64) {
(true, _) => String::new(),
(false, true) => b64url_encode(payload),
(false, false) => String::from_utf8(payload.to_vec()).unwrap(),
};
format!("{protected}.{payload_part}.{}", b64url_encode(&sig))
}
fn hs_key() -> Jwk {
keys::oct(32, "")
}
fn verify_with(jws: &str, opts: &VerifyOptions<'_>) -> Result<Verified> {
verify_compact_with(&FAKE, jws, &hs_key(), opts)
}
fn verify(jws: &str) -> Result<Verified> {
verify_with(jws, &VerifyOptions::new(ALLOW_HS256).unwrap())
}
fn verify_code(jws: &str) -> &'static str {
verify(jws).map_or_else(|e| e.code(), |_| "ok")
}
fn parts(jws: &str) -> [String; 3] {
let p: Vec<&str> = jws.split('.').collect();
[p[0].to_owned(), p[1].to_owned(), p[2].to_owned()]
}
#[test]
fn verifies_a_forged_token() {
let jws = forge(&A::HS256, HS256, b"hello", false);
let v = verify(&jws).unwrap();
assert_eq!(v.alg, A::HS256);
assert_eq!(
v.protected,
Some(Header::from_json(HS256.as_bytes()).unwrap())
);
assert_eq!(v.unprotected, None);
assert_eq!(*v.payload, b"hello");
assert_eq!(v.signature_index, 0);
}
#[test]
fn exactly_three_parts() {
let [p, l, s] = parts(&forge(&A::HS256, HS256, b"hello", false));
for jws in [
String::new(),
p.clone(),
format!("{p}.{l}"),
format!("{p}.{l}.{s}.{s}"),
format!("{p}.{l}.{s}."),
format!(".{p}.{l}.{s}"),
"...".to_owned(),
] {
assert_eq!(verify_code(&jws), "InvalidFormat", "{jws}");
}
}
#[test]
fn input_over_max_input_len() {
let jws = forge(&A::HS256, HS256, b"hello", false);
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
assert!(verify_with(&jws, &opts.max_input_len(jws.len())).is_ok());
assert_eq!(
verify_with(&jws, &opts.max_input_len(jws.len() - 1)).map_err(|e| e.code()),
Err("InvalidFormat")
);
assert_eq!(
verify_with("a.b", &opts.max_input_len(1)),
Err(Error::InvalidFormat("compact JWS exceeds max_input_len"))
);
}
#[test]
fn strict_base64url_in_every_part() {
let jws = forge(&A::HS256, HS256, b"hello world", false);
let [p, l, s] = parts(&jws);
let mutations: [fn(&str) -> String; 7] = [
|x| format!("{x}="),
|x| format!("{x}=="),
|x| format!("+{}", &x[1..]),
|x| format!("/{}", &x[1..]),
|x| format!("{} {}", &x[..2], &x[2..]),
|x| format!("{}\n{}", &x[..2], &x[2..]),
|x| format!("{}\r\n{}", &x[..2], &x[2..]),
];
for m in mutations {
for jws in [
format!("{}.{l}.{s}", m(&p)),
format!("{p}.{}.{s}", m(&l)),
format!("{p}.{l}.{}", m(&s)),
] {
assert_eq!(verify(&jws), Err(Error::InvalidBase64), "{jws:?}");
}
}
}
#[test]
fn a_non_canonical_payload_part_never_reaches_the_signature() {
let protected = b64url_encode(HS256.as_bytes());
let sig = fake_signature(&A::HS256, format!("{protected}.QR").as_bytes());
let jws = format!("{protected}.QR.{}", b64url_encode(&sig));
assert_eq!(verify(&jws), Err(Error::InvalidBase64));
let canonical = forge(&A::HS256, HS256, &[0x41], false);
assert_eq!(parts(&canonical)[1], "QQ");
assert_eq!(*verify(&canonical).unwrap().payload, [0x41]);
}
#[test]
fn protected_header_must_be_one_json_object() {
for header in [
&br#"{"alg":"HS256"}x"#[..],
br#"{"alg":"HS256"}{}"#,
br#"{"alg":"HS256"},"#,
br#"["HS256"]"#,
br#""HS256""#,
b"",
b"{\"alg\":\"HS256\",\"x\":\"\xff\"}",
] {
let jws = format!("{}.aGk.", b64url_encode(header));
assert_eq!(verify_code(&jws), "InvalidJson", "{header:?}");
}
}
#[test]
fn duplicate_header_member_is_invalid_json() {
for header in [
r#"{"alg":"HS256","alg":"HS256"}"#,
r#"{"alg":"HS256","alg":"none"}"#,
r#"{"alg":"HS256","b64":false,"crit":["b64"],"b64":false}"#,
r#"{"alg":"HS256","x":1,"x":2}"#,
] {
let jws = forge(&A::HS256, header, b"hi", false);
assert_eq!(verify_code(&jws), "InvalidJson", "{header}");
}
}
#[test]
fn header_structure_rules_apply() {
for header in [
r#"{"typ":"JWT"}"#,
r#"{"alg":"HS256","b64":false}"#,
r#"{"alg":"HS256","b64":true}"#,
r#"{"alg":"HS256","crit":["x-a"]}"#,
r#"{"alg":1}"#,
] {
let jws = forge(&A::HS256, header, b"hi", false);
assert_eq!(verify_code(&jws), "InvalidHeader", "{header}");
}
}
#[test]
fn detached_payload_requires_an_empty_payload_part() {
let attached = forge(&A::HS256, HS256, b"hello", false);
let opts = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(b"hello");
assert_eq!(
verify_with(&attached, &opts).map_err(|e| e.code()),
Err("InvalidFormat")
);
let detached = forge(&A::HS256, HS256, b"hello", true);
assert!(detached.contains(".."));
let v = verify_with(&detached, &opts).unwrap();
assert_eq!(*v.payload, b"hello");
let wrong = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(b"hellO");
assert_eq!(verify_with(&detached, &wrong), Err(Error::InvalidSignature));
}
#[test]
fn empty_payload_part_without_detached_is_an_empty_payload() {
let empty = forge(&A::HS256, HS256, b"", false);
let v = verify(&empty).unwrap();
assert_eq!(*v.payload, b"");
let detached = forge(&A::HS256, HS256, b"hello", true);
assert_eq!(verify(&detached), Err(Error::InvalidSignature));
let opts = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(b"");
assert_eq!(*verify_with(&empty, &opts).unwrap().payload, b"");
}
#[test]
fn b64_false_payload_part_is_the_raw_octets() {
for payload in [
&b"$02"[..],
b"QQ",
b"hello world",
b" ~!",
"\u{e9}".as_bytes(),
b"",
] {
let jws = forge(&A::HS256, HS256_B64_FALSE, payload, false);
let v = verify(&jws).unwrap();
assert_eq!(*v.payload, payload, "{jws}");
assert_eq!(v.protected.unwrap().b64, Some(false));
}
let jws = forge(&A::HS256, HS256_B64_FALSE, b"QQ", false);
assert_eq!(*verify(&jws).unwrap().payload, b"QQ");
}
#[test]
fn b64_false_detached_rfc7797_section_4_2() {
let protected = "eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19";
let input = format!("{protected}.$.02");
let sig = b64url_encode(&fake_signature(&A::HS256, input.as_bytes()));
let jws = format!("{protected}..{sig}");
let opts = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(b"$.02");
let v = verify_with(&jws, &opts).unwrap();
assert_eq!(*v.payload, b"$.02");
assert_eq!(v.alg, A::HS256);
let rfc = format!("{protected}..A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY");
assert_eq!(verify_with(&rfc, &opts), Err(Error::InvalidSignature));
}
#[test]
fn rfc7515_a1_token_parses_and_keeps_the_crlf_header() {
let protected = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9";
let payload_part = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ";
let rfc = format!("{protected}.{payload_part}.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
assert_eq!(verify(&rfc), Err(Error::InvalidSignature));
assert_eq!(
verify_compact_with(
&REJECTING,
&rfc,
&hs_key(),
&VerifyOptions::new(ALLOW_HS256).unwrap()
),
Err(Error::InvalidSignature)
);
let input = format!("{protected}.{payload_part}");
let sig = b64url_encode(&fake_signature(&A::HS256, input.as_bytes()));
let v = verify(&format!("{input}.{sig}")).unwrap();
assert_eq!(v.protected.unwrap().typ.as_deref(), Some("JWT"));
assert_eq!(
*v.payload,
b"{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}"
);
}
const A5_NONE: &str = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.";
#[test]
fn rfc7515_a5_none_is_never_allowed() {
let all: Vec<SigningAlg> = SigningAlg::KNOWN
.iter()
.filter(|a| **a != A::None)
.cloned()
.collect();
let lists: [&[SigningAlg]; 3] = [ALLOW_HS256, &[A::RS256, A::ES256], &all];
let keys = [hs_key(), keys::rsa(2048), keys::p256(""), keys::ed25519("")];
let forged_sig = format!(
"{}{}",
A5_NONE,
b64url_encode(&fake_signature(
&A::None,
A5_NONE.trim_end_matches('.').as_bytes()
))
);
for allowed in lists {
let opts = VerifyOptions::new(allowed).unwrap();
for key in &keys {
for jws in [A5_NONE, forged_sig.as_str()] {
assert_eq!(
verify_compact(jws, key, &opts),
Err(Error::AlgorithmNotAllowed)
);
assert_eq!(
verify_compact_with(&FAKE, jws, key, &opts),
Err(Error::AlgorithmNotAllowed)
);
}
}
}
}
#[test]
fn steps_5_to_9_come_from_the_shared_rules() {
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
let code = |header: &str, key: &Jwk| {
let jws = forge(&A::HS256, header, b"hi", false);
verify_compact_with(&FAKE, &jws, key, &opts).map_or_else(|e| e.code(), |_| "ok")
};
let hs = hs_key();
assert_eq!(
code(r#"{"alg":"HS256","crit":["x-a"],"x-a":1}"#, &hs),
"UnknownCritical"
);
assert_eq!(code(r#"{"alg":"HS1"}"#, &hs), "UnsupportedAlgorithm");
assert_eq!(code(r#"{"alg":"HS384"}"#, &hs), "AlgorithmNotAllowed");
assert_eq!(code(HS256, &keys::rsa(2048)), "KeyAlgorithmMismatch");
assert_eq!(code(HS256, &keys::oct(31, "")), "InvalidKey");
assert_eq!(code(HS256, &hs), "ok");
}
#[test]
fn tampering_is_an_invalid_signature() {
let jws = forge(&A::HS256, HS256, b"hello", false);
let [p, _, s] = parts(&jws);
let other_payload = format!("{p}.{}.{s}", b64url_encode(b"hellO"));
let other_header = format!(
"{}.{}.{s}",
b64url_encode(br#"{"alg":"HS256","kid":"x"}"#),
b64url_encode(b"hello")
);
let mut sig = crate::util::b64url_decode(&s).unwrap();
sig[0] ^= 1;
let other_sig = format!("{p}.{}.{}", b64url_encode(b"hello"), b64url_encode(&sig));
let short_sig = format!("{p}.{}.", b64url_encode(b"hello"));
for t in [other_payload, other_header, other_sig, short_sig] {
assert_eq!(verify(&t), Err(Error::InvalidSignature), "{t}");
}
assert_eq!(
verify_compact_with(
&REJECTING,
&jws,
&hs_key(),
&VerifyOptions::new(ALLOW_HS256).unwrap()
),
Err(Error::InvalidSignature)
);
}
#[test]
fn precedence_of_steps_1_to_4() {
let jws = forge(&A::HS256, HS256, b"hello", false);
let [p, l, s] = parts(&jws);
let bad_json = b64url_encode(br#"{"alg":"HS256""#);
assert_eq!(verify_code(&format!("{p}=.{l}.{s}.x")), "InvalidFormat");
let opts = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(b"hello");
assert_eq!(
verify_with(&format!("{bad_json}.{l}.{s}="), &opts).map_err(|e| e.code()),
Err("InvalidFormat")
);
assert_eq!(
verify_code(&format!("{bad_json}.{l}.{s}=")),
"InvalidBase64"
);
assert_eq!(verify_code(&format!("{bad_json}.{l}=.{s}")), "InvalidJson");
let no_alg = b64url_encode(br#"{"typ":"JWT"}"#);
assert_eq!(verify_code(&format!("{no_alg}.{l}=.{s}")), "InvalidBase64");
let none_bad_crit = b64url_encode(br#"{"alg":"none","crit":["x-a"]}"#);
assert_eq!(
verify_code(&format!("{none_bad_crit}.{l}.{s}")),
"InvalidHeader"
);
}
fn protected(json: &str) -> ProtectedHeader {
ProtectedHeader::from_encoded(&b64url_encode(json.as_bytes())).unwrap()
}
fn sign(header_json: &str, payload: &[u8], detached: bool) -> Result<String> {
let opts = SignOptions::new().detached(detached);
sign_compact_with(&FAKE, &protected(header_json), payload, &hs_key(), &opts)
}
fn sign_code(header_json: &str, payload: &[u8], detached: bool) -> &'static str {
sign(header_json, payload, detached).map_or_else(|e| e.code(), |_| "ok")
}
#[test]
fn sign_emits_three_parts() {
let jws = sign(HS256, b"hello", false).unwrap();
assert_eq!(jws, forge(&A::HS256, HS256, b"hello", false));
let [p, l, s] = parts(&jws);
assert_eq!(p, b64url_encode(HS256.as_bytes()));
assert_eq!(l, "aGVsbG8");
let input = format!("{p}.{l}");
assert_eq!(
s,
b64url_encode(&fake_signature(&A::HS256, input.as_bytes()))
);
assert_eq!(parts(&sign(HS256, b"", false).unwrap())[1], "");
}
#[test]
fn sign_keeps_the_rfc7515_a1_header_encoding() {
let a1 = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9";
let payload =
b"{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}";
let header = ProtectedHeader::from_encoded(a1).unwrap();
let jws =
sign_compact_with(&FAKE, &header, payload, &hs_key(), &SignOptions::new()).unwrap();
let input = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ";
let sig = b64url_encode(&fake_signature(&A::HS256, input.as_bytes()));
assert_eq!(jws, format!("{input}.{sig}"));
}
#[test]
fn sign_detached_emits_an_empty_payload_part() {
let jws = sign(HS256, b"hello", true).unwrap();
assert_eq!(jws, forge(&A::HS256, HS256, b"hello", true));
let [p, l, _] = parts(&jws);
assert_eq!(l, "");
assert!(jws.starts_with(&format!("{p}..")));
let opts = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(b"hello");
assert_eq!(*verify_with(&jws, &opts).unwrap().payload, b"hello");
}
#[test]
fn sign_b64_false_attached_payload_rules() {
for payload in [
&b"$.02"[..],
b".",
b"a\nb",
b"\t",
b"\x1f",
b"\x7f",
b"\x80",
"\u{e9}".as_bytes(),
b"\x00",
] {
assert_eq!(
sign_code(HS256_B64_FALSE, payload, false),
"InvalidFormat",
"{payload:?}"
);
}
for payload in [&b"$02"[..], b" ", b"~", b" !~-_", b""] {
let jws = sign(HS256_B64_FALSE, payload, false).unwrap();
assert_eq!(jws, forge(&A::HS256, HS256_B64_FALSE, payload, false));
assert_eq!(parts(&jws)[1].as_bytes(), payload);
assert_eq!(*verify(&jws).unwrap().payload, payload);
}
}
#[test]
fn sign_b64_false_detached_rfc7797_section_4_2() {
let header = ProtectedHeader::from_encoded(
"eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19",
)
.unwrap();
let opts = SignOptions::new().detached(true);
let jws = sign_compact_with(&FAKE, &header, b"$.02", &hs_key(), &opts).unwrap();
let input = "eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19.$.02";
let sig = b64url_encode(&fake_signature(&A::HS256, input.as_bytes()));
assert_eq!(
jws,
format!("eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..{sig}")
);
assert_eq!(sign_code(HS256_B64_FALSE, b"\x00\xff.\n", true), "ok");
}
#[test]
fn sign_b64_must_be_critical() {
for header in [
r#"{"alg":"HS256","b64":false}"#,
r#"{"alg":"HS256","b64":true}"#,
r#"{"alg":"HS256","b64":false,"crit":["x-a"],"x-a":1}"#,
] {
for detached in [false, true] {
assert_eq!(
sign_code(header, b"$02", detached),
"InvalidHeader",
"{header}"
);
}
}
assert_eq!(
sign_code(r#"{"alg":"HS256","b64":false}"#, b"$.02", false),
"InvalidHeader"
);
let t = r#"{"alg":"HS256","b64":true,"crit":["b64"]}"#;
assert_eq!(parts(&sign(t, b"$.02", false).unwrap())[1], "JC4wMg");
}
#[test]
fn sign_policy_errors() {
assert_eq!(sign_code(r#"{"typ":"JWT"}"#, b"x", false), "InvalidHeader");
assert_eq!(
sign_code(r#"{"alg":"HS256","crit":["x-a"],"x-a":1}"#, b"x", false),
"UnknownCritical"
);
assert_eq!(
sign_code(r#"{"alg":"HS1"}"#, b"x", false),
"UnsupportedAlgorithm"
);
assert_eq!(
sign_code(r#"{"alg":"none"}"#, b"x", false),
"AlgorithmNotAllowed"
);
assert_eq!(
sign_code(r#"{"alg":"RS256"}"#, b"x", false),
"KeyAlgorithmMismatch"
);
assert_eq!(sign_code(r#"{"alg":"HS512"}"#, b"x", false), "InvalidKey");
let opts = SignOptions::new();
let es = protected(r#"{"alg":"ES256"}"#);
assert_eq!(
sign_compact_with(&FAKE, &es, b"x", &keys::p256(""), &opts),
Err(Error::KeyAlgorithmMismatch)
);
assert_eq!(
sign_compact_with(&FAKE, &es, b"x", &hs_key(), &opts),
Err(Error::KeyAlgorithmMismatch)
);
}
#[test]
fn sign_verify_round_trip() {
let cases = [
(
A::HS256,
r#"{"alg":"HS256","kid":"k1"}"#,
keys::oct(32, ""),
keys::oct(32, ""),
),
(
A::HS512,
r#"{"alg":"HS512"}"#,
keys::oct(64, ""),
keys::oct(64, ""),
),
(
A::ES256,
r#"{"alg":"ES256","typ":"JWT"}"#,
keys::p256_private(""),
keys::p256(""),
),
(
A::EdDSA,
r#"{"alg":"EdDSA"}"#,
keys::ed25519_private(""),
keys::ed25519(""),
),
(
A::Ed25519,
r#"{"alg":"Ed25519"}"#,
keys::ed25519_private(""),
keys::ed25519(""),
),
];
for (alg, header_json, private, public) in cases {
let allowed = [alg.clone()];
for (payload, detached, b64_false) in [
(&b"hello"[..], false, false),
(b"", false, false),
(b"\x00\xff binary", false, false),
(b"\x00\xff binary", true, false),
(b"$02", false, true),
(b"$.02", true, true),
] {
let json = if b64_false {
header_json.replacen('}', r#","b64":false,"crit":["b64"]}"#, 1)
} else {
header_json.to_owned()
};
let header = protected(&json);
let opts = SignOptions::new().detached(detached);
let jws = sign_compact_with(&FAKE, &header, payload, &private, &opts).unwrap();
let mut vopts = VerifyOptions::new(&allowed).unwrap();
if detached {
vopts = vopts.detached_payload(payload);
}
let v = verify_compact_with(&FAKE, &jws, &public, &vopts).unwrap();
assert_eq!(v.alg, alg);
assert_eq!(v.protected.as_ref(), Some(header.header()));
assert_eq!(v.unprotected, None);
assert_eq!(*v.payload, payload);
assert_eq!(v.signature_index, 0);
}
}
}
#[test]
fn max_input_len_does_not_count_the_detached_payload() {
let payload = vec![b'a'; 4096];
let jws = forge(&A::HS256, HS256, &payload, true);
let opts = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(&payload)
.max_input_len(jws.len());
assert_eq!(*verify_with(&jws, &opts).unwrap().payload, payload);
}
#[test]
fn the_received_header_encoding_is_signed() {
let jws = forge(&A::HS256, HS256, b"hello", false);
let [_, l, s] = parts(&jws);
let respaced = b64url_encode(br#"{ "alg":"HS256"}"#);
assert_eq!(
verify(&format!("{respaced}.{l}.{s}")),
Err(Error::InvalidSignature)
);
let b64_false = forge(&A::HS256, HS256_B64_FALSE, b"aGVsbG8", false);
let [p, _, _] = parts(&b64_false);
assert_eq!(
verify(&format!("{p}.{l}.{s}")),
Err(Error::InvalidSignature)
);
}
#[test]
fn b64_of_the_wrong_type_is_invalid_header() {
for header in [
r#"{"alg":"HS256","b64":"false","crit":["b64"]}"#,
r#"{"alg":"HS256","b64":0,"crit":["b64"]}"#,
] {
let jws = format!("{}.$02.", b64url_encode(header.as_bytes()));
assert_eq!(verify_code(&jws), "InvalidHeader", "{header}");
}
}
#[test]
fn errors_carry_no_input() {
const MARKER: &str = "MARKERxyz";
let marker_b64 = b64url_encode(MARKER.as_bytes());
let header_b64 = b64url_encode(format!(r#"{{"alg":"HS256","x":"{MARKER}"}}"#).as_bytes());
let detached = VerifyOptions::new(ALLOW_HS256)
.unwrap()
.detached_payload(MARKER.as_bytes());
let cases = [
(format!("{MARKER}.{MARKER}"), None),
(format!("{MARKER}.{MARKER}.{MARKER}"), None),
(format!("{header_b64}.{marker_b64}.{MARKER}="), None),
(format!("{marker_b64}.{marker_b64}."), None),
(format!("{header_b64}.{marker_b64}."), None),
(format!("{header_b64}.{marker_b64}."), Some(detached)),
];
for (jws, opts) in cases {
let opts = opts.unwrap_or_else(|| VerifyOptions::new(ALLOW_HS256).unwrap());
let err = verify_with(&jws, &opts).unwrap_err();
for text in [err.to_string(), format!("{err:?}")] {
assert!(!text.contains(MARKER), "{text}");
assert!(!text.contains(&marker_b64), "{text}");
}
}
let err = sign(HS256_B64_FALSE, format!("{MARKER}.\n").as_bytes(), false).unwrap_err();
assert!(!err.to_string().contains(MARKER));
}
use crate::test_counters::{jws_family_signs as signs, jws_family_verifies as verifies};
const COUNTING: Families = Families {
sign: |alg, key, input| {
signs::tick();
(FAKE.sign)(alg, key, input)
},
verify: |alg, key, input, signature| {
verifies::tick();
(FAKE.verify)(alg, key, input, signature)
},
};
fn family_calls<R>(f: impl FnOnce() -> R) -> (R, u64, u64) {
let ((result, verify_calls), sign_calls) = signs::count(|| verifies::count(f));
(result, sign_calls, verify_calls)
}
fn other_hs_key() -> Jwk {
keys::jwk(&format!(
r#"{{"kty":"oct","k":"{}"}}"#,
b64url_encode(&[0x33; 32])
))
}
fn step_1_to_7_failures() -> Vec<(String, VerifyOptions<'static>, &'static str)> {
let jws = forge(&A::HS256, HS256, b"hello", false);
let [p, l, s] = parts(&jws);
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
let header = |json: &str| forge(&A::HS256, json, b"hi", false);
let no_alg = b64url_encode(br#"{"typ":"JWT"}"#);
let bad_json = b64url_encode(br#"{"alg":"HS256""#);
let detached = opts.detached_payload(b"hello");
vec![
(
jws.clone(),
opts.max_input_len(jws.len() - 1),
"InvalidFormat",
),
("a.b".to_owned(), opts, "InvalidFormat"),
(format!("{jws}.{s}"), opts, "InvalidFormat"),
(jws.clone(), detached, "InvalidFormat"),
(format!("{p}.{l}.{s}="), opts, "InvalidBase64"),
(format!("{p}=.{l}.{s}"), opts, "InvalidBase64"),
(format!("{bad_json}.{l}.{s}"), opts, "InvalidJson"),
(
header(r#"{"alg":"HS256","alg":"HS256"}"#),
opts,
"InvalidJson",
),
(header(r#"{"alg":1}"#), opts, "InvalidHeader"),
(
header(r#"{"alg":"HS256","crit":[]}"#),
opts,
"InvalidHeader",
),
(
format!(
"{}.$02.",
b64url_encode(br#"{"alg":"HS256","b64":0,"crit":["b64"]}"#)
),
opts,
"InvalidHeader",
),
(format!("{p}.{l}=.{s}"), opts, "InvalidBase64"),
(format!("{no_alg}.{l}=.{s}"), opts, "InvalidBase64"),
(header(r#"{"typ":"JWT"}"#), opts, "InvalidHeader"),
(
header(r#"{"alg":"HS256","b64":false}"#),
opts,
"InvalidHeader",
),
(
header(r#"{"alg":"HS256","crit":["x-a"]}"#),
opts,
"InvalidHeader",
),
(
header(r#"{"alg":"HS256","crit":["x-a"],"x-a":1}"#),
opts,
"UnknownCritical",
),
(header(r#"{"alg":"HS1"}"#), opts, "UnsupportedAlgorithm"),
(header(r#"{"alg":"HS384"}"#), opts, "AlgorithmNotAllowed"),
(A5_NONE.to_owned(), opts, "AlgorithmNotAllowed"),
]
}
#[test]
fn precheck_gives_verify_compacts_error_for_each_step_1_to_7() {
let candidate_keys = [hs_key(), keys::p384(), keys::p256(""), keys::ed25519("")];
for (jws, opts, code) in step_1_to_7_failures() {
let (pre, sign_calls, verify_calls) = family_calls(|| precheck_compact(&jws, &opts));
let err = pre.map(drop).unwrap_err();
assert_eq!(err.code(), code, "{jws}");
assert_eq!((sign_calls, verify_calls), (0, 0), "{jws}");
for key in &candidate_keys {
assert_eq!(verify_compact(&jws, key, &opts), Err(err.clone()), "{jws}");
assert_eq!(
verify_compact_with(&COUNTING, &jws, key, &opts),
Err(err.clone()),
"{jws}"
);
}
}
}
#[test]
fn step_8_and_9_failures_pass_precheck_and_fail_in_finish() {
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
let good = forge(&A::HS256, HS256, b"hello", false);
let [p, _, s] = parts(&good);
let tampered = format!("{p}.{}.{s}", b64url_encode(b"hellO"));
let detached = forge(&A::HS256, HS256, b"hello", true);
let wrong_detached = opts.detached_payload(b"hellO");
let cases: [(&str, VerifyOptions<'_>, Jwk, &str, u64); 8] = [
(&good, opts, keys::ed25519(""), "KeyAlgorithmMismatch", 0),
(&good, opts, keys::p256(""), "KeyAlgorithmMismatch", 0),
(
&good,
opts,
keys::oct(32, r#","alg":"HS512""#),
"KeyAlgorithmMismatch",
0,
),
(
&good,
opts,
keys::oct(32, r#","use":"enc""#),
"KeyAlgorithmMismatch",
0,
),
(
&good,
opts,
keys::oct(32, r#","key_ops":["sign"]"#),
"KeyAlgorithmMismatch",
0,
),
(&good, opts, keys::oct(31, ""), "InvalidKey", 0),
(&tampered, opts, hs_key(), "InvalidSignature", 1),
(&detached, wrong_detached, hs_key(), "InvalidSignature", 1),
];
for (jws, opts, key, code, expected_verifies) in cases {
let (pre, _, verify_calls) = family_calls(|| precheck_compact(jws, &opts));
assert_eq!(verify_calls, 0);
let pre = pre.unwrap();
let (finished, sign_calls, verify_calls) =
family_calls(|| finish_compact(&COUNTING, &pre, &key));
let err = finished.unwrap_err();
assert_eq!(err.code(), code, "{jws} {code}");
assert_eq!((sign_calls, verify_calls), (0, expected_verifies), "{code}");
assert_eq!(verify_compact_with(&COUNTING, jws, &key, &opts), Err(err));
}
}
#[test]
fn finish_matches_verify_compact_on_success() {
let b64_false = forge(&A::HS256, HS256_B64_FALSE, b"$02", false);
let rfc7797_detached = forge(&A::HS256, HS256_B64_FALSE, b"$.02", true);
let attached = forge(
&A::HS256,
r#"{"alg":"HS256","typ":"JWT","kid":"k1"}"#,
b"hi",
false,
);
let detached = forge(&A::HS256, HS256, b"\x00\xff", true);
let empty = forge(&A::HS256, HS256, b"", false);
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
for (jws, opts) in [
(&attached, opts),
(&empty, opts),
(&b64_false, opts),
(&detached, opts.detached_payload(b"\x00\xff")),
(&rfc7797_detached, opts.detached_payload(b"$.02")),
] {
let pre = precheck_compact(jws, &opts).unwrap();
let (v, sign_calls, verify_calls) =
family_calls(|| finish_compact(&COUNTING, &pre, &hs_key()));
assert_eq!((sign_calls, verify_calls), (0, 1));
assert_eq!(
v,
verify_compact_with(&FAKE, jws, &hs_key(), &opts),
"{jws}"
);
}
}
#[test]
fn one_precheck_finishes_against_several_keys() {
let right = hs_key();
let header = protected(r#"{"alg":"HS256","typ":"JWT","kid":"k1"}"#);
let jws = sign_compact(&header, b"payload", &right, &SignOptions::new()).unwrap();
let opts = VerifyOptions::new(&[A::HS256, A::ES256]).unwrap();
let pre = precheck_compact(&jws, &opts).unwrap();
assert_eq!(pre.alg(), &A::HS256);
assert_eq!(pre.header(), header.header());
assert_eq!(pre.header().kid.as_deref(), Some("k1"));
assert_eq!(pre.header().typ.as_deref(), Some("JWT"));
let expected = verify_compact(&jws, &right, &opts).unwrap();
assert_eq!(
finish_compact(&FAMILIES, &pre, &keys::p256("")),
Err(Error::KeyAlgorithmMismatch)
);
assert_eq!(
finish_compact(&FAMILIES, &pre, &other_hs_key()),
Err(Error::InvalidSignature)
);
assert_eq!(
finish_compact(&FAMILIES, &pre, &right),
Ok(expected.clone())
);
assert_eq!(
finish_compact(&FAMILIES, &pre, &other_hs_key()),
Err(Error::InvalidSignature)
);
assert_eq!(finish_compact(&FAMILIES, &pre, &right), Ok(expected));
}
#[test]
fn precheck_holds_the_payload_wiped_or_borrowed() {
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
let jws = forge(&A::HS256, HS256, b"secret", false);
let pre = precheck_compact(&jws, &opts).unwrap();
let _: &Zeroizing<Vec<u8>> = &pre.signing_input;
match &pre.payload {
PrecheckedPayload::Decoded(decoded) => {
let decoded: &Zeroizing<Vec<u8>> = decoded;
assert_eq!(**decoded, b"secret");
}
PrecheckedPayload::Borrowed(_) => panic!("a b64 payload must be decoded"),
}
let unencoded = forge(&A::HS256, HS256_B64_FALSE, b"$02", false);
let pre = precheck_compact(&unencoded, &opts).unwrap();
let [_, part, _] = parts(&unencoded);
assert!(
matches!(pre.payload, PrecheckedPayload::Borrowed(b) if b == part.as_bytes()
&& unencoded.as_bytes().as_ptr_range().contains(&b.as_ptr()))
);
let content = b"detached".to_vec();
let detached = forge(&A::HS256, HS256, &content, true);
let with_content = opts.detached_payload(&content);
let pre = precheck_compact(&detached, &with_content).unwrap();
assert!(
matches!(pre.payload, PrecheckedPayload::Borrowed(b) if core::ptr::eq(b, &content[..]))
);
let v = finish_compact(&FAKE, &pre, &hs_key()).unwrap();
let _: &Zeroizing<Vec<u8>> = &v.payload;
assert_eq!(*v.payload, content);
}
#[test]
fn precheck_debug_prints_no_payload_or_signature_bytes() {
const MARKER: &[u8] = b"MARKERxyz-payload";
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
let attached = forge(&A::HS256, HS256, MARKER, false);
let unencoded = forge(&A::HS256, HS256_B64_FALSE, MARKER, false);
let detached = forge(&A::HS256, HS256, MARKER, true);
for (jws, opts) in [
(&attached, opts),
(&unencoded, opts),
(&detached, opts.detached_payload(MARKER)),
] {
let pre = precheck_compact(jws, &opts).unwrap();
let text = format!("{pre:?}");
let [_, _, s] = parts(jws);
let signature = crate::util::b64url_decode(&s).unwrap();
for secret in [
"MARKER".to_owned(),
b64url_encode(MARKER),
format!("{MARKER:?}"),
s.clone(),
format!("{:?}", &signature[..4])
.trim_end_matches(']')
.to_owned(),
] {
assert!(!text.contains(&secret), "{text} contains {secret}");
}
assert!(text.contains("payload_len: 17"), "{text}");
assert!(text.contains("signature_len: 32"), "{text}");
assert!(text.contains("HS256"), "{text}");
}
}
#[test]
fn public_functions_use_the_real_families() {
let opts = VerifyOptions::new(ALLOW_HS256).unwrap();
assert_eq!(
verify_compact("a.b", &hs_key(), &opts).map_err(|e| e.code()),
Err("InvalidFormat")
);
let sign_opts = SignOptions::new();
assert_eq!(
sign_compact(&protected(r#"{"alg":"none"}"#), b"x", &hs_key(), &sign_opts),
Err(Error::AlgorithmNotAllowed)
);
assert!(
sign_compact(
&protected(r#"{"alg":"RS256"}"#),
b"x",
&keys::rsa_private(""),
&sign_opts
)
.is_ok()
);
assert_eq!(
sign_compact(
&protected(r#"{"alg":"RS256"}"#),
b"x",
&keys::rsa(2048),
&sign_opts
),
Err(Error::KeyAlgorithmMismatch)
);
assert_eq!(
sign_compact(&protected(HS256_B64_FALSE), b"$.02", &hs_key(), &sign_opts)
.map_err(|e| e.code()),
Err("InvalidFormat")
);
}
}