Problem
RFC 7515 §4.1.11 requires a JWS recipient to reject the token if the header's crit list names any critical extension the recipient doesn't understand and process. OxyJWT already implements exactly this check — but only for the RFC 7797 detached-payload (b64: false) path, in jws::validate_rfc7797_header:
for param in crit {
match param_str {
"b64" => has_b64 = true,
other => return Err(format!("Unsupported critical header parameter in 'crit': '{other}'")),
}
}
The ordinary verified-decode path (api::verify_and_parse_impl, used by decode/decode_complete for every non-detached token) never looks at crit at all. A JWT with a signed header naming an unrecognized critical extension verifies and decodes successfully, contrary to the RFC:
header = {"alg": "HS256", "typ": "JWT", "crit": ["exp-strict-mode"], "exp-strict-mode": True}
# ... signed correctly with the real secret ...
oxyjwt.decode(token, secret, algorithms=["HS256"]) # succeeds; RFC 7515 says it must not
Confirmed by direct reproduction against the current dev branch.
This is not a signature-forgery vector (the header is inside the signed portion, so an attacker without the signing key can't add crit to someone else's token). The impact is protocol/interop-shaped: if an issuer signs tokens using crit to signal a security-relevant extension a relying party must enforce (some JOSE profiles do this), OxyJWT silently ignores the extension instead of refusing the token, and application code has no signal that something it doesn't understand was present.
Proposal
Generalize the existing crit-rejection logic (currently private to validate_rfc7797_header) into a small, reusable check — e.g. jws::reject_unknown_crit(header, understood: &[&str]) — and call it from verify_and_parse_impl for every verified decode, not just the detached-payload path. Since this library doesn't implement any JWS header extensions itself, the "understood" set is effectively just {"b64"} (only meaningful for the detached case); for an ordinary token, any crit entry at all should be rejected, matching the strictness the RFC recommends for implementations that don't support extensions.
Acceptance criteria
Files
rust/src/jws.rs, rust/src/api.rs
Branch
issue/<N>-general-crit-rejection from dev
Problem
RFC 7515 §4.1.11 requires a JWS recipient to reject the token if the header's
critlist names any critical extension the recipient doesn't understand and process. OxyJWT already implements exactly this check — but only for the RFC 7797 detached-payload (b64: false) path, injws::validate_rfc7797_header:The ordinary verified-decode path (
api::verify_and_parse_impl, used bydecode/decode_completefor every non-detached token) never looks atcritat all. A JWT with a signed header naming an unrecognized critical extension verifies and decodes successfully, contrary to the RFC:Confirmed by direct reproduction against the current
devbranch.This is not a signature-forgery vector (the header is inside the signed portion, so an attacker without the signing key can't add
critto someone else's token). The impact is protocol/interop-shaped: if an issuer signs tokens usingcritto signal a security-relevant extension a relying party must enforce (some JOSE profiles do this), OxyJWT silently ignores the extension instead of refusing the token, and application code has no signal that something it doesn't understand was present.Proposal
Generalize the existing crit-rejection logic (currently private to
validate_rfc7797_header) into a small, reusable check — e.g.jws::reject_unknown_crit(header, understood: &[&str])— and call it fromverify_and_parse_implfor every verified decode, not just the detached-payload path. Since this library doesn't implement any JWS header extensions itself, the "understood" set is effectively just{"b64"}(only meaningful for the detached case); for an ordinary token, anycritentry at all should be rejected, matching the strictness the RFC recommends for implementations that don't support extensions.Acceptance criteria
critentry is rejected bydecode/decode_complete(the reproduction above must start raising)crithandling (which additionally requiresb64to be listed) is preserved, sharing the underlying check rather than duplicating itcritheader, or an absent/empty one where already rejected today, is unaffectedFiles
rust/src/jws.rs,rust/src/api.rsBranch
issue/<N>-general-crit-rejectionfromdev