1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
10use ring::{
11 rand::{SecureRandom, SystemRandom},
12 signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED},
13};
14use serde::{
15 de::{DeserializeSeed, Error as _, MapAccess, SeqAccess, Visitor},
16 Deserialize, Deserializer, Serialize,
17};
18use sha2::{Digest, Sha256};
19use std::fmt;
20use thiserror::Error;
21use zeroize::Zeroizing;
22
23#[cfg(feature = "jwt-target-acceptance-review")]
24use crate::jwt_target_acceptance::{
25 JwtTargetAcceptanceBinding, JwtTargetAcceptanceNotEligibleReason,
26};
27
28pub const JWT_POLICY_REVIEW_POLICY_ID: &str = "termivar.jwt-local-policy/es256-v1";
30pub const JWT_POLICY_REVIEW_AUDIT_SCHEMA: &str = "security.jwt-policy-review-audit/v1";
32pub const MAX_COMPACT_JWT_BYTES: usize = 4 * 1024;
34pub const MAX_LOCAL_PUBLIC_JWK_BYTES: usize = 2 * 1024;
36pub const MAX_JWT_HEADER_BYTES: usize = 1024;
38pub const MAX_JWT_CLAIMS_BYTES: usize = 3 * 1024;
40pub const ES256_SIGNATURE_BYTES: usize = 64;
42pub const MAX_REQUIRED_CLAIMS: usize = 16;
44pub const MAX_CLOCK_SKEW_SECONDS: u32 = 3600;
46
47const MAX_EXPECTED_TYPE_BYTES: usize = 128;
48const MAX_EXPECTED_STRING_BYTES: usize = 512;
49const MAX_CLAIM_NAME_BYTES: usize = 128;
50const MAX_POLICY_REFERENCE_BYTES: usize = 64;
51const MAX_JSON_DEPTH: usize = 8;
52const MAX_JSON_NODES: usize = 256;
53const MAX_JSON_MEMBERS: usize = 128;
54const MAX_JSON_ARRAY_ITEMS: usize = 64;
55const MAX_JSON_STRING_BYTES: usize = 1024;
56const JSON_DUPLICATE_SENTINEL: &str = "termivar-jwt-json-duplicate";
57const JSON_LIMIT_SENTINEL: &str = "termivar-jwt-json-limit";
58
59pub struct SecretCompactJwt {
62 bytes: Zeroizing<Vec<u8>>,
63}
64
65impl SecretCompactJwt {
66 pub fn new(bytes: Vec<u8>) -> Result<Self, JwtInputError> {
68 let bytes = Zeroizing::new(bytes);
69 if bytes.is_empty() {
70 return Err(JwtInputError::EmptyToken);
71 }
72 if bytes.len() > MAX_COMPACT_JWT_BYTES {
73 return Err(JwtInputError::TokenTooLarge);
74 }
75 Ok(Self { bytes })
76 }
77
78 fn as_bytes(&self) -> &[u8] {
79 self.bytes.as_slice()
80 }
81}
82
83#[cfg(feature = "jwt-target-acceptance-review")]
92pub(crate) struct JwtTargetAcceptanceCredentials {
93 valid: SecretCompactJwt,
94 invalid_signature_control: SecretCompactJwt,
95 preparation_binding: JwtTargetAcceptanceBinding,
96}
97
98#[cfg(feature = "jwt-target-acceptance-review")]
99impl JwtTargetAcceptanceCredentials {
100 pub(crate) fn valid_bytes(&self) -> &[u8] {
101 self.valid.as_bytes()
102 }
103
104 pub(crate) fn invalid_signature_control_bytes(&self) -> &[u8] {
105 self.invalid_signature_control.as_bytes()
106 }
107
108 pub(crate) const fn preparation_binding(&self) -> &JwtTargetAcceptanceBinding {
109 &self.preparation_binding
110 }
111
112 #[cfg(test)]
113 pub(crate) fn synthetic_for_test() -> Self {
114 Self {
115 valid: SecretCompactJwt::new(b"eyJhbGciOiJFUzI1NiJ9.e30.AA".to_vec()).unwrap(),
116 invalid_signature_control: SecretCompactJwt::new(
117 b"eyJhbGciOiJFUzI1NiJ9.e30.AQ".to_vec(),
118 )
119 .unwrap(),
120 preparation_binding: JwtTargetAcceptanceBinding::synthetic_for_test(0xA5),
121 }
122 }
123}
124
125#[cfg(feature = "jwt-target-acceptance-review")]
130pub struct JwtTargetAcceptanceRuntimeInput {
131 credentials: Option<JwtTargetAcceptanceCredentials>,
132 not_eligible_reason: Option<JwtTargetAcceptanceNotEligibleReason>,
133 preparation_binding: JwtTargetAcceptanceBinding,
134}
135
136#[cfg(feature = "jwt-target-acceptance-review")]
137impl JwtTargetAcceptanceRuntimeInput {
138 pub(crate) fn into_parts(
139 self,
140 ) -> (
141 Option<JwtTargetAcceptanceCredentials>,
142 Option<JwtTargetAcceptanceNotEligibleReason>,
143 JwtTargetAcceptanceBinding,
144 ) {
145 (
146 self.credentials,
147 self.not_eligible_reason,
148 self.preparation_binding,
149 )
150 }
151
152 #[cfg(test)]
153 pub(crate) fn synthetic_eligible_for_test() -> Self {
154 let credentials = JwtTargetAcceptanceCredentials::synthetic_for_test();
155 let preparation_binding = credentials.preparation_binding().clone();
156 Self {
157 credentials: Some(credentials),
158 not_eligible_reason: None,
159 preparation_binding,
160 }
161 }
162}
163
164#[cfg(feature = "jwt-target-acceptance-review")]
165impl fmt::Debug for JwtTargetAcceptanceRuntimeInput {
166 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167 formatter
168 .debug_struct("JwtTargetAcceptanceRuntimeInput")
169 .field(
170 "credentials",
171 &self.credentials.as_ref().map(|_| "<redacted>"),
172 )
173 .field("not_eligible_reason", &self.not_eligible_reason)
174 .field("preparation_binding", &"<opaque-per-preparation>")
175 .finish()
176 }
177}
178
179#[cfg(feature = "jwt-target-acceptance-review")]
183pub struct PreparedJwtTargetAcceptance {
184 local_audit: JwtPolicyReviewAudit,
185 runtime_input: JwtTargetAcceptanceRuntimeInput,
186}
187
188#[cfg(feature = "jwt-target-acceptance-review")]
189impl PreparedJwtTargetAcceptance {
190 pub fn local_audit(&self) -> &JwtPolicyReviewAudit {
191 &self.local_audit
192 }
193
194 pub fn into_parts(self) -> (JwtPolicyReviewAudit, JwtTargetAcceptanceRuntimeInput) {
195 (self.local_audit, self.runtime_input)
196 }
197}
198
199#[cfg(feature = "jwt-target-acceptance-review")]
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
203pub enum JwtTargetControlPreparationError {
204 #[error("JWT target control preparation could not mint its private binding")]
207 PreparationBindingUnavailable,
208 #[error("JWT target control preparation violated the compact-token contract")]
211 InvalidCompactToken,
212 #[error("JWT target control preparation violated the invalid-signature contract")]
215 InvalidControlInvariant,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
220#[non_exhaustive]
221pub enum JwtInputError {
222 #[error("JWT input is empty")]
224 EmptyToken,
225 #[error("JWT input exceeds its compiled byte limit")]
227 TokenTooLarge,
228}
229
230pub struct Es256LocalPublicKey {
235 sec1_public_point: [u8; 65],
236 public_key_sha256: String,
237}
238
239impl Es256LocalPublicKey {
240 pub fn from_jwk_json(bytes: Vec<u8>) -> Result<Self, Es256PublicJwkError> {
250 let bytes = Zeroizing::new(bytes);
254 if bytes.is_empty() {
255 return Err(Es256PublicJwkError::Malformed);
256 }
257 if bytes.len() > MAX_LOCAL_PUBLIC_JWK_BYTES {
258 return Err(Es256PublicJwkError::TooLarge);
259 }
260
261 let document = parse_private_json(bytes.as_slice()).map_err(|failure| match failure {
262 JsonFailure::DuplicateKey => Es256PublicJwkError::DuplicateKey,
263 JsonFailure::LimitExceeded => Es256PublicJwkError::LimitExceeded,
264 JsonFailure::Malformed => Es256PublicJwkError::Malformed,
265 })?;
266 let object = document.as_object().ok_or(Es256PublicJwkError::Malformed)?;
267
268 for (name, _) in object {
269 match name.as_str() {
270 "kty" | "crv" | "alg" | "x" | "y" => {},
271 "d" => return Err(Es256PublicJwkError::PrivateMaterialProhibited),
272 "jku" | "x5u" | "jwk" | "x5c" => {
273 return Err(Es256PublicJwkError::KeyMetadataProhibited)
274 },
275 _ => return Err(Es256PublicJwkError::UnsupportedMember),
276 }
277 }
278 if object.len() != 5 {
279 return Err(Es256PublicJwkError::MissingMember);
280 }
281 if document.string_member("kty") != Some("EC")
282 || document.string_member("crv") != Some("P-256")
283 || document.string_member("alg") != Some("ES256")
284 {
285 return Err(Es256PublicJwkError::UnsupportedKey);
286 }
287
288 let x = decode_canonical_coordinate(
289 document
290 .string_member("x")
291 .ok_or(Es256PublicJwkError::InvalidCoordinate)?,
292 )?;
293 let y = decode_canonical_coordinate(
294 document
295 .string_member("y")
296 .ok_or(Es256PublicJwkError::InvalidCoordinate)?,
297 )?;
298 let mut sec1_public_point = [0_u8; 65];
299 sec1_public_point[0] = 0x04;
300 sec1_public_point[1..33].copy_from_slice(&x);
301 sec1_public_point[33..].copy_from_slice(&y);
302 let public_key_sha256 = sha256_reference(&sec1_public_point);
303 Ok(Self {
304 sec1_public_point,
305 public_key_sha256,
306 })
307 }
308
309 fn as_sec1_bytes(&self) -> &[u8] {
310 &self.sec1_public_point
311 }
312
313 fn public_key_sha256(&self) -> &str {
314 &self.public_key_sha256
315 }
316}
317
318fn sha256_reference(bytes: &[u8]) -> String {
319 const HEX: &[u8; 16] = b"0123456789abcdef";
320 let digest = Sha256::digest(bytes);
321 let mut output = String::with_capacity("sha256:".len() + digest.len() * 2);
322 output.push_str("sha256:");
323 for byte in digest {
324 output.push(char::from(HEX[usize::from(byte >> 4)]));
325 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
326 }
327 output
328}
329
330fn decode_canonical_coordinate(value: &str) -> Result<[u8; 32], Es256PublicJwkError> {
331 if !is_unpadded_base64url(value.as_bytes()) {
332 return Err(Es256PublicJwkError::InvalidCoordinate);
333 }
334 let decoded = Zeroizing::new(
335 URL_SAFE_NO_PAD
336 .decode(value.as_bytes())
337 .map_err(|_| Es256PublicJwkError::InvalidCoordinate)?,
338 );
339 let coordinate: [u8; 32] = decoded
340 .as_slice()
341 .try_into()
342 .map_err(|_| Es256PublicJwkError::InvalidCoordinate)?;
343 if URL_SAFE_NO_PAD.encode(coordinate) != value {
344 return Err(Es256PublicJwkError::InvalidCoordinate);
345 }
346 Ok(coordinate)
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
351#[non_exhaustive]
352pub enum Es256PublicJwkError {
353 #[error("local ES256 public JWK exceeds its compiled byte limit")]
355 TooLarge,
356 #[error("local ES256 public JWK is malformed")]
358 Malformed,
359 #[error("local ES256 public JWK contains a duplicate JSON key")]
361 DuplicateKey,
362 #[error("local ES256 public JWK exceeds JSON structural limits")]
364 LimitExceeded,
365 #[error("local ES256 public JWK is missing a required member")]
367 MissingMember,
368 #[error("local ES256 public JWK contains an unsupported member")]
370 UnsupportedMember,
371 #[error("local ES256 public JWK must not contain private key material")]
373 PrivateMaterialProhibited,
374 #[error("local ES256 public JWK must not contain key-selection metadata")]
376 KeyMetadataProhibited,
377 #[error("local public JWK is not an ES256 P-256 verification key")]
379 UnsupportedKey,
380 #[error("local ES256 public JWK coordinate is invalid")]
382 InvalidCoordinate,
383}
384
385pub struct JwtLocalPolicy {
388 operator_policy_reference: String,
389 operator_policy_revision: String,
390 expected_type: Zeroizing<String>,
391 expected_issuer: Zeroizing<String>,
392 expected_audience: Zeroizing<String>,
393 required_claims: Vec<Zeroizing<String>>,
394 require_expiration: bool,
395 allowed_clock_skew_seconds: u32,
396}
397
398impl JwtLocalPolicy {
399 pub fn new(
402 operator_policy_identity: (&str, &str),
403 expected_type: &str,
404 expected_issuer: &str,
405 expected_audience: &str,
406 required_claims: &[&str],
407 require_expiration: bool,
408 allowed_clock_skew_seconds: u32,
409 ) -> Result<Self, JwtPolicyError> {
410 let (operator_policy_reference, operator_policy_revision) = operator_policy_identity;
411 if !valid_policy_reference(operator_policy_reference) {
412 return Err(JwtPolicyError::InvalidPolicyReference);
413 }
414 if !valid_policy_reference(operator_policy_revision) {
415 return Err(JwtPolicyError::InvalidPolicyRevision);
416 }
417 if allowed_clock_skew_seconds > MAX_CLOCK_SKEW_SECONDS {
418 return Err(JwtPolicyError::ClockSkewTooLarge);
419 }
420 let expected_type = copy_expected(expected_type, MAX_EXPECTED_TYPE_BYTES)?;
421 let expected_issuer = copy_expected(expected_issuer, MAX_EXPECTED_STRING_BYTES)?;
422 let expected_audience = copy_expected(expected_audience, MAX_EXPECTED_STRING_BYTES)?;
423 if required_claims.len() > MAX_REQUIRED_CLAIMS {
424 return Err(JwtPolicyError::TooManyRequiredClaims);
425 }
426 let mut copied_claims: Vec<Zeroizing<String>> = Vec::with_capacity(required_claims.len());
427 for claim in required_claims {
428 if !valid_claim_name(claim) {
429 return Err(JwtPolicyError::InvalidRequiredClaim);
430 }
431 if copied_claims.iter().any(|known| known.as_str() == *claim) {
432 return Err(JwtPolicyError::DuplicateRequiredClaim);
433 }
434 copied_claims.push(Zeroizing::new((*claim).to_owned()));
435 }
436 Ok(Self {
437 operator_policy_reference: operator_policy_reference.to_owned(),
438 operator_policy_revision: operator_policy_revision.to_owned(),
439 expected_type,
440 expected_issuer,
441 expected_audience,
442 required_claims: copied_claims,
443 require_expiration,
444 allowed_clock_skew_seconds,
445 })
446 }
447}
448
449fn valid_policy_reference(value: &str) -> bool {
450 !value.is_empty()
451 && value.len() <= MAX_POLICY_REFERENCE_BYTES
452 && value.bytes().enumerate().all(|(index, byte)| {
453 byte.is_ascii_lowercase()
454 || byte.is_ascii_digit()
455 || (index > 0 && matches!(byte, b'-' | b'_' | b'.'))
456 })
457}
458
459fn copy_expected(value: &str, maximum: usize) -> Result<Zeroizing<String>, JwtPolicyError> {
460 if value.is_empty() || value.len() > maximum || value.chars().any(char::is_control) {
461 return Err(JwtPolicyError::InvalidExpectedValue);
462 }
463 Ok(Zeroizing::new(value.to_owned()))
464}
465
466fn valid_claim_name(value: &str) -> bool {
467 !value.is_empty()
468 && value.len() <= MAX_CLAIM_NAME_BYTES
469 && value.bytes().all(|byte| {
470 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':' | b'/')
471 })
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
476#[non_exhaustive]
477pub enum JwtPolicyError {
478 #[error("JWT operator policy reference is invalid")]
480 InvalidPolicyReference,
481 #[error("JWT operator policy revision is invalid")]
483 InvalidPolicyRevision,
484 #[error("JWT policy expected value is invalid")]
486 InvalidExpectedValue,
487 #[error("JWT policy has too many required claims")]
489 TooManyRequiredClaims,
490 #[error("JWT policy required claim name is invalid")]
492 InvalidRequiredClaim,
493 #[error("JWT policy required claim name is duplicated")]
495 DuplicateRequiredClaim,
496 #[error("JWT policy clock skew exceeds its compiled limit")]
498 ClockSkewTooLarge,
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
504pub struct JwtEvaluationTime {
505 pub unix_seconds: i64,
507}
508
509#[derive(Debug, Clone, PartialEq, Eq)]
511pub struct JwtPolicyReviewAudit {
512 parsing: JwtParsingStatus,
514 policy_status: JwtPolicyStatus,
516 local_signature: JwtLocalSignatureStatus,
518 target_acceptance: JwtTargetAcceptanceStatus,
520 methodology: JwtPolicyMethodology,
522 external_activity: JwtExternalActivity,
524 policy_violations: Vec<JwtPolicyViolation>,
526 #[cfg(feature = "jwt-target-acceptance-review")]
527 target_preparation_binding: Option<JwtTargetAcceptanceBinding>,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq)]
533pub struct JwtPolicyMethodology {
534 operator_policy_reference: String,
536 operator_policy_revision: String,
539 local_public_key_sha256: String,
542 expected_type_selected: bool,
544 expected_issuer_selected: bool,
546 expected_audience_selected: bool,
548 required_claim_count: u8,
550 require_expiration: bool,
552 allowed_clock_skew_seconds: u32,
554 evaluation_time_unix_seconds: i64,
556 clock_assurance: JwtClockAssurance,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
562#[serde(rename_all = "snake_case")]
563pub enum JwtClockAssurance {
564 LocalSystemClockNotIndependentlyVerified,
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571pub struct JwtExternalActivity {
572 target_request_count: u8,
574 remote_key_retrieval: JwtExternalOperationStatus,
576 token_forwarding: JwtExternalOperationStatus,
578}
579
580#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
582#[serde(rename_all = "snake_case")]
583pub enum JwtExternalOperationStatus {
584 NotPerformed,
586}
587
588#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
590#[serde(rename_all = "snake_case")]
591pub enum JwtParsingStatus {
592 Parsed,
594 Rejected(JwtParseRejection),
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
600#[serde(rename_all = "snake_case")]
601pub enum JwtParseRejection {
602 EncryptedTokenUnsupported,
604 InvalidSegmentCount,
606 EmptySegment,
608 InvalidBase64Url,
610 DecodedSegmentTooLarge,
612 MalformedJson,
614 DuplicateJsonKey,
616 JsonLimitExceeded,
618 MalformedProtectedHeader,
620 UnsecuredAlgorithmUnsupported,
622 AlgorithmUnsupported,
624 CriticalHeaderUnsupported,
626 UnencodedPayloadUnsupported,
628 CompressionUnsupported,
630 NestedTokenUnsupported,
632 KeyMetadataUnsupported,
634 InvalidSignatureLength,
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
640#[serde(rename_all = "snake_case")]
641pub enum JwtPolicyStatus {
642 NotEvaluated,
644 Consistent,
646 Inconsistent,
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
652#[serde(rename_all = "snake_case")]
653pub enum JwtLocalSignatureStatus {
654 NotEvaluated,
656 Verified,
658 Invalid,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
664#[serde(rename_all = "snake_case")]
665pub enum JwtTargetAcceptanceStatus {
666 NotPerformed,
668}
669
670#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
672#[serde(rename_all = "snake_case")]
673pub enum JwtPolicyViolation {
674 MissingType,
676 TypeMismatch,
678 MissingIssuer,
680 InvalidIssuerType,
682 IssuerMismatch,
684 MissingAudience,
686 InvalidAudienceType,
688 AudienceMismatch,
690 MissingRequiredClaim { ordinal: u8 },
693 MissingExpiration,
695 InvalidExpiration,
697 Expired,
699 InvalidNotBefore,
701 NotYetValid,
703}
704
705impl JwtPolicyReviewAudit {
706 pub const fn schema(&self) -> &'static str {
708 JWT_POLICY_REVIEW_AUDIT_SCHEMA
709 }
710
711 pub const fn policy(&self) -> &'static str {
713 JWT_POLICY_REVIEW_POLICY_ID
714 }
715
716 pub const fn selected(&self) -> bool {
718 true
719 }
720
721 pub const fn parsing_status(&self) -> JwtParsingStatus {
723 self.parsing
724 }
725
726 pub const fn policy_status(&self) -> JwtPolicyStatus {
728 self.policy_status
729 }
730
731 pub const fn local_signature_status(&self) -> JwtLocalSignatureStatus {
733 self.local_signature
734 }
735
736 pub const fn target_acceptance_status(&self) -> JwtTargetAcceptanceStatus {
738 self.target_acceptance
739 }
740
741 pub const fn methodology(&self) -> &JwtPolicyMethodology {
743 &self.methodology
744 }
745
746 pub const fn external_activity(&self) -> JwtExternalActivity {
748 self.external_activity
749 }
750
751 pub fn policy_violations(&self) -> &[JwtPolicyViolation] {
753 &self.policy_violations
754 }
755
756 fn rejected(
757 reason: JwtParseRejection,
758 key: &Es256LocalPublicKey,
759 policy: &JwtLocalPolicy,
760 evaluation_time: JwtEvaluationTime,
761 ) -> Self {
762 Self {
763 parsing: JwtParsingStatus::Rejected(reason),
764 policy_status: JwtPolicyStatus::NotEvaluated,
765 local_signature: JwtLocalSignatureStatus::NotEvaluated,
766 target_acceptance: JwtTargetAcceptanceStatus::NotPerformed,
767 methodology: methodology(key, policy, evaluation_time),
768 external_activity: no_external_activity(),
769 policy_violations: Vec::new(),
770 #[cfg(feature = "jwt-target-acceptance-review")]
771 target_preparation_binding: None,
772 }
773 }
774
775 #[cfg(feature = "jwt-target-acceptance-review")]
776 pub(crate) const fn target_preparation_binding(&self) -> Option<&JwtTargetAcceptanceBinding> {
777 self.target_preparation_binding.as_ref()
778 }
779}
780
781impl JwtPolicyMethodology {
782 pub fn operator_policy_reference(&self) -> &str {
783 &self.operator_policy_reference
784 }
785
786 pub fn operator_policy_revision(&self) -> &str {
787 &self.operator_policy_revision
788 }
789
790 pub fn local_public_key_sha256(&self) -> &str {
791 &self.local_public_key_sha256
792 }
793
794 pub const fn expected_type_selected(&self) -> bool {
795 self.expected_type_selected
796 }
797
798 pub const fn expected_issuer_selected(&self) -> bool {
799 self.expected_issuer_selected
800 }
801
802 pub const fn expected_audience_selected(&self) -> bool {
803 self.expected_audience_selected
804 }
805
806 pub const fn required_claim_count(&self) -> u8 {
807 self.required_claim_count
808 }
809
810 pub const fn require_expiration(&self) -> bool {
811 self.require_expiration
812 }
813
814 pub const fn allowed_clock_skew_seconds(&self) -> u32 {
815 self.allowed_clock_skew_seconds
816 }
817
818 pub const fn evaluation_time_unix_seconds(&self) -> i64 {
819 self.evaluation_time_unix_seconds
820 }
821
822 pub const fn clock_assurance(&self) -> JwtClockAssurance {
823 self.clock_assurance
824 }
825}
826
827impl JwtExternalActivity {
828 pub const fn target_request_count(self) -> u8 {
829 self.target_request_count
830 }
831
832 pub const fn remote_key_retrieval(self) -> JwtExternalOperationStatus {
833 self.remote_key_retrieval
834 }
835
836 pub const fn token_forwarding(self) -> JwtExternalOperationStatus {
837 self.token_forwarding
838 }
839}
840
841pub fn review_compact_jwt(
843 token: &SecretCompactJwt,
844 key: &Es256LocalPublicKey,
845 policy: &JwtLocalPolicy,
846 evaluation_time: JwtEvaluationTime,
847) -> JwtPolicyReviewAudit {
848 match review_parsed(token, key, policy, evaluation_time) {
849 Ok((local_signature, policy_violations)) => JwtPolicyReviewAudit {
850 parsing: JwtParsingStatus::Parsed,
851 policy_status: if policy_violations.is_empty() {
852 JwtPolicyStatus::Consistent
853 } else {
854 JwtPolicyStatus::Inconsistent
855 },
856 local_signature,
857 target_acceptance: JwtTargetAcceptanceStatus::NotPerformed,
858 methodology: methodology(key, policy, evaluation_time),
859 external_activity: no_external_activity(),
860 policy_violations,
861 #[cfg(feature = "jwt-target-acceptance-review")]
862 target_preparation_binding: None,
863 },
864 Err(reason) => JwtPolicyReviewAudit::rejected(reason, key, policy, evaluation_time),
865 }
866}
867
868#[cfg(feature = "jwt-target-acceptance-review")]
876pub fn review_and_prepare_target_acceptance(
877 token: SecretCompactJwt,
878 key: &Es256LocalPublicKey,
879 policy: &JwtLocalPolicy,
880 evaluation_time: JwtEvaluationTime,
881) -> Result<PreparedJwtTargetAcceptance, JwtTargetControlPreparationError> {
882 let mut binding_bytes = [0_u8; 32];
883 SystemRandom::new()
884 .fill(&mut binding_bytes)
885 .map_err(|_| JwtTargetControlPreparationError::PreparationBindingUnavailable)?;
886 let preparation_binding = JwtTargetAcceptanceBinding::from_random_bytes(binding_bytes);
887 let mut local_audit = review_compact_jwt(&token, key, policy, evaluation_time);
888 local_audit.target_preparation_binding = Some(preparation_binding.clone());
889 let eligible = local_audit.parsing_status() == JwtParsingStatus::Parsed
890 && local_audit.policy_status() == JwtPolicyStatus::Consistent
891 && local_audit.local_signature_status() == JwtLocalSignatureStatus::Verified;
892 if !eligible {
893 let not_eligible_reason = match local_audit.parsing_status() {
894 JwtParsingStatus::Rejected(_) => {
895 JwtTargetAcceptanceNotEligibleReason::LocalParsingNotEstablished
896 },
897 JwtParsingStatus::Parsed
898 if local_audit.policy_status() != JwtPolicyStatus::Consistent =>
899 {
900 JwtTargetAcceptanceNotEligibleReason::LocalPolicyNotEstablished
901 },
902 _ => JwtTargetAcceptanceNotEligibleReason::LocalSignatureNotEstablished,
903 };
904 return Ok(PreparedJwtTargetAcceptance {
905 local_audit,
906 runtime_input: JwtTargetAcceptanceRuntimeInput {
907 credentials: None,
908 not_eligible_reason: Some(not_eligible_reason),
909 preparation_binding,
910 },
911 });
912 }
913
914 let invalid_signature_control = derive_invalid_signature_control(&token)?;
915 let control_audit =
916 review_compact_jwt(&invalid_signature_control, key, policy, evaluation_time);
917 if control_audit.parsing_status() != JwtParsingStatus::Parsed
918 || control_audit.policy_status() != JwtPolicyStatus::Consistent
919 || control_audit.local_signature_status() != JwtLocalSignatureStatus::Invalid
920 {
921 return Err(JwtTargetControlPreparationError::InvalidControlInvariant);
922 }
923
924 Ok(PreparedJwtTargetAcceptance {
925 local_audit,
926 runtime_input: JwtTargetAcceptanceRuntimeInput {
927 credentials: Some(JwtTargetAcceptanceCredentials {
928 valid: token,
929 invalid_signature_control,
930 preparation_binding: preparation_binding.clone(),
931 }),
932 not_eligible_reason: None,
933 preparation_binding,
934 },
935 })
936}
937
938#[cfg(feature = "jwt-target-acceptance-review")]
939fn derive_invalid_signature_control(
940 token: &SecretCompactJwt,
941) -> Result<SecretCompactJwt, JwtTargetControlPreparationError> {
942 let bytes = token.as_bytes();
943 let segments = bytes.split(|byte| *byte == b'.').collect::<Vec<_>>();
944 if segments.len() != 3
945 || segments.iter().any(|segment| segment.is_empty())
946 || segments
947 .iter()
948 .any(|segment| !is_unpadded_base64url(segment))
949 {
950 return Err(JwtTargetControlPreparationError::InvalidCompactToken);
951 }
952 let mut signature = Zeroizing::new(Vec::new());
953 URL_SAFE_NO_PAD
954 .decode_vec(segments[2], &mut signature)
955 .map_err(|_| JwtTargetControlPreparationError::InvalidCompactToken)?;
956 if signature.len() != ES256_SIGNATURE_BYTES {
957 return Err(JwtTargetControlPreparationError::InvalidCompactToken);
958 }
959 signature[0] ^= 1;
960
961 const ENCODED_ES256_SIGNATURE_BYTES: usize = 86;
964 let mut encoded_signature = Zeroizing::new(vec![0_u8; ENCODED_ES256_SIGNATURE_BYTES]);
965 let encoded = URL_SAFE_NO_PAD
966 .encode_slice(signature.as_slice(), encoded_signature.as_mut_slice())
967 .map_err(|_| JwtTargetControlPreparationError::InvalidControlInvariant)?;
968 if encoded != ENCODED_ES256_SIGNATURE_BYTES {
969 return Err(JwtTargetControlPreparationError::InvalidControlInvariant);
970 }
971
972 let length = segments[0]
973 .len()
974 .checked_add(1)
975 .and_then(|value| value.checked_add(segments[1].len()))
976 .and_then(|value| value.checked_add(1))
977 .and_then(|value| value.checked_add(encoded))
978 .ok_or(JwtTargetControlPreparationError::InvalidControlInvariant)?;
979 if length > MAX_COMPACT_JWT_BYTES {
980 return Err(JwtTargetControlPreparationError::InvalidControlInvariant);
981 }
982 let mut control = Zeroizing::new(Vec::with_capacity(length));
983 control.extend_from_slice(segments[0]);
984 control.push(b'.');
985 control.extend_from_slice(segments[1]);
986 control.push(b'.');
987 control.extend_from_slice(&encoded_signature[..encoded]);
988 let control = std::mem::take(&mut *control);
989 SecretCompactJwt::new(control)
990 .map_err(|_| JwtTargetControlPreparationError::InvalidControlInvariant)
991}
992
993fn methodology(
994 key: &Es256LocalPublicKey,
995 policy: &JwtLocalPolicy,
996 evaluation_time: JwtEvaluationTime,
997) -> JwtPolicyMethodology {
998 JwtPolicyMethodology {
999 operator_policy_reference: policy.operator_policy_reference.clone(),
1000 operator_policy_revision: policy.operator_policy_revision.clone(),
1001 local_public_key_sha256: key.public_key_sha256().to_owned(),
1002 expected_type_selected: true,
1003 expected_issuer_selected: true,
1004 expected_audience_selected: true,
1005 required_claim_count: u8::try_from(policy.required_claims.len())
1006 .expect("required-claim ceiling fits u8"),
1007 require_expiration: policy.require_expiration,
1008 allowed_clock_skew_seconds: policy.allowed_clock_skew_seconds,
1009 evaluation_time_unix_seconds: evaluation_time.unix_seconds,
1010 clock_assurance: JwtClockAssurance::LocalSystemClockNotIndependentlyVerified,
1011 }
1012}
1013
1014const fn no_external_activity() -> JwtExternalActivity {
1015 JwtExternalActivity {
1016 target_request_count: 0,
1017 remote_key_retrieval: JwtExternalOperationStatus::NotPerformed,
1018 token_forwarding: JwtExternalOperationStatus::NotPerformed,
1019 }
1020}
1021
1022fn review_parsed(
1023 token: &SecretCompactJwt,
1024 key: &Es256LocalPublicKey,
1025 policy: &JwtLocalPolicy,
1026 evaluation_time: JwtEvaluationTime,
1027) -> Result<(JwtLocalSignatureStatus, Vec<JwtPolicyViolation>), JwtParseRejection> {
1028 let bytes = token.as_bytes();
1029 let segments: Vec<&[u8]> = bytes.split(|byte| *byte == b'.').collect();
1030 if segments.len() == 5 {
1031 return Err(JwtParseRejection::EncryptedTokenUnsupported);
1032 }
1033 if segments.len() != 3 {
1034 return Err(JwtParseRejection::InvalidSegmentCount);
1035 }
1036 if segments.iter().any(|segment| segment.is_empty()) {
1037 return Err(JwtParseRejection::EmptySegment);
1038 }
1039 if segments
1040 .iter()
1041 .any(|segment| !is_unpadded_base64url(segment))
1042 {
1043 return Err(JwtParseRejection::InvalidBase64Url);
1044 }
1045
1046 let header_bytes = decode_segment(segments[0], MAX_JWT_HEADER_BYTES)?;
1047 let claims_bytes = decode_segment(segments[1], MAX_JWT_CLAIMS_BYTES)?;
1048 let signature = decode_segment(segments[2], ES256_SIGNATURE_BYTES)?;
1049 if signature.len() != ES256_SIGNATURE_BYTES {
1050 return Err(JwtParseRejection::InvalidSignatureLength);
1051 }
1052
1053 let header = parse_private_json(header_bytes.as_slice()).map_err(map_json_failure)?;
1054 let claims = parse_private_json(claims_bytes.as_slice()).map_err(map_json_failure)?;
1055 if header.as_object().is_none() || claims.as_object().is_none() {
1056 return Err(JwtParseRejection::MalformedJson);
1057 }
1058 validate_protected_header(&header)?;
1059
1060 let signing_input_length = segments[0]
1061 .len()
1062 .checked_add(1)
1063 .and_then(|length| length.checked_add(segments[1].len()))
1064 .ok_or(JwtParseRejection::InvalidSegmentCount)?;
1065 let verifier = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, key.as_sec1_bytes());
1066 let local_signature = if verifier
1067 .verify(&bytes[..signing_input_length], signature.as_slice())
1068 .is_ok()
1069 {
1070 JwtLocalSignatureStatus::Verified
1071 } else {
1072 JwtLocalSignatureStatus::Invalid
1073 };
1074 let violations = evaluate_policy(&header, &claims, policy, evaluation_time);
1075 Ok((local_signature, violations))
1076}
1077
1078fn is_unpadded_base64url(value: &[u8]) -> bool {
1079 !value.is_empty()
1080 && value
1081 .iter()
1082 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
1083}
1084
1085fn decode_segment(segment: &[u8], maximum: usize) -> Result<Zeroizing<Vec<u8>>, JwtParseRejection> {
1086 let mut decoded = Zeroizing::new(Vec::new());
1090 URL_SAFE_NO_PAD
1091 .decode_vec(segment, &mut decoded)
1092 .map_err(|_| JwtParseRejection::InvalidBase64Url)?;
1093 if decoded.len() > maximum {
1094 return Err(JwtParseRejection::DecodedSegmentTooLarge);
1095 }
1096 let canonical = Zeroizing::new(URL_SAFE_NO_PAD.encode(decoded.as_slice()));
1097 if canonical.as_bytes() != segment {
1098 return Err(JwtParseRejection::InvalidBase64Url);
1099 }
1100 Ok(decoded)
1101}
1102
1103fn map_json_failure(failure: JsonFailure) -> JwtParseRejection {
1104 match failure {
1105 JsonFailure::Malformed => JwtParseRejection::MalformedJson,
1106 JsonFailure::DuplicateKey => JwtParseRejection::DuplicateJsonKey,
1107 JsonFailure::LimitExceeded => JwtParseRejection::JsonLimitExceeded,
1108 }
1109}
1110
1111fn validate_protected_header(header: &PrivateJsonValue) -> Result<(), JwtParseRejection> {
1112 match header.string_member("alg") {
1113 Some("ES256") => {},
1114 Some("none") => return Err(JwtParseRejection::UnsecuredAlgorithmUnsupported),
1115 Some(_) => return Err(JwtParseRejection::AlgorithmUnsupported),
1116 None => return Err(JwtParseRejection::MalformedProtectedHeader),
1117 }
1118 if header.object_member("enc").is_some() {
1119 return Err(JwtParseRejection::EncryptedTokenUnsupported);
1120 }
1121 if header.object_member("zip").is_some() {
1122 return Err(JwtParseRejection::CompressionUnsupported);
1123 }
1124 if ["jku", "x5u", "jwk", "x5c"]
1125 .iter()
1126 .any(|name| header.object_member(name).is_some())
1127 {
1128 return Err(JwtParseRejection::KeyMetadataUnsupported);
1129 }
1130 if let Some(content_type) = header.object_member("cty") {
1131 let content_type = content_type
1132 .as_str()
1133 .ok_or(JwtParseRejection::MalformedProtectedHeader)?;
1134 if content_type.eq_ignore_ascii_case("jwt")
1135 || content_type.eq_ignore_ascii_case("application/jwt")
1136 {
1137 return Err(JwtParseRejection::NestedTokenUnsupported);
1138 }
1139 }
1140 if header.object_member("b64").is_some() {
1141 return Err(JwtParseRejection::UnencodedPayloadUnsupported);
1142 }
1143 if header.object_member("crit").is_some() {
1144 return Err(JwtParseRejection::CriticalHeaderUnsupported);
1145 }
1146 if let Some(token_type) = header.object_member("typ") {
1147 if token_type.as_str().is_none() {
1148 return Err(JwtParseRejection::MalformedProtectedHeader);
1149 }
1150 }
1151 Ok(())
1152}
1153
1154fn evaluate_policy(
1155 header: &PrivateJsonValue,
1156 claims: &PrivateJsonValue,
1157 policy: &JwtLocalPolicy,
1158 evaluation_time: JwtEvaluationTime,
1159) -> Vec<JwtPolicyViolation> {
1160 let mut violations = Vec::new();
1161 match header.string_member("typ") {
1162 None => violations.push(JwtPolicyViolation::MissingType),
1163 Some(actual) if actual != policy.expected_type.as_str() => {
1164 violations.push(JwtPolicyViolation::TypeMismatch)
1165 },
1166 Some(_) => {},
1167 }
1168 match claims.object_member("iss") {
1169 None => violations.push(JwtPolicyViolation::MissingIssuer),
1170 Some(PrivateJsonValue::String(actual))
1171 if actual.as_str() == policy.expected_issuer.as_str() => {},
1172 Some(PrivateJsonValue::String(_)) => violations.push(JwtPolicyViolation::IssuerMismatch),
1173 Some(_) => violations.push(JwtPolicyViolation::InvalidIssuerType),
1174 }
1175 match claims.object_member("aud") {
1176 None => violations.push(JwtPolicyViolation::MissingAudience),
1177 Some(PrivateJsonValue::String(actual))
1178 if actual.as_str() == policy.expected_audience.as_str() => {},
1179 Some(PrivateJsonValue::String(_)) => {
1180 violations.push(JwtPolicyViolation::AudienceMismatch)
1181 }
1182 Some(PrivateJsonValue::Array(values)) => {
1183 if values.iter().any(|value| {
1184 matches!(value, PrivateJsonValue::String(actual) if actual.as_str() == policy.expected_audience.as_str())
1185 }) {
1186 if values
1187 .iter()
1188 .any(|value| !matches!(value, PrivateJsonValue::String(_)))
1189 {
1190 violations.push(JwtPolicyViolation::InvalidAudienceType);
1191 }
1192 } else if values
1193 .iter()
1194 .any(|value| !matches!(value, PrivateJsonValue::String(_)))
1195 {
1196 violations.push(JwtPolicyViolation::InvalidAudienceType);
1197 } else {
1198 violations.push(JwtPolicyViolation::AudienceMismatch);
1199 }
1200 }
1201 Some(_) => violations.push(JwtPolicyViolation::InvalidAudienceType),
1202 }
1203 for (ordinal, required) in policy.required_claims.iter().enumerate() {
1204 if claims.object_member(required.as_str()).is_none() {
1205 violations.push(JwtPolicyViolation::MissingRequiredClaim {
1206 ordinal: u8::try_from(ordinal).expect("required-claim ceiling fits u8"),
1207 });
1208 }
1209 }
1210
1211 match claims.object_member("exp") {
1212 None if policy.require_expiration => violations.push(JwtPolicyViolation::MissingExpiration),
1213 None => {},
1214 Some(value) => match value.numeric_date() {
1215 Some(expiration)
1216 if evaluation_time.unix_seconds
1217 >= expiration.saturating_add(i64::from(policy.allowed_clock_skew_seconds)) =>
1218 {
1219 violations.push(JwtPolicyViolation::Expired);
1220 },
1221 Some(_) => {},
1222 None => violations.push(JwtPolicyViolation::InvalidExpiration),
1223 },
1224 }
1225 if let Some(value) = claims.object_member("nbf") {
1226 match value.numeric_date() {
1227 Some(not_before)
1228 if evaluation_time
1229 .unix_seconds
1230 .saturating_add(i64::from(policy.allowed_clock_skew_seconds))
1231 < not_before =>
1232 {
1233 violations.push(JwtPolicyViolation::NotYetValid);
1234 },
1235 Some(_) => {},
1236 None => violations.push(JwtPolicyViolation::InvalidNotBefore),
1237 }
1238 }
1239 violations
1240}
1241
1242enum PrivateJsonValue {
1243 Null,
1244 Bool,
1245 I64(i64),
1246 U64(u64),
1247 F64,
1248 String(Zeroizing<String>),
1249 Array(Vec<Self>),
1250 Object(Vec<(Zeroizing<String>, Self)>),
1251}
1252
1253impl PrivateJsonValue {
1254 fn as_object(&self) -> Option<&[(Zeroizing<String>, Self)]> {
1255 match self {
1256 Self::Object(entries) => Some(entries),
1257 _ => None,
1258 }
1259 }
1260
1261 fn as_str(&self) -> Option<&str> {
1262 match self {
1263 Self::String(value) => Some(value.as_str()),
1264 _ => None,
1265 }
1266 }
1267
1268 fn object_member(&self, name: &str) -> Option<&Self> {
1269 self.as_object()?.iter().find_map(|(key, value)| {
1270 if key.as_str() == name {
1271 Some(value)
1272 } else {
1273 None
1274 }
1275 })
1276 }
1277
1278 fn string_member(&self, name: &str) -> Option<&str> {
1279 self.object_member(name)?.as_str()
1280 }
1281
1282 fn numeric_date(&self) -> Option<i64> {
1283 match self {
1284 Self::I64(value) => Some(*value),
1285 Self::U64(value) => i64::try_from(*value).ok(),
1286 _ => None,
1287 }
1288 }
1289}
1290
1291#[derive(Default)]
1292struct JsonBudget {
1293 nodes: usize,
1294 members: usize,
1295}
1296
1297impl JsonBudget {
1298 fn enter<E: serde::de::Error>(&mut self, depth: usize) -> Result<(), E> {
1299 if depth > MAX_JSON_DEPTH || self.nodes >= MAX_JSON_NODES {
1300 return Err(E::custom(JSON_LIMIT_SENTINEL));
1301 }
1302 self.nodes += 1;
1303 Ok(())
1304 }
1305
1306 fn member<E: serde::de::Error>(&mut self) -> Result<(), E> {
1307 if self.members >= MAX_JSON_MEMBERS {
1308 return Err(E::custom(JSON_LIMIT_SENTINEL));
1309 }
1310 self.members += 1;
1311 Ok(())
1312 }
1313}
1314
1315struct PrivateJsonSeed<'a> {
1316 budget: &'a mut JsonBudget,
1317 depth: usize,
1318}
1319
1320impl<'de> DeserializeSeed<'de> for PrivateJsonSeed<'_> {
1321 type Value = PrivateJsonValue;
1322
1323 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1324 where
1325 D: Deserializer<'de>,
1326 {
1327 self.budget.enter::<D::Error>(self.depth)?;
1328 deserializer.deserialize_any(PrivateJsonVisitor {
1329 budget: self.budget,
1330 depth: self.depth,
1331 })
1332 }
1333}
1334
1335struct PrivateJsonVisitor<'a> {
1336 budget: &'a mut JsonBudget,
1337 depth: usize,
1338}
1339
1340impl<'de> Visitor<'de> for PrivateJsonVisitor<'_> {
1341 type Value = PrivateJsonValue;
1342
1343 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1344 formatter.write_str("bounded JSON")
1345 }
1346
1347 fn visit_unit<E>(self) -> Result<Self::Value, E> {
1348 Ok(PrivateJsonValue::Null)
1349 }
1350
1351 fn visit_none<E>(self) -> Result<Self::Value, E> {
1352 Ok(PrivateJsonValue::Null)
1353 }
1354
1355 fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
1356 Ok(PrivateJsonValue::Bool)
1357 }
1358
1359 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
1360 Ok(PrivateJsonValue::I64(value))
1361 }
1362
1363 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
1364 Ok(PrivateJsonValue::U64(value))
1365 }
1366
1367 fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E> {
1368 Ok(PrivateJsonValue::F64)
1369 }
1370
1371 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1372 where
1373 E: serde::de::Error,
1374 {
1375 if value.len() > MAX_JSON_STRING_BYTES {
1376 return Err(E::custom(JSON_LIMIT_SENTINEL));
1377 }
1378 Ok(PrivateJsonValue::String(Zeroizing::new(value.to_owned())))
1379 }
1380
1381 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1382 where
1383 E: serde::de::Error,
1384 {
1385 let value = Zeroizing::new(value);
1386 if value.len() > MAX_JSON_STRING_BYTES {
1387 return Err(E::custom(JSON_LIMIT_SENTINEL));
1388 }
1389 Ok(PrivateJsonValue::String(value))
1390 }
1391
1392 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
1393 where
1394 A: SeqAccess<'de>,
1395 {
1396 let mut values = Vec::new();
1397 while let Some(value) = sequence.next_element_seed(PrivateJsonSeed {
1398 budget: self.budget,
1399 depth: self.depth + 1,
1400 })? {
1401 if values.len() >= MAX_JSON_ARRAY_ITEMS {
1402 return Err(A::Error::custom(JSON_LIMIT_SENTINEL));
1403 }
1404 values.push(value);
1405 }
1406 Ok(PrivateJsonValue::Array(values))
1407 }
1408
1409 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1410 where
1411 A: MapAccess<'de>,
1412 {
1413 let mut entries: Vec<(Zeroizing<String>, PrivateJsonValue)> = Vec::new();
1414 while let Some(key) = map.next_key::<String>()? {
1415 let key = Zeroizing::new(key);
1416 if key.len() > MAX_JSON_STRING_BYTES {
1417 return Err(A::Error::custom(JSON_LIMIT_SENTINEL));
1418 }
1419 if entries
1420 .iter()
1421 .any(|(known, _)| known.as_str() == key.as_str())
1422 {
1423 return Err(A::Error::custom(JSON_DUPLICATE_SENTINEL));
1424 }
1425 self.budget.member::<A::Error>()?;
1426 let value = map.next_value_seed(PrivateJsonSeed {
1427 budget: self.budget,
1428 depth: self.depth + 1,
1429 })?;
1430 entries.push((key, value));
1431 }
1432 Ok(PrivateJsonValue::Object(entries))
1433 }
1434}
1435
1436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1437enum JsonFailure {
1438 Malformed,
1439 DuplicateKey,
1440 LimitExceeded,
1441}
1442
1443fn parse_private_json(bytes: &[u8]) -> Result<PrivateJsonValue, JsonFailure> {
1444 let mut budget = JsonBudget::default();
1445 let mut deserializer = serde_json::Deserializer::from_slice(bytes);
1446 let parsed = PrivateJsonSeed {
1447 budget: &mut budget,
1448 depth: 0,
1449 }
1450 .deserialize(&mut deserializer)
1451 .map_err(classify_json_error)?;
1452 deserializer.end().map_err(classify_json_error)?;
1453 Ok(parsed)
1454}
1455
1456fn classify_json_error(error: serde_json::Error) -> JsonFailure {
1457 let message = error.to_string();
1458 if message.contains(JSON_DUPLICATE_SENTINEL) {
1459 JsonFailure::DuplicateKey
1460 } else if message.contains(JSON_LIMIT_SENTINEL) {
1461 JsonFailure::LimitExceeded
1462 } else {
1463 JsonFailure::Malformed
1464 }
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469 use super::*;
1470
1471 const RFC7515_PUBLIC_JWK: &[u8] = br#"{"kty":"EC","crv":"P-256","alg":"ES256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"}"#;
1472 const RFC7515_ES256_JWS: &str = concat!(
1473 "eyJhbGciOiJFUzI1NiJ9.",
1474 "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt",
1475 "cGxlLmNvbS9pc19yb290Ijp0cnVlfQ.",
1476 "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSA",
1477 "pmWQxfKTUJqPP3-Kg6NU1Q"
1478 );
1479
1480 fn key() -> Es256LocalPublicKey {
1481 Es256LocalPublicKey::from_jwk_json(RFC7515_PUBLIC_JWK.to_vec()).expect("RFC public key")
1482 }
1483
1484 fn rfc_policy() -> JwtLocalPolicy {
1485 JwtLocalPolicy::new(
1486 ("rfc7515-example", "rfc7515-example-v1"),
1487 "JWT",
1488 "joe",
1489 "termivar-fixture",
1490 &["http://example.com/is_root"],
1491 true,
1492 0,
1493 )
1494 .expect("policy")
1495 }
1496
1497 fn review_literal(token: &str, policy: &JwtLocalPolicy, now: i64) -> JwtPolicyReviewAudit {
1498 let token = SecretCompactJwt::new(token.as_bytes().to_vec()).expect("bounded token");
1499 review_compact_jwt(
1500 &token,
1501 &key(),
1502 policy,
1503 JwtEvaluationTime { unix_seconds: now },
1504 )
1505 }
1506
1507 fn unsigned_test_token(header: &str, claims: &str) -> String {
1508 let signature = [0_u8; ES256_SIGNATURE_BYTES];
1509 format!(
1510 "{}.{}.{}",
1511 URL_SAFE_NO_PAD.encode(header.as_bytes()),
1512 URL_SAFE_NO_PAD.encode(claims.as_bytes()),
1513 URL_SAFE_NO_PAD.encode(signature)
1514 )
1515 }
1516
1517 #[cfg(feature = "jwt-target-acceptance-review")]
1518 fn locally_verified_target_fixture() -> (SecretCompactJwt, Es256LocalPublicKey, JwtLocalPolicy)
1519 {
1520 use ring::{
1521 rand::SystemRandom,
1522 signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_FIXED_SIGNING},
1523 };
1524
1525 let rng = SystemRandom::new();
1526 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1527 .expect("generate task-owned key");
1528 let pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1529 .expect("parse task-owned key");
1530 let point = pair.public_key().as_ref();
1531 let public_jwk = format!(
1532 concat!(
1533 "{{\"kty\":\"EC\",\"crv\":\"P-256\",\"alg\":\"ES256\",",
1534 "\"x\":\"{}\",\"y\":\"{}\"}}"
1535 ),
1536 URL_SAFE_NO_PAD.encode(&point[1..33]),
1537 URL_SAFE_NO_PAD.encode(&point[33..65]),
1538 );
1539 let key = Es256LocalPublicKey::from_jwk_json(public_jwk.into_bytes())
1540 .expect("task-owned public key");
1541 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"ES256","typ":"JWT"}"#);
1542 let claims = URL_SAFE_NO_PAD
1543 .encode(br#"{"iss":"owned-issuer","aud":"owned-api","scope":"read","exp":4000000000}"#);
1544 let signing_input = format!("{header}.{claims}");
1545 let signature = pair
1546 .sign(&rng, signing_input.as_bytes())
1547 .expect("sign task-owned token");
1548 let token = SecretCompactJwt::new(
1549 format!(
1550 "{signing_input}.{}",
1551 URL_SAFE_NO_PAD.encode(signature.as_ref())
1552 )
1553 .into_bytes(),
1554 )
1555 .expect("bounded token");
1556 let policy = JwtLocalPolicy::new(
1557 ("owned-target-policy", "owned-target-policy-v1"),
1558 "JWT",
1559 "owned-issuer",
1560 "owned-api",
1561 &["scope"],
1562 true,
1563 0,
1564 )
1565 .expect("policy");
1566 (token, key, policy)
1567 }
1568
1569 #[cfg(feature = "jwt-target-acceptance-review")]
1570 #[test]
1571 fn target_handoff_is_sealed_until_all_local_gates_pass() {
1572 let (token, key, policy) = locally_verified_target_fixture();
1573 let prepared = review_and_prepare_target_acceptance(
1574 token,
1575 &key,
1576 &policy,
1577 JwtEvaluationTime {
1578 unix_seconds: 1_900_000_000,
1579 },
1580 )
1581 .expect("prepare control");
1582 let (audit, runtime_input) = prepared.into_parts();
1583 let (credentials, not_eligible_reason, preparation_binding) = runtime_input.into_parts();
1584 assert_eq!(audit.parsing_status(), JwtParsingStatus::Parsed);
1585 assert_eq!(audit.policy_status(), JwtPolicyStatus::Consistent);
1586 assert_eq!(
1587 audit.local_signature_status(),
1588 JwtLocalSignatureStatus::Verified
1589 );
1590 let credentials = credentials.expect("eligible handoff");
1591 assert_eq!(not_eligible_reason, None);
1592 assert_eq!(credentials.preparation_binding(), &preparation_binding);
1593 assert_eq!(
1594 audit.target_preparation_binding(),
1595 Some(&preparation_binding)
1596 );
1597 assert_ne!(
1598 credentials.valid_bytes(),
1599 credentials.invalid_signature_control_bytes()
1600 );
1601 let valid_parts = credentials
1602 .valid_bytes()
1603 .split(|byte| *byte == b'.')
1604 .collect::<Vec<_>>();
1605 let invalid_parts = credentials
1606 .invalid_signature_control_bytes()
1607 .split(|byte| *byte == b'.')
1608 .collect::<Vec<_>>();
1609 assert_eq!(valid_parts.len(), 3);
1610 assert_eq!(invalid_parts.len(), 3);
1611 assert_eq!(valid_parts[0], invalid_parts[0]);
1612 assert_eq!(valid_parts[1], invalid_parts[1]);
1613 let valid_signature = URL_SAFE_NO_PAD
1614 .decode(valid_parts[2])
1615 .expect("decode valid signature independently");
1616 let invalid_signature = URL_SAFE_NO_PAD
1617 .decode(invalid_parts[2])
1618 .expect("decode invalid signature independently");
1619 assert_eq!(valid_signature.len(), ES256_SIGNATURE_BYTES);
1620 assert_eq!(invalid_signature.len(), ES256_SIGNATURE_BYTES);
1621 assert_eq!(
1622 valid_signature
1623 .iter()
1624 .zip(&invalid_signature)
1625 .map(|(valid, invalid)| (valid ^ invalid).count_ones())
1626 .sum::<u32>(),
1627 1,
1628 "the control changes exactly one signature bit"
1629 );
1630 let mut signing_input = Vec::new();
1631 signing_input.extend_from_slice(valid_parts[0]);
1632 signing_input.push(b'.');
1633 signing_input.extend_from_slice(valid_parts[1]);
1634 let verifier =
1635 UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, key.sec1_public_point.as_slice());
1636 assert!(verifier.verify(&signing_input, &valid_signature).is_ok());
1637 assert!(verifier.verify(&signing_input, &invalid_signature).is_err());
1638 let invalid = SecretCompactJwt::new(credentials.invalid_signature_control_bytes().to_vec())
1639 .expect("bounded invalid control");
1640 let control = review_compact_jwt(
1641 &invalid,
1642 &key,
1643 &policy,
1644 JwtEvaluationTime {
1645 unix_seconds: 1_900_000_000,
1646 },
1647 );
1648 assert_eq!(control.parsing_status(), JwtParsingStatus::Parsed);
1649 assert_eq!(control.policy_status(), JwtPolicyStatus::Consistent);
1650 assert_eq!(
1651 control.local_signature_status(),
1652 JwtLocalSignatureStatus::Invalid
1653 );
1654
1655 let (token, key, _) = locally_verified_target_fixture();
1656 let mismatched = JwtLocalPolicy::new(
1657 ("owned-target-policy", "owned-target-policy-v2"),
1658 "JWT",
1659 "other-issuer",
1660 "owned-api",
1661 &["scope"],
1662 true,
1663 0,
1664 )
1665 .expect("mismatched policy");
1666 let prepared = review_and_prepare_target_acceptance(
1667 token,
1668 &key,
1669 &mismatched,
1670 JwtEvaluationTime {
1671 unix_seconds: 1_900_000_000,
1672 },
1673 )
1674 .expect("local mismatch remains an audit outcome");
1675 let (audit, runtime_input) = prepared.into_parts();
1676 let (credentials, not_eligible_reason, preparation_binding) = runtime_input.into_parts();
1677 assert_eq!(audit.policy_status(), JwtPolicyStatus::Inconsistent);
1678 assert!(credentials.is_none());
1679 assert_eq!(
1680 not_eligible_reason,
1681 Some(JwtTargetAcceptanceNotEligibleReason::LocalPolicyNotEstablished)
1682 );
1683 assert_eq!(
1684 audit.target_preparation_binding(),
1685 Some(&preparation_binding)
1686 );
1687 }
1688
1689 #[test]
1690 fn rfc7515_es256_vector_separates_all_four_states() {
1691 let audit = review_literal(RFC7515_ES256_JWS, &rfc_policy(), 1_300_819_300);
1692 assert_eq!(audit.parsing, JwtParsingStatus::Parsed);
1693 assert_eq!(audit.policy_status, JwtPolicyStatus::Inconsistent);
1694 assert_eq!(audit.local_signature, JwtLocalSignatureStatus::Verified);
1695 assert_eq!(
1696 audit.target_acceptance,
1697 JwtTargetAcceptanceStatus::NotPerformed
1698 );
1699 assert_eq!(
1700 audit.methodology,
1701 JwtPolicyMethodology {
1702 operator_policy_reference: "rfc7515-example".to_owned(),
1703 operator_policy_revision: "rfc7515-example-v1".to_owned(),
1704 local_public_key_sha256:
1705 "sha256:dcd2446ca98830c843c493a672364ba971d674fbef5ce87d697165b31e90300a"
1706 .to_owned(),
1707 expected_type_selected: true,
1708 expected_issuer_selected: true,
1709 expected_audience_selected: true,
1710 required_claim_count: 1,
1711 require_expiration: true,
1712 allowed_clock_skew_seconds: 0,
1713 evaluation_time_unix_seconds: 1_300_819_300,
1714 clock_assurance: JwtClockAssurance::LocalSystemClockNotIndependentlyVerified,
1715 }
1716 );
1717 assert_eq!(audit.external_activity, no_external_activity());
1718 assert_eq!(
1719 audit.methodology.local_public_key_sha256,
1720 "sha256:dcd2446ca98830c843c493a672364ba971d674fbef5ce87d697165b31e90300a"
1721 );
1722 assert_eq!(
1723 audit.policy_violations,
1724 vec![
1725 JwtPolicyViolation::MissingType,
1726 JwtPolicyViolation::MissingAudience,
1727 ]
1728 );
1729 }
1730
1731 #[test]
1732 fn wrong_public_key_does_not_change_parsing_or_policy() {
1733 let wrong_jwk = br#"{"kty":"EC","crv":"P-256","alg":"ES256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEQ","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"}"#;
1734 let wrong_key =
1735 Es256LocalPublicKey::from_jwk_json(wrong_jwk.to_vec()).expect("bounded public JWK");
1736 let token = SecretCompactJwt::new(RFC7515_ES256_JWS.as_bytes().to_vec()).expect("token");
1737 let audit = review_compact_jwt(
1738 &token,
1739 &wrong_key,
1740 &rfc_policy(),
1741 JwtEvaluationTime {
1742 unix_seconds: 1_300_819_300,
1743 },
1744 );
1745 assert_eq!(audit.parsing, JwtParsingStatus::Parsed);
1746 assert_eq!(audit.policy_status, JwtPolicyStatus::Inconsistent);
1747 assert_eq!(audit.local_signature, JwtLocalSignatureStatus::Invalid);
1748 }
1749
1750 #[test]
1751 fn malformed_duplicate_and_wrong_compact_shapes_are_rejected() {
1752 let policy = rfc_policy();
1753 let malformed = unsigned_test_token("not-json", r#"{"iss":"joe"}"#);
1754 assert_eq!(
1755 review_literal(&malformed, &policy, 0).parsing,
1756 JwtParsingStatus::Rejected(JwtParseRejection::MalformedJson)
1757 );
1758 let duplicate_header = unsigned_test_token(
1759 r#"{"alg":"ES256","alg":"ES256"}"#,
1760 r#"{"iss":"joe","exp":2000000000,"http://example.com/is_root":true}"#,
1761 );
1762 assert_eq!(
1763 review_literal(&duplicate_header, &policy, 0).parsing,
1764 JwtParsingStatus::Rejected(JwtParseRejection::DuplicateJsonKey)
1765 );
1766 let duplicate_claim = unsigned_test_token(
1767 r#"{"alg":"ES256"}"#,
1768 r#"{"iss":"joe","iss":"joe","exp":2000000000}"#,
1769 );
1770 assert_eq!(
1771 review_literal(&duplicate_claim, &policy, 0).parsing,
1772 JwtParsingStatus::Rejected(JwtParseRejection::DuplicateJsonKey)
1773 );
1774 assert_eq!(
1775 review_literal("a.b.c.d.e", &policy, 0).parsing,
1776 JwtParsingStatus::Rejected(JwtParseRejection::EncryptedTokenUnsupported)
1777 );
1778 assert_eq!(
1779 review_literal("a.b", &policy, 0).parsing,
1780 JwtParsingStatus::Rejected(JwtParseRejection::InvalidSegmentCount)
1781 );
1782 }
1783
1784 #[test]
1785 fn issuer_audience_and_type_checks_use_exact_values() {
1786 let token = unsigned_test_token(
1787 r#"{"alg":"ES256","typ":"JWT"}"#,
1788 r#"{"iss":"issuer-b","aud":["api-b","api-c"],"exp":2000000000,"scope":"read"}"#,
1789 );
1790 let policy = JwtLocalPolicy::new(
1791 ("exact-claim-policy", "exact-claim-policy-v1"),
1792 "at+jwt",
1793 "issuer-a",
1794 "api-a",
1795 &["scope", "tenant"],
1796 true,
1797 0,
1798 )
1799 .expect("policy");
1800 let audit = review_literal(&token, &policy, 1_900_000_000);
1801 assert_eq!(audit.parsing, JwtParsingStatus::Parsed);
1802 assert_eq!(audit.policy_status, JwtPolicyStatus::Inconsistent);
1803 assert_eq!(audit.local_signature, JwtLocalSignatureStatus::Invalid);
1804 assert_eq!(
1805 audit.policy_violations,
1806 vec![
1807 JwtPolicyViolation::TypeMismatch,
1808 JwtPolicyViolation::IssuerMismatch,
1809 JwtPolicyViolation::AudienceMismatch,
1810 JwtPolicyViolation::MissingRequiredClaim { ordinal: 1 },
1811 ]
1812 );
1813 }
1814
1815 #[test]
1816 fn expiration_not_before_and_skew_are_integral_and_bounded() {
1817 let token = unsigned_test_token(
1818 r#"{"alg":"ES256","typ":"JWT"}"#,
1819 r#"{"iss":"issuer","aud":"audience","exp":100,"nbf":110}"#,
1820 );
1821 let no_skew = JwtLocalPolicy::new(
1822 ("no-skew", "no-skew-v1"),
1823 "JWT",
1824 "issuer",
1825 "audience",
1826 &[],
1827 true,
1828 0,
1829 )
1830 .expect("policy");
1831 assert_eq!(
1832 review_literal(&token, &no_skew, 100).policy_violations,
1833 vec![JwtPolicyViolation::Expired, JwtPolicyViolation::NotYetValid]
1834 );
1835 let with_skew = JwtLocalPolicy::new(
1836 ("with-skew", "with-skew-v1"),
1837 "JWT",
1838 "issuer",
1839 "audience",
1840 &[],
1841 true,
1842 11,
1843 )
1844 .expect("policy");
1845 assert_eq!(
1846 review_literal(&token, &with_skew, 100).policy_status,
1847 JwtPolicyStatus::Consistent
1848 );
1849 let invalid = unsigned_test_token(
1850 r#"{"alg":"ES256","typ":"JWT"}"#,
1851 r#"{"iss":"issuer","aud":"audience","exp":100.5,"nbf":true}"#,
1852 );
1853 assert_eq!(
1854 review_literal(&invalid, &no_skew, 0).policy_violations,
1855 vec![
1856 JwtPolicyViolation::InvalidExpiration,
1857 JwtPolicyViolation::InvalidNotBefore,
1858 ]
1859 );
1860 }
1861
1862 #[test]
1863 fn unsupported_jose_forms_are_rejected_without_key_authority() {
1864 let cases = [
1865 (
1866 r#"{"alg":"none"}"#,
1867 JwtParseRejection::UnsecuredAlgorithmUnsupported,
1868 ),
1869 (
1870 r#"{"alg":"HS256"}"#,
1871 JwtParseRejection::AlgorithmUnsupported,
1872 ),
1873 (
1874 r#"{"alg":"ES256","crit":["unknown"]}"#,
1875 JwtParseRejection::CriticalHeaderUnsupported,
1876 ),
1877 (
1878 r#"{"alg":"ES256","b64":false,"crit":["b64"]}"#,
1879 JwtParseRejection::UnencodedPayloadUnsupported,
1880 ),
1881 (
1882 r#"{"alg":"ES256","b64":true}"#,
1883 JwtParseRejection::UnencodedPayloadUnsupported,
1884 ),
1885 (
1886 r#"{"alg":"ES256","zip":"DEF"}"#,
1887 JwtParseRejection::CompressionUnsupported,
1888 ),
1889 (
1890 r#"{"alg":"ES256","cty":"JWT"}"#,
1891 JwtParseRejection::NestedTokenUnsupported,
1892 ),
1893 (
1894 r#"{"alg":"ES256","jku":"https://keys.invalid/jwks"}"#,
1895 JwtParseRejection::KeyMetadataUnsupported,
1896 ),
1897 (
1898 r#"{"alg":"ES256","x5u":"https://keys.invalid/cert"}"#,
1899 JwtParseRejection::KeyMetadataUnsupported,
1900 ),
1901 (
1902 r#"{"alg":"ES256","jwk":{"kty":"EC"}}"#,
1903 JwtParseRejection::KeyMetadataUnsupported,
1904 ),
1905 (
1906 r#"{"alg":"ES256","x5c":[]}"#,
1907 JwtParseRejection::KeyMetadataUnsupported,
1908 ),
1909 ];
1910 for (header, expected) in cases {
1911 let token = unsigned_test_token(header, r#"{}"#);
1912 assert_eq!(
1913 review_literal(&token, &rfc_policy(), 0).parsing,
1914 JwtParsingStatus::Rejected(expected)
1915 );
1916 }
1917 }
1918
1919 #[test]
1920 fn token_key_id_is_inert_under_the_single_explicit_local_key_policy() {
1921 let without_key_id = unsigned_test_token(r#"{"alg":"ES256"}"#, r#"{}"#);
1922 let with_hostile_key_id = unsigned_test_token(
1923 r#"{"alg":"ES256","kid":"../../untrusted-key?source=remote"}"#,
1924 r#"{}"#,
1925 );
1926 let baseline = review_literal(&without_key_id, &rfc_policy(), 0);
1927 let key_id = review_literal(&with_hostile_key_id, &rfc_policy(), 0);
1928 assert_eq!(baseline.parsing, JwtParsingStatus::Parsed);
1929 assert_eq!(key_id.parsing, JwtParsingStatus::Parsed);
1930 assert_eq!(key_id.policy_status, baseline.policy_status);
1931 assert_eq!(key_id.local_signature, baseline.local_signature);
1932 assert_eq!(key_id.external_activity, no_external_activity());
1933 assert_eq!(
1934 key_id.target_acceptance,
1935 JwtTargetAcceptanceStatus::NotPerformed
1936 );
1937 }
1938
1939 #[test]
1940 fn local_jwk_is_a_strict_public_es256_subset() {
1941 let x = "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU";
1942 let y = "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0";
1943 for (document, expected) in [
1944 (
1945 format!(r#"{{"kty":"RSA","crv":"P-256","alg":"ES256","x":"{x}","y":"{y}"}}"#),
1946 Es256PublicJwkError::UnsupportedKey,
1947 ),
1948 (
1949 format!(r#"{{"kty":"EC","crv":"P-384","alg":"ES256","x":"{x}","y":"{y}"}}"#),
1950 Es256PublicJwkError::UnsupportedKey,
1951 ),
1952 (
1953 format!(r#"{{"kty":"EC","crv":"P-256","x":"{x}","y":"{y}"}}"#),
1954 Es256PublicJwkError::MissingMember,
1955 ),
1956 (
1957 format!(
1958 r#"{{"kty":"EC","crv":"P-256","alg":"ES256","x":"{x}","x":"{x}","y":"{y}"}}"#
1959 ),
1960 Es256PublicJwkError::DuplicateKey,
1961 ),
1962 (
1963 format!(
1964 r#"{{"kty":"EC","crv":"P-256","alg":"ES256","x":"{x}","y":"{y}","kid":"ignored-nowhere"}}"#
1965 ),
1966 Es256PublicJwkError::UnsupportedMember,
1967 ),
1968 ] {
1969 assert_eq!(
1970 Es256LocalPublicKey::from_jwk_json(document.into_bytes()).err(),
1971 Some(expected)
1972 );
1973 }
1974 assert_eq!(
1975 Es256LocalPublicKey::from_jwk_json(
1976 br#"{"kty":"EC","crv":"P-256","alg":"ES256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","d":"private"}"#.to_vec()
1977 )
1978 .err(),
1979 Some(Es256PublicJwkError::PrivateMaterialProhibited)
1980 );
1981 assert_eq!(
1982 Es256LocalPublicKey::from_jwk_json(
1983 br#"{"kty":"EC","crv":"P-256","alg":"ES256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","jku":"https://invalid"}"#.to_vec()
1984 )
1985 .err(),
1986 Some(Es256PublicJwkError::KeyMetadataProhibited)
1987 );
1988 assert_eq!(
1989 Es256LocalPublicKey::from_jwk_json(
1990 br#"{"kty":"EC","crv":"P-256","alg":"ES256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU=","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"}"#.to_vec()
1991 )
1992 .err(),
1993 Some(Es256PublicJwkError::InvalidCoordinate)
1994 );
1995 }
1996
1997 #[test]
1998 fn canonical_coordinates_do_not_claim_curve_membership_before_verification() {
1999 let zero = URL_SAFE_NO_PAD.encode([0_u8; 32]);
2000 let document =
2001 format!(r#"{{"kty":"EC","crv":"P-256","alg":"ES256","x":"{zero}","y":"{zero}"}}"#);
2002 let structurally_bounded = Es256LocalPublicKey::from_jwk_json(document.into_bytes())
2003 .expect("closed JWK shape and canonical coordinates");
2004 let token = SecretCompactJwt::new(RFC7515_ES256_JWS.as_bytes().to_vec()).expect("token");
2005 let audit = review_compact_jwt(
2006 &token,
2007 &structurally_bounded,
2008 &rfc_policy(),
2009 JwtEvaluationTime {
2010 unix_seconds: 1_300_819_300,
2011 },
2012 );
2013 assert_eq!(audit.parsing, JwtParsingStatus::Parsed);
2014 assert_eq!(audit.policy_status, JwtPolicyStatus::Inconsistent);
2015 assert_eq!(audit.local_signature, JwtLocalSignatureStatus::Invalid);
2016 assert_eq!(audit.external_activity, no_external_activity());
2017 }
2018
2019 #[test]
2020 fn oversize_and_structural_limits_fail_closed() {
2021 assert_eq!(
2022 SecretCompactJwt::new(vec![b'a'; MAX_COMPACT_JWT_BYTES + 1]).err(),
2023 Some(JwtInputError::TokenTooLarge)
2024 );
2025 let deep_claims = r#"{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":1}}}}}}}}}"#;
2026 let token = unsigned_test_token(r#"{"alg":"ES256"}"#, deep_claims);
2027 assert_eq!(
2028 review_literal(&token, &rfc_policy(), 0).parsing,
2029 JwtParsingStatus::Rejected(JwtParseRejection::JsonLimitExceeded)
2030 );
2031 }
2032
2033 #[test]
2034 fn policy_reference_and_compact_base64url_are_canonical() {
2035 assert_eq!(
2036 JwtLocalPolicy::new(
2037 ("Uppercase", "uppercase-v1"),
2038 "JWT",
2039 "issuer",
2040 "audience",
2041 &[],
2042 false,
2043 0,
2044 )
2045 .err(),
2046 Some(JwtPolicyError::InvalidPolicyReference)
2047 );
2048 assert_eq!(
2049 JwtLocalPolicy::new(
2050 ("valid-policy", "Invalid Revision"),
2051 "JWT",
2052 "issuer",
2053 "audience",
2054 &[],
2055 false,
2056 0,
2057 )
2058 .err(),
2059 Some(JwtPolicyError::InvalidPolicyRevision)
2060 );
2061 for (expected_type, expected_issuer, expected_audience) in [
2062 ("", "issuer", "audience"),
2063 ("JWT", "", "audience"),
2064 ("JWT", "issuer", ""),
2065 ] {
2066 assert_eq!(
2067 JwtLocalPolicy::new(
2068 ("required-bindings", "required-bindings-v1"),
2069 expected_type,
2070 expected_issuer,
2071 expected_audience,
2072 &[],
2073 true,
2074 0,
2075 )
2076 .err(),
2077 Some(JwtPolicyError::InvalidExpectedValue)
2078 );
2079 }
2080 let mut token = unsigned_test_token(r#"{"alg":"ES256"}"#, r#"{}"#);
2081 let final_character = token.pop().expect("signature segment");
2082 assert_eq!(final_character, 'A');
2083 token.push('B');
2084 assert_eq!(
2085 review_literal(&token, &rfc_policy(), 0).parsing,
2086 JwtParsingStatus::Rejected(JwtParseRejection::InvalidBase64Url)
2087 );
2088 }
2089
2090 #[test]
2091 fn audit_and_errors_never_retain_token_key_or_claim_canaries() {
2092 const CANARY: &str = "TERMIVAR-JWT-SECRET-CANARY-7d21";
2093 let token = unsigned_test_token(
2094 r#"{"alg":"ES256","typ":"wrong"}"#,
2095 &format!(r#"{{"iss":"{CANARY}","exp":0}}"#),
2096 );
2097 let policy = JwtLocalPolicy::new(
2098 ("canary-redaction", "canary-redaction-v1"),
2099 CANARY,
2100 "expected-issuer",
2101 "expected-audience",
2102 &[],
2103 true,
2104 0,
2105 )
2106 .expect("policy");
2107 let audit = review_literal(&token, &policy, 1);
2108 let encoded = format!("{audit:?}");
2112 assert!(!encoded.contains(CANARY));
2113 assert!(!encoded.contains("expected-issuer"));
2114 assert!(!encoded.contains(&token));
2115 assert_eq!(
2116 audit.target_acceptance,
2117 JwtTargetAcceptanceStatus::NotPerformed
2118 );
2119 }
2120}