use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use serde::{de, Deserialize, Deserializer, Serialize};
use thiserror::Error;
use time::OffsetDateTime;
use zeroize::Zeroize;
use crate::spiffe_id::{SpiffeId, SpiffeIdError, TrustDomain};
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
use crate::bundle::jwt::{JwtAuthority, JwtBundle};
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
use crate::bundle::BundleSource;
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
use jsonwebtoken::{DecodingKey, Validation};
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum JwtAlg {
RS256,
RS384,
RS512,
ES256,
ES384,
ES512,
PS256,
PS384,
PS512,
}
impl JwtAlg {
fn parse(s: &str) -> Option<Self> {
Some(match s {
"RS256" => Self::RS256,
"RS384" => Self::RS384,
"RS512" => Self::RS512,
"ES256" => Self::ES256,
"ES384" => Self::ES384,
"ES512" => Self::ES512,
"PS256" => Self::PS256,
"PS384" => Self::PS384,
"PS512" => Self::PS512,
_ => return None,
})
}
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
const fn to_jsonwebtoken(self) -> Option<jsonwebtoken::Algorithm> {
match self {
Self::RS256 => Some(jsonwebtoken::Algorithm::RS256),
Self::RS384 => Some(jsonwebtoken::Algorithm::RS384),
Self::RS512 => Some(jsonwebtoken::Algorithm::RS512),
Self::ES256 => Some(jsonwebtoken::Algorithm::ES256),
Self::ES384 => Some(jsonwebtoken::Algorithm::ES384),
Self::ES512 => None,
Self::PS256 => Some(jsonwebtoken::Algorithm::PS256),
Self::PS384 => Some(jsonwebtoken::Algorithm::PS384),
Self::PS512 => Some(jsonwebtoken::Algorithm::PS512),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JwtSvid {
spiffe_id: SpiffeId,
hint: Option<Arc<str>>,
expiry: OffsetDateTime,
claims: Claims,
kid: String,
token: Token,
alg: JwtAlg,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
struct Header {
#[serde(default)]
kid: Option<String>,
#[serde(default)]
typ: Option<String>,
alg: String,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum JwtSvidError {
#[error("invalid spiffe_id in token 'sub' claim")]
InvalidSubject(#[from] SpiffeIdError),
#[error("token header 'kid' not found")]
MissingKeyId,
#[error("token header 'typ' should be 'JWT' or 'JOSE'")]
InvalidTyp,
#[error("invalid token expiration ('exp') claim")]
InvalidExpiration,
#[error("algorithm in 'alg' header is not supported")]
UnsupportedAlgorithm,
#[error("algorithm in 'alg' header is unsupported by offline verification backend: {0:?}")]
BackendUnsupportedAlgorithm(JwtAlg),
#[error("malformed jwt token: expected 3 dot-separated parts")]
InvalidJwtFormat,
#[error("malformed jwt token: invalid base64url encoding")]
InvalidBase64,
#[error("malformed jwt token: invalid json")]
InvalidJson(#[source] serde_json::Error),
#[error("cannot find JWT bundle for trust domain: {0}")]
BundleNotFound(TrustDomain),
#[error("cannot find JWT authority for key_id: {0}")]
AuthorityNotFound(String),
#[error("expected audience in {0:?} (audience={1:?})")]
InvalidAudience(Vec<String>, Vec<String>),
#[error("bundle source error")]
BundleSourceError(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("jwt offline verification not enabled (enable feature: jwt-verify-rust-crypto or jwt-verify-aws-lc-rs)")]
JwtVerifyNotEnabled,
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
#[error("cannot parse authority JWK JSON: {0}")]
InvalidAuthorityJwk(#[from] serde_json::Error),
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
#[error("cannot decode token")]
InvalidToken(#[from] jsonwebtoken::errors::Error),
}
impl From<std::convert::Infallible> for JwtSvidError {
fn from(never: std::convert::Infallible) -> Self {
match never {}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Zeroize)]
#[zeroize(drop)]
struct Token {
inner: String,
}
impl From<&str> for Token {
fn from(token: &str) -> Self {
Self {
inner: token.to_owned(),
}
}
}
impl AsRef<str> for Token {
fn as_ref(&self) -> &str {
self.inner.as_ref()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Claims {
sub: String,
#[serde(deserialize_with = "string_or_seq_string")]
aud: Vec<String>,
exp: i64,
}
impl Claims {
pub fn sub(&self) -> &str {
&self.sub
}
pub fn aud(&self) -> &[String] {
&self.aud
}
pub const fn exp(&self) -> i64 {
self.exp
}
}
impl JwtSvid {
pub fn from_workload_api_token(token: &str) -> Result<Self, JwtSvidError> {
Self::parse_insecure(token)
}
pub fn parse_insecure(token: &str) -> Result<Self, JwtSvidError> {
Self::from_str(token)
}
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
pub fn parse_and_validate<B, T>(
token: &str,
bundle_source: &B,
expected_audience: &[T],
) -> Result<Self, JwtSvidError>
where
B: BundleSource<Item = JwtBundle>,
B::Error: std::error::Error + Send + Sync + 'static,
T: AsRef<str> + fmt::Debug,
{
use jsonwebtoken::jwk::Jwk;
let untrusted = Self::parse_insecure(token)?;
let jw_alg = untrusted
.alg
.to_jsonwebtoken()
.ok_or(JwtSvidError::BackendUnsupportedAlgorithm(untrusted.alg))?;
let jwt_authority = Self::find_jwt_authority(
bundle_source,
untrusted.spiffe_id.trust_domain(),
&untrusted.kid,
)?;
let mut validation = Validation::new(jw_alg);
validation.validate_exp = true;
validation.leeway = 0;
let aud: Vec<&str> = expected_audience.iter().map(AsRef::as_ref).collect();
validation.set_audience(&aud);
let jwk: Jwk = serde_json::from_slice(jwt_authority.jwk_json())?;
let dec_key = DecodingKey::from_jwk(&jwk)?;
jsonwebtoken::decode::<Claims>(token, &dec_key, &validation)?;
Ok(untrusted)
}
#[must_use]
pub fn with_hint(mut self, hint: impl Into<Arc<str>>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn token(&self) -> &str {
self.token.as_ref()
}
pub const fn spiffe_id(&self) -> &SpiffeId {
&self.spiffe_id
}
pub fn audience(&self) -> &[String] {
&self.claims.aud
}
pub const fn expiry(&self) -> OffsetDateTime {
self.expiry
}
pub fn key_id(&self) -> &str {
&self.kid
}
pub const fn claims(&self) -> &Claims {
&self.claims
}
pub fn hint(&self) -> Option<&str> {
self.hint.as_deref()
}
#[cfg(any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs"))]
fn find_jwt_authority<B>(
bundle_source: &B,
trust_domain: &TrustDomain,
key_id: &str,
) -> Result<Arc<JwtAuthority>, JwtSvidError>
where
B: BundleSource<Item = JwtBundle>,
B::Error: std::error::Error + Send + Sync + 'static,
{
let bundle = bundle_source
.bundle_for_trust_domain(trust_domain)
.map_err(|e| JwtSvidError::BundleSourceError(Box::new(e)))?
.ok_or_else(|| JwtSvidError::BundleNotFound(trust_domain.clone()))?;
bundle
.find_jwt_authority(key_id) .cloned() .ok_or_else(|| JwtSvidError::AuthorityNotFound(key_id.to_owned()))
}
}
impl FromStr for JwtSvid {
type Err = JwtSvidError;
fn from_str(token: &str) -> Result<Self, Self::Err> {
let mut it = token.split('.');
let header_b64 = it.next().ok_or(JwtSvidError::InvalidJwtFormat)?;
let claims_b64 = it.next().ok_or(JwtSvidError::InvalidJwtFormat)?;
let _sig_b64 = it.next().ok_or(JwtSvidError::InvalidJwtFormat)?;
if it.next().is_some() {
return Err(JwtSvidError::InvalidJwtFormat);
}
let header_json = decode_b64url_to_vec(header_b64)?;
let claims_json = decode_b64url_to_vec(claims_b64)?;
let header: Header =
serde_json::from_slice(&header_json).map_err(JwtSvidError::InvalidJson)?;
let claims: Claims =
serde_json::from_slice(&claims_json).map_err(JwtSvidError::InvalidJson)?;
if let Some(t) = header.typ.as_deref() {
match t {
"JWT" | "JOSE" => {}
_ => return Err(JwtSvidError::InvalidTyp),
}
}
let alg = JwtAlg::parse(header.alg.as_str()).ok_or(JwtSvidError::UnsupportedAlgorithm)?;
let kid = header.kid.ok_or(JwtSvidError::MissingKeyId)?;
let spiffe_id = SpiffeId::from_str(&claims.sub)?;
let expiry = OffsetDateTime::from_unix_timestamp(claims.exp)
.map_err(|time::error::ComponentRange { .. }| JwtSvidError::InvalidExpiration)?;
Ok(Self {
spiffe_id,
hint: None,
expiry,
claims,
kid,
alg,
token: Token::from(token),
})
}
}
const MAX_JWT_AUDIENCE_COUNT: usize = 32;
fn string_or_seq_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrVec(PhantomData<Vec<String>>);
impl<'de> de::Visitor<'de> for StringOrVec {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("string or sequence of strings")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![v.to_owned()])
}
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut result = Vec::new();
while let Some(elem) = seq.next_element::<String>()? {
if result.len() >= MAX_JWT_AUDIENCE_COUNT {
return Err(de::Error::custom(format!(
"JWT `aud` claim has too many entries (max {MAX_JWT_AUDIENCE_COUNT})"
)));
}
result.push(elem);
}
Ok(result)
}
}
deserializer.deserialize_any(StringOrVec(PhantomData))
}
const MAX_JWT_SEGMENT_SIZE: usize = 64 * 1024;
fn decode_b64url_to_vec(input: &str) -> Result<Vec<u8>, JwtSvidError> {
use base64ct::{Base64UrlUnpadded, Encoding as _};
if input.len() > MAX_JWT_SEGMENT_SIZE * 4 / 3 {
return Err(JwtSvidError::InvalidBase64);
}
let mut buf = vec![0u8; input.len()];
let len = Base64UrlUnpadded::decode(input, &mut buf)
.map_err(|err| {
match err {
base64ct::Error::InvalidLength | base64ct::Error::InvalidEncoding => {}
}
JwtSvidError::InvalidBase64
})?
.len();
if len > MAX_JWT_SEGMENT_SIZE {
return Err(JwtSvidError::InvalidBase64);
}
buf.truncate(len);
Ok(buf)
}
#[cfg(test)]
fn mk_token(header_json: &str, claims_json: &str) -> String {
use base64ct::{Base64UrlUnpadded, Encoding as _};
let h = Base64UrlUnpadded::encode_string(header_json.as_bytes());
let c = Base64UrlUnpadded::encode_string(claims_json.as_bytes());
format!("{h}.{c}.sig")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_insecure_ok_with_aud_string() {
let token = mk_token(
r#"{"alg":"ES256","kid":"k1","typ":"JWT"}"#,
r#"{"sub":"spiffe://example.org/service","aud":"aud1","exp":4294967295}"#,
);
let svid = JwtSvid::parse_insecure(&token).unwrap();
assert_eq!(svid.spiffe_id().to_string(), "spiffe://example.org/service");
assert_eq!(svid.key_id(), "k1");
assert_eq!(svid.audience(), &["aud1".to_string()]);
assert_eq!(svid.token(), token);
}
#[test]
fn parse_insecure_ok_with_aud_array() {
let token = mk_token(
r#"{"alg":"RS256","kid":"k1"}"#,
r#"{"sub":"spiffe://example.org/service","aud":["a","b"],"exp":4294967295}"#,
);
let svid = JwtSvid::parse_insecure(&token).unwrap();
assert_eq!(svid.audience(), &["a".to_string(), "b".to_string()]);
}
#[test]
fn parse_insecure_rejects_missing_kid() {
let token = mk_token(
r#"{"alg":"ES256"}"#,
r#"{"sub":"spiffe://example.org/service","aud":"aud1","exp":4294967295}"#,
);
let err = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(err, JwtSvidError::MissingKeyId));
}
#[test]
fn parse_insecure_rejects_invalid_typ() {
let token = mk_token(
r#"{"alg":"ES256","kid":"k1","typ":"NOPE"}"#,
r#"{"sub":"spiffe://example.org/service","aud":"aud1","exp":4294967295}"#,
);
let err = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(err, JwtSvidError::InvalidTyp));
}
#[test]
fn parse_insecure_rejects_unsupported_alg() {
let token = mk_token(
r#"{"alg":"HS256","kid":"k1"}"#,
r#"{"sub":"spiffe://example.org/service","aud":"aud1","exp":4294967295}"#,
);
let err = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(err, JwtSvidError::UnsupportedAlgorithm));
}
#[test]
fn parse_insecure_accepts_es512_alg() {
let token = mk_token(
r#"{"alg":"ES512","kid":"k1"}"#,
r#"{"sub":"spiffe://example.org/service","aud":"aud1","exp":4294967295}"#,
);
let svid = JwtSvid::parse_insecure(&token).unwrap();
assert_eq!(svid.alg, JwtAlg::ES512);
}
#[test]
fn parse_insecure_rejects_bad_format() {
let err = JwtSvid::parse_insecure("a.b").unwrap_err();
assert!(matches!(err, JwtSvidError::InvalidJwtFormat));
}
#[test]
fn parse_insecure_rejects_bad_base64() {
let err = JwtSvid::parse_insecure("!!!.!!!.sig").unwrap_err();
assert!(matches!(err, JwtSvidError::InvalidBase64));
}
#[test]
fn parse_insecure_rejects_invalid_json() {
let token = mk_token(
r#"{"alg":"ES256","kid":"k1"}"#,
r#"{"sub":,"aud":"aud1","exp":4294967295}"#, );
let err = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(err, JwtSvidError::InvalidJson(_)));
}
#[test]
fn parse_insecure_rejects_invalid_sub() {
let token = mk_token(
r#"{"alg":"ES256","kid":"k1"}"#,
r#"{"sub":"not-a-spiffe-id","aud":"aud1","exp":4294967295}"#,
);
let err = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(err, JwtSvidError::InvalidSubject(_)));
}
#[test]
fn parse_insecure_rejects_invalid_exp() {
let token = mk_token(
r#"{"alg":"ES256","kid":"k1"}"#,
r#"{"sub":"spiffe://example.org/service","aud":"aud1","exp":"nope"}"#,
);
let err = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(err, JwtSvidError::InvalidJson(_)));
}
}
#[expect(
clippy::expect_used,
clippy::tests_outside_test_module,
clippy::unwrap_used,
reason = "https://github.com/rust-lang/rust-clippy/issues/16476"
)]
#[cfg(all(
test,
any(feature = "jwt-verify-rust-crypto", feature = "jwt-verify-aws-lc-rs")
))]
mod test {
use super::*;
use crate::bundle::jwt::JwtBundleSet;
use base64ct::{Base64UrlUnpadded, Encoding as _};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use p256::ecdsa::SigningKey;
use p256::elliptic_curve::pkcs8::EncodePrivateKey as _;
use p256::elliptic_curve::rand_core::OsRng;
use std::time::{SystemTime, UNIX_EPOCH};
fn b64u(data: &[u8]) -> String {
Base64UrlUnpadded::encode_string(data)
}
fn make_es256_public_jwk_json(signing_key: &SigningKey, kid: &str) -> Vec<u8> {
let verifying_key = signing_key.verifying_key();
let point = verifying_key.to_encoded_point(false);
let x = point.x().expect("x coordinate missing");
let y = point.y().expect("y coordinate missing");
serde_json::to_vec(&serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": b64u(x),
"y": b64u(y),
"alg": "ES256",
"use": "sig",
"kid": kid,
}))
.expect("JWK should serialize")
}
fn new_es256_authority_and_encoding_key(kid: &str) -> (JwtAuthority, EncodingKey) {
let signing_key = SigningKey::random(&mut OsRng);
let pkcs8_der = signing_key
.to_pkcs8_der()
.expect("pkcs8 der should serialize");
let encoding_key = EncodingKey::from_ec_der(pkcs8_der.as_bytes());
let jwk_json = make_es256_public_jwk_json(&signing_key, kid);
let authority =
JwtAuthority::from_jwk_json(&jwk_json).expect("authority should parse from JWK JSON");
(authority, encoding_key)
}
#[test]
fn test_parse_and_validate_jwt_svid() {
let test_key_id = "test-key-id";
let (authority, encoding_key) = new_es256_authority_and_encoding_key(test_key_id);
let target_audience = vec!["audience".to_owned()];
let token = generate_token(
target_audience.clone(),
"spiffe://example.org/service".to_string(),
Some("JWT".to_string()),
Some(test_key_id.to_string()),
0xFFFF_FFFF,
Algorithm::ES256,
&encoding_key,
);
let mut bundle_source = JwtBundleSet::default();
let trust_domain = TrustDomain::new("example.org").unwrap();
let mut bundle = JwtBundle::new(trust_domain);
bundle.add_jwt_authority(authority);
bundle_source.add_bundle(bundle);
let jwt_svid = JwtSvid::parse_and_validate(&token, &bundle_source, &["audience"]).unwrap();
assert_eq!(
jwt_svid.spiffe_id,
SpiffeId::new("spiffe://example.org/service").unwrap()
);
assert_eq!(jwt_svid.audience(), &target_audience);
assert_eq!(jwt_svid.token(), token);
}
#[test]
fn test_parse_jwt_svid_with_unsupported_algorithm() {
let target_audience = vec!["audience".to_owned()];
let token = generate_token(
target_audience,
"spiffe://example.org/service".to_string(),
Some("JWT".to_string()),
Some("some_key_id".to_string()),
0xFFFF_FFFF,
Algorithm::HS256,
&EncodingKey::from_secret("secret".as_ref()),
);
let result = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(result, JwtSvidError::UnsupportedAlgorithm));
}
#[test]
fn test_parse_and_validate_es512_fails_before_bundle_lookup() {
let token = mk_token(
r#"{"alg":"ES512","kid":"some_key_id"}"#,
r#"{"sub":"spiffe://example.org/service","aud":"audience","exp":4294967295}"#,
);
let bundle_source = JwtBundleSet::default();
let result =
JwtSvid::parse_and_validate(&token, &bundle_source, &["audience"]).unwrap_err();
assert!(matches!(
result,
JwtSvidError::BackendUnsupportedAlgorithm(JwtAlg::ES512)
));
}
#[test]
fn test_parse_invalid_jwt_svid_without_key_id() {
let (_authority, encoding_key) = new_es256_authority_and_encoding_key("ignored-kid");
let target_audience = vec!["audience".to_owned()];
let token = generate_token(
target_audience,
"spiffe://example.org/service".to_string(),
Some("JWT".to_string()),
None, 0xFFFF_FFFF,
Algorithm::ES256,
&encoding_key,
);
let result = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(result, JwtSvidError::MissingKeyId));
}
#[test]
fn test_parse_invalid_jwt_svid_with_invalid_header_typ() {
let (_authority, encoding_key) = new_es256_authority_and_encoding_key("ignored-kid");
let target_audience = vec!["audience".to_owned()];
let token = generate_token(
target_audience,
"spiffe://example.org/service".to_string(),
Some("OTHER".to_string()), Some("kid".to_string()),
0xFFFF_FFFF,
Algorithm::ES256,
&encoding_key,
);
let result = JwtSvid::parse_insecure(&token).unwrap_err();
assert!(matches!(result, JwtSvidError::InvalidTyp));
}
#[test]
fn test_parse_and_validate_jwt_svid_from_expired_token() {
let test_key_id = "test-key-id";
let (authority, encoding_key) = new_es256_authority_and_encoding_key(test_key_id);
let target_audience = vec!["audience".to_owned()];
let token = generate_token(
target_audience,
"spiffe://example.org/service".to_string(),
Some("JWT".to_string()),
Some(test_key_id.to_string()),
1, Algorithm::ES256,
&encoding_key,
);
let mut bundle_source = JwtBundleSet::default();
let trust_domain = TrustDomain::new("example.org").unwrap();
let mut bundle = JwtBundle::new(trust_domain);
bundle.add_jwt_authority(authority);
bundle_source.add_bundle(bundle);
let result =
JwtSvid::parse_and_validate(&token, &bundle_source, &["audience"]).unwrap_err();
assert!(matches!(result, JwtSvidError::InvalidToken(_)));
}
fn generate_token(
aud: Vec<String>,
sub: String,
typ: Option<String>,
kid: Option<String>,
exp: i64,
alg: Algorithm,
encoding_key: &EncodingKey,
) -> String {
let claims = Claims { sub, aud, exp };
let mut header = Header::new(alg);
header.typ = typ;
header.kid = kid;
encode(&header, &claims, encoding_key).unwrap()
}
fn make_es256_public_jwk_json_with_use_jwt_svid(
signing_key: &SigningKey,
kid: &str,
) -> Vec<u8> {
let verifying_key = signing_key.verifying_key();
let point = verifying_key.to_encoded_point(false);
let x = point.x().expect("x coordinate missing");
let y = point.y().expect("y coordinate missing");
serde_json::to_vec(&serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": b64u(x),
"y": b64u(y),
"alg": "ES256",
"use": "jwt-svid",
"kid": kid,
}))
.expect("JWK should serialize")
}
#[test]
fn test_accepts_jwk_with_use_jwt_svid() {
let kid = "test-key-id-jwt-svid";
let signing_key = SigningKey::random(&mut OsRng);
let pkcs8_der = signing_key
.to_pkcs8_der()
.expect("PKCS#8 DER serialization should succeed");
let encoding_key = EncodingKey::from_ec_der(pkcs8_der.as_bytes());
let jwk_json = make_es256_public_jwk_json_with_use_jwt_svid(&signing_key, kid);
let authority = JwtAuthority::from_jwk_json(&jwk_json)
.expect("issue regression: should accept JWK with use=jwt-svid");
let trust_domain = TrustDomain::new("example.org").expect("valid trust domain");
let mut bundle = JwtBundle::new(trust_domain);
bundle.add_jwt_authority(authority);
let mut bundle_set = JwtBundleSet::default();
bundle_set.add_bundle(bundle);
let target_audience = vec!["audience".to_owned()];
let exp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_secs()
+ 300;
let token = generate_token(
target_audience.clone(),
"spiffe://example.org/workload".to_string(),
None, Some(kid.to_string()), exp.try_into().unwrap(),
Algorithm::ES256,
&encoding_key,
);
let svid = JwtSvid::parse_and_validate(&token, &bundle_set, &["audience"])
.expect("issue regression: JWT-SVID signed by use=jwt-svid key should validate");
assert_eq!(
svid.spiffe_id().to_string(),
"spiffe://example.org/workload"
);
assert_eq!(svid.audience(), &target_audience);
}
#[test]
fn test_jwt_audience_claim_size_limit() {
let kid = "test-key-id";
let signing_key = SigningKey::random(&mut OsRng);
let pkcs8_der = signing_key
.to_pkcs8_der()
.expect("PKCS#8 DER serialization should succeed");
let encoding_key = EncodingKey::from_ec_der(pkcs8_der.as_bytes());
let jwk_json = make_es256_public_jwk_json_with_use_jwt_svid(&signing_key, kid);
let authority = JwtAuthority::from_jwk_json(&jwk_json).expect("valid JWK JSON");
let trust_domain = TrustDomain::new("example.org").expect("valid trust domain");
let mut bundle = JwtBundle::new(trust_domain);
bundle.add_jwt_authority(authority);
let mut bundle_set = JwtBundleSet::default();
bundle_set.add_bundle(bundle);
let exp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_secs()
.saturating_add(60)
.try_into()
.unwrap();
let excessive_audiences: Vec<String> = (0..33).map(|i| format!("aud{i}")).collect();
let oversized_token = generate_token(
excessive_audiences,
"spiffe://example.org/workload".to_string(),
None,
Some(kid.to_string()),
exp,
Algorithm::ES256,
&encoding_key,
);
let result = JwtSvid::parse_and_validate(&oversized_token, &bundle_set, &["aud0"]);
assert!(
matches!(result, Err(JwtSvidError::InvalidJson(_))),
"should reject token with excessive `aud` claim array size during deserialization"
);
let result_insecure = JwtSvid::parse_insecure(&oversized_token);
assert!(
matches!(result_insecure, Err(JwtSvidError::InvalidJson(_))),
"parse_insecure should reject oversized `aud` claim during deserialization"
);
let matching_audience = "expected50".to_string();
let matching_token_audiences = vec![matching_audience];
let matching_token = generate_token(
matching_token_audiences,
"spiffe://example.org/workload".to_string(),
None,
Some(kid.to_string()),
exp,
Algorithm::ES256,
&encoding_key,
);
let large_expected_audiences: Vec<String> =
(0..100).map(|i| format!("expected{i}")).collect();
let result =
JwtSvid::parse_and_validate(&matching_token, &bundle_set, &large_expected_audiences);
assert!(
result.is_ok(),
"large expected_audience array should be accepted when audiences match"
);
}
}