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};
mod alg;
mod compact;
mod ecdsa;
mod eddsa;
mod hmac;
mod json;
mod process;
mod rsa;
#[cfg(test)]
mod wycheproof_jose;
pub use compact::{sign_compact, verify_compact};
pub use json::{sign_flattened, sign_general, verify_json};
pub(crate) use alg::{FAMILIES, Families};
pub(crate) use compact::{CompactPrechecked, finish_compact, precheck_compact};
pub(crate) use process::furthest;
#[derive(Clone, PartialEq)]
pub struct ProtectedHeader {
header: Header,
encoded: String,
}
impl ProtectedHeader {
pub fn new(header: Header) -> Result<Self> {
let json = Zeroizing::new(header.to_json()?);
let encoded = b64url_encode(json.as_bytes());
Ok(Self { header, encoded })
}
pub fn from_encoded(encoded: &str) -> Result<Self> {
let json = Zeroizing::new(b64url_decode(encoded)?);
let header = Header::from_json(&json)?;
Ok(Self {
header,
encoded: encoded.to_owned(),
})
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn encoded(&self) -> &str {
&self.encoded
}
pub fn into_header(self) -> Header {
self.header
}
}
impl fmt::Debug for ProtectedHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProtectedHeader")
.field("header", &self.header)
.field("encoded_len", &self.encoded.len())
.finish()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SignOptions {
pub detached: bool,
}
impl SignOptions {
pub fn new() -> Self {
Self::default()
}
pub fn detached(mut self, detached: bool) -> Self {
self.detached = detached;
self
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct Signer<'a> {
pub protected: Option<ProtectedHeader>,
pub unprotected: Option<Header>,
pub key: &'a Jwk,
}
impl<'a> Signer<'a> {
pub fn new(key: &'a Jwk) -> Self {
Self {
protected: None,
unprotected: None,
key,
}
}
pub fn protected(mut self, protected: ProtectedHeader) -> Self {
self.protected = Some(protected);
self
}
pub fn unprotected(mut self, unprotected: Header) -> Self {
self.unprotected = Some(unprotected);
self
}
}
impl fmt::Debug for Signer<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Signer")
.field("protected", &self.protected)
.field("unprotected", &self.unprotected)
.field("key", self.key)
.finish()
}
}
#[derive(Clone, Copy)]
pub struct VerifyOptions<'a> {
allowed_algs: &'a [SigningAlg],
detached_payload: Option<&'a [u8]>,
max_input_len: usize,
max_signatures: usize,
}
impl<'a> VerifyOptions<'a> {
pub const DEFAULT_MAX_INPUT_LEN: usize = 1024 * 1024;
pub const DEFAULT_MAX_SIGNATURES: usize = 16;
pub fn new(allowed_algs: &'a [SigningAlg]) -> Result<Self> {
let usable = |alg: &SigningAlg| alg.is_known() && *alg != SigningAlg::None;
if allowed_algs.is_empty() || !allowed_algs.iter().all(usable) {
return Err(Error::AlgorithmNotAllowed);
}
Ok(Self {
allowed_algs,
detached_payload: None,
max_input_len: Self::DEFAULT_MAX_INPUT_LEN,
max_signatures: Self::DEFAULT_MAX_SIGNATURES,
})
}
pub fn detached_payload(mut self, payload: &'a [u8]) -> Self {
self.detached_payload = Some(payload);
self
}
pub fn max_input_len(mut self, max_input_len: usize) -> Self {
self.max_input_len = max_input_len;
self
}
pub fn max_signatures(mut self, max_signatures: usize) -> Self {
self.max_signatures = max_signatures;
self
}
pub fn allowed_algs(&self) -> &'a [SigningAlg] {
self.allowed_algs
}
}
impl fmt::Debug for VerifyOptions<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VerifyOptions")
.field("allowed_algs", &self.allowed_algs)
.field(
"detached_payload_len",
&self.detached_payload.map(<[u8]>::len),
)
.field("max_input_len", &self.max_input_len)
.field("max_signatures", &self.max_signatures)
.finish()
}
}
#[derive(Clone, PartialEq)]
#[non_exhaustive]
pub struct Verified {
pub alg: SigningAlg,
pub protected: Option<Header>,
pub unprotected: Option<Header>,
pub payload: Zeroizing<Vec<u8>>,
pub signature_index: usize,
}
impl fmt::Debug for Verified {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Verified")
.field("alg", &self.alg)
.field("protected", &self.protected)
.field("unprotected", &self.unprotected)
.field("payload_len", &self.payload.len())
.field("signature_index", &self.signature_index)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::Value;
const A1_PROTECTED: &str = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9";
const SECRET: &str = "MARKERsecretMARKERsecretMARKERsecretMARKERs";
const MARKER: &str = "MARKERsecret";
fn assert_no_marker<T: fmt::Debug>(value: &T) {
for debug in [format!("{value:?}"), format!("{value:#?}")] {
assert!(!debug.contains(MARKER), "Debug leaks: {debug}");
}
}
fn hs256_header() -> Header {
let mut h = Header::new();
h.alg = Some("HS256".into());
h
}
#[test]
fn protected_header_new_encodes_deterministic_json() {
let p = ProtectedHeader::new(hs256_header()).unwrap();
assert_eq!(p.encoded(), "eyJhbGciOiJIUzI1NiJ9");
assert_eq!(p.header(), &hs256_header());
}
#[test]
fn protected_header_from_encoded_keeps_the_octets() {
let p = ProtectedHeader::from_encoded(A1_PROTECTED).unwrap();
assert_eq!(p.encoded(), A1_PROTECTED);
assert_eq!(p.header().typ.as_deref(), Some("JWT"));
assert_eq!(p.header().signing_alg(), Ok(SigningAlg::HS256));
assert_ne!(
ProtectedHeader::new(p.header().clone()).unwrap().encoded(),
A1_PROTECTED
);
assert_eq!(p.into_header().alg.as_deref(), Some("HS256"));
}
#[test]
fn protected_header_from_encoded_errors() {
assert_eq!(
ProtectedHeader::from_encoded("eyJhbGciOiJIUzI1NiJ9="),
Err(Error::InvalidBase64)
);
assert_eq!(
ProtectedHeader::from_encoded("WzFd")
.map(drop)
.unwrap_err()
.code(),
"InvalidJson"
);
let dup = b64url_encode(br#"{"alg":"HS256","alg":"none"}"#);
assert_eq!(
ProtectedHeader::from_encoded(&dup)
.map(drop)
.unwrap_err()
.code(),
"InvalidJson"
);
let typed = b64url_encode(br#"{"alg":1}"#);
assert_eq!(
ProtectedHeader::from_encoded(&typed)
.map(drop)
.unwrap_err()
.code(),
"InvalidHeader"
);
}
#[test]
fn protected_header_debug_hides_the_encoding() {
let mut h = hs256_header();
let mut jwk = crate::json::Object::new();
jwk.insert("kty", "oct");
jwk.insert("k", SECRET);
h.jwk = Some(jwk);
let p = ProtectedHeader::new(h).unwrap();
assert_no_marker(&p);
assert!(!format!("{p:?}").contains(p.encoded()));
}
#[test]
fn sign_options_default_to_attached() {
assert_eq!(SignOptions::new(), SignOptions { detached: false });
assert!(SignOptions::new().detached(true).detached);
assert!(!SignOptions::new().detached(true).detached(false).detached);
}
#[test]
fn verify_options_reject_empty_none_and_unknown() {
let cases: [&[SigningAlg]; 5] = [
&[],
&[SigningAlg::None],
&[SigningAlg::HS256, SigningAlg::None],
&[SigningAlg::Unknown("HS1".into())],
&[SigningAlg::Unknown("HS256".into())],
];
for allowed in cases {
assert_eq!(
VerifyOptions::new(allowed).map(drop),
Err(Error::AlgorithmNotAllowed),
"{allowed:?}"
);
}
}
#[test]
fn verify_options_defaults_and_builders() {
let allowed = [SigningAlg::HS256, SigningAlg::EdDSA];
let o = VerifyOptions::new(&allowed).unwrap();
assert_eq!(o.allowed_algs(), &allowed);
assert_eq!(o.detached_payload, None);
assert_eq!(o.max_input_len, 1024 * 1024);
assert_eq!(o.max_signatures, 16);
let o = o
.detached_payload(b"abc")
.max_input_len(10)
.max_signatures(2);
assert_eq!(o.detached_payload, Some(&b"abc"[..]));
assert_eq!((o.max_input_len, o.max_signatures), (10, 2));
let all: Vec<SigningAlg> = SigningAlg::KNOWN
.iter()
.filter(|a| **a != SigningAlg::None)
.cloned()
.collect();
assert!(VerifyOptions::new(&all).is_ok());
}
#[test]
fn verify_options_debug_hides_the_detached_payload() {
let allowed = [SigningAlg::HS256];
let o = VerifyOptions::new(&allowed)
.unwrap()
.detached_payload(SECRET.as_bytes());
assert_no_marker(&o);
assert!(format!("{o:?}").contains("detached_payload_len: Some(43)"));
}
#[test]
fn signer_debug_redacts_key_material() {
let oct = Jwk::from_json(format!(r#"{{"kty":"oct","k":"{SECRET}"}}"#).as_bytes()).unwrap();
let ec = Jwk::from_json(
format!(
r#"{{"kty":"EC","crv":"P-256","x":"Qm0_Y3iAhrDndVQ247skVXv2bFfwnTvlLUyup22HGDM","y":"MXHhtkN4vHVS87SCP6IfVsIXSeUWHsOjnuQIgzhWL6Q","d":"{SECRET}"}}"#
)
.as_bytes(),
)
.unwrap();
let okp = Jwk::from_json(
format!(
r#"{{"kty":"OKP","crv":"Ed25519","x":"TrgjJgt_upZmFJb7d22T8iGZ_57PzgtlGh4Tq4b-tkc","d":"{SECRET}"}}"#
)
.as_bytes(),
)
.unwrap();
for key in [&oct, &ec, &okp] {
let mut unprotected = Header::new();
unprotected
.insert_other("x-secret", Value::from(SECRET))
.unwrap();
let signer = Signer::new(key)
.protected(ProtectedHeader::new(hs256_header()).unwrap())
.unprotected(unprotected);
assert_no_marker(&signer);
assert!(format!("{signer:?}").contains("<redacted>"));
}
}
#[test]
fn verified_payload_is_wiped_on_drop() {
fn is_zeroizing(_: &Zeroizing<Vec<u8>>) {}
let key = Jwk::from_json(format!(r#"{{"kty":"oct","k":"{SECRET}"}}"#).as_bytes()).unwrap();
let protected = ProtectedHeader::new(hs256_header()).unwrap();
let jws = compact::sign_compact(&protected, b"secret", &key, &SignOptions::new()).unwrap();
let allowed = [SigningAlg::HS256];
let opts = VerifyOptions::new(&allowed).unwrap();
for verified in [
compact::verify_compact(&jws, &key, &opts).unwrap(),
compact::verify_compact(
&jws.replace(".c2VjcmV0.", ".."),
&key,
&opts.detached_payload(b"secret"),
)
.unwrap(),
] {
is_zeroizing(&verified.payload);
assert_eq!(*verified.payload, b"secret");
}
}
#[test]
fn verified_debug_hides_the_payload() {
let mut protected = hs256_header();
protected
.insert_other("x-secret", Value::from(SECRET))
.unwrap();
let v = Verified {
alg: SigningAlg::HS256,
protected: Some(protected),
unprotected: None,
payload: Zeroizing::new(SECRET.as_bytes().to_vec()),
signature_index: 0,
};
assert_no_marker(&v);
assert!(format!("{v:?}").contains("payload_len: 43"));
}
}