1use crate::artifacts::token::{DecodedToken, PicTokenClaims, decode_token, sign_token};
34use crate::artifacts::{
35 PicContinuityCose, PicContinuityPayload, PicPcaCose, PicPcaPayload, PicTransitionCose,
36 PicTransitionPayload, artifact_sha256,
37};
38use crate::authority::attenuation::{AttenuationOrder, Attenuations, materialize};
39use crate::cose::CoseSigned;
40use crate::error::{ContinuityError, RejectReason};
41use crate::por::PorValidator;
42use crate::trust::{
43 ArtifactSigner, ArtifactVerifier, RevocationCheck, SettlementPolicy, TrustedCheckpoint,
44};
45
46#[derive(Debug, Clone)]
52pub struct SettledState {
53 pub claims: PicTokenClaims,
55 pub continuity: PicContinuityPayload,
57 pub pca_bytes: Vec<u8>,
59 pub checkpoint: PicPcaPayload,
61}
62
63pub fn verify_settled(
68 token: &str,
69 realm: &dyn ArtifactVerifier,
70) -> Result<SettledState, ContinuityError> {
71 let decoded = decode_token(token)?;
72 check_token_type(&decoded)?;
73 check_expected_jws_algorithm(&decoded, realm, "PIC Token JWT")?;
74 if !realm.verify(&decoded.signing_input, &decoded.signature) {
75 return Err(RejectReason::RealmSignature("PIC Token JWT").into());
76 }
77 let claims = decoded.claims;
78 check_claims_profile(&claims)?;
79
80 let continuity_bytes = claims.root_bytes()?;
81 let continuity_cose = PicContinuityCose::from_bytes(&continuity_bytes)?;
82 check_expected_cose_algorithm(continuity_cose.algorithm(), realm, "PIC Continuity COSE")?;
83 let continuity: PicContinuityPayload = continuity_cose
84 .verify_with(|data, sig| {
85 if realm.verify(data, sig) {
86 Ok(())
87 } else {
88 Err(crate::cose::CoseError::VerificationFailed)
89 }
90 })
91 .map_err(|_| RejectReason::RealmSignature("PIC Continuity COSE"))?;
92 continuity.check_profile()?;
93 continuity.require_settled()?;
94 continuity.check_root_hash()?;
95
96 let pca_bytes = continuity.root.pca.clone();
97 let pca_cose = PicPcaCose::from_bytes(&pca_bytes)?;
98 check_expected_cose_algorithm(pca_cose.algorithm(), realm, "PIC PCA COSE")?;
99 let checkpoint: PicPcaPayload = pca_cose
100 .verify_with(|data, sig| {
101 if realm.verify(data, sig) {
102 Ok(())
103 } else {
104 Err(crate::cose::CoseError::VerificationFailed)
105 }
106 })
107 .map_err(|_| RejectReason::RealmSignature("PIC PCA COSE"))?;
108 checkpoint.validate()?;
109 check_exp_matches_checkpoint(&claims, &checkpoint, "PIC Token JWT")?;
110
111 Ok(SettledState {
112 claims,
113 continuity,
114 pca_bytes,
115 checkpoint,
116 })
117}
118
119#[derive(Debug, Clone, Default)]
125pub struct SettlementContext {
126 pub iss: String,
128 pub sub: Option<String>,
130 pub aud: Option<String>,
132 pub iat: Option<i64>,
134 pub exp: Option<i64>,
136 pub jti: Option<String>,
138}
139
140#[derive(Debug, Clone)]
142pub struct SettledIssue {
143 pub token: String,
145 pub pca_bytes: Vec<u8>,
147 pub checkpoint: PicPcaPayload,
149 pub continuity_bytes: Vec<u8>,
151}
152
153pub fn issue_settled(
158 checkpoint: PicPcaPayload,
159 realm: &dyn ArtifactSigner,
160 ctx: &SettlementContext,
161) -> Result<SettledIssue, ContinuityError> {
162 checkpoint.validate()?;
163 let exp = match (checkpoint.expires_at, ctx.exp) {
164 (Some(checkpoint_exp), Some(context_exp)) if checkpoint_exp != context_exp => {
165 return Err(RejectReason::Malformed(
166 "settlement context exp does not match pca.expires_at".to_owned(),
167 )
168 .into());
169 }
170 (Some(checkpoint_exp), _) => Some(checkpoint_exp),
171 (None, context_exp) => context_exp,
172 };
173
174 let pca_cose: PicPcaCose =
175 CoseSigned::sign_with(&checkpoint, realm.kid(), realm.cose_algorithm(), |data| {
176 realm.sign(data)
177 })?;
178 let pca_bytes = pca_cose.to_bytes()?;
179
180 let continuity = PicContinuityPayload::settled(pca_bytes.clone());
181 let continuity_cose: PicContinuityCose =
182 CoseSigned::sign_with(&continuity, realm.kid(), realm.cose_algorithm(), |data| {
183 realm.sign(data)
184 })?;
185 let continuity_bytes = continuity_cose.to_bytes()?;
186
187 let mut claims = PicTokenClaims::for_continuity(&continuity_bytes);
188 claims.iss = Some(ctx.iss.clone());
189 claims.sub = ctx.sub.clone();
190 claims.aud = ctx.aud.clone();
191 claims.iat = ctx.iat;
192 claims.exp = exp;
193 if let (Some(checkpoint_lineage), Some(context_jti)) = (&checkpoint.lineage_id, &ctx.jti)
194 && checkpoint_lineage != context_jti
195 {
196 return Err(RejectReason::Malformed(
197 "settlement context jti does not match pca.lineage_id".to_owned(),
198 )
199 .into());
200 }
201 claims.jti = ctx.jti.clone().or_else(|| checkpoint.lineage_id.clone());
202 let token = sign_token(&claims, realm)?;
203
204 Ok(SettledIssue {
205 token,
206 pca_bytes,
207 checkpoint,
208 continuity_bytes,
209 })
210}
211
212pub struct SettlementAuthority<'a> {
218 pub trusted: &'a dyn TrustedCheckpoint,
220 pub por: &'a dyn PorValidator,
222 pub revocation: &'a dyn RevocationCheck,
224 pub policy: &'a dyn SettlementPolicy,
226 pub order: &'a dyn AttenuationOrder,
228 pub realm: &'a dyn ArtifactSigner,
230}
231
232impl SettlementAuthority<'_> {
233 pub fn settle(
239 &self,
240 candidate_token: &str,
241 ctx: &SettlementContext,
242 ) -> Result<SettledIssue, ContinuityError> {
243 let decoded = decode_token(candidate_token)
246 .map_err(|e| RejectReason::Malformed(format!("candidate token: {e}")))?;
247 check_token_type(&decoded)?;
248 check_claims_profile(&decoded.claims)?;
249 let continuity_bytes = decoded
250 .claims
251 .root_bytes()
252 .map_err(|e| RejectReason::Malformed(format!("pic.root: {e}")))?;
253
254 let continuity_cose = PicContinuityCose::from_bytes(&continuity_bytes)
257 .map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
258 let continuity: PicContinuityPayload = continuity_cose
259 .payload_unverified()
260 .map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
261 continuity.check_profile()?;
262 if continuity.root.pca.is_empty() || continuity.root.pca_hash.is_empty() {
263 return Err(RejectReason::Malformed("empty continuity root".into()).into());
264 }
265
266 let transition_bytes = continuity.candidate_transition()?.to_vec();
268
269 let transition_cose = PicTransitionCose::from_bytes(&transition_bytes)
271 .map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
272 let transition: PicTransitionPayload = transition_cose
273 .payload_unverified()
274 .map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
275 transition.check_profile()?;
276
277 let por = &transition.proof_of_relationship;
279 if por.por_type != self.por.accepted_type() {
280 return Err(RejectReason::PorType(por.por_type.clone()).into());
281 }
282 if por.evidence.is_empty() {
283 return Err(RejectReason::PorRejected("empty evidence".into()).into());
284 }
285
286 let workload = self.por.validate(por)?;
289
290 check_expected_cose_algorithm(
293 transition_cose.algorithm(),
294 workload.as_ref(),
295 "PIC Continuity Transition COSE",
296 )?;
297 transition_cose
298 .verify_with(|data, sig| {
299 if workload.verify(data, sig) {
300 Ok(())
301 } else {
302 Err(crate::cose::CoseError::VerificationFailed)
303 }
304 })
305 .map_err(|_| RejectReason::WorkloadSignature("PIC Continuity Transition COSE"))?;
306 check_expected_cose_algorithm(
307 continuity_cose.algorithm(),
308 workload.as_ref(),
309 "candidate PIC Continuity COSE",
310 )?;
311 continuity_cose
312 .verify_with(|data, sig| {
313 if workload.verify(data, sig) {
314 Ok(())
315 } else {
316 Err(crate::cose::CoseError::VerificationFailed)
317 }
318 })
319 .map_err(|_| RejectReason::WorkloadSignature("candidate PIC Continuity COSE"))?;
320 check_expected_jws_algorithm(&decoded, workload.as_ref(), "candidate PIC Token JWT")?;
321 if !workload.verify(&decoded.signing_input, &decoded.signature) {
322 return Err(RejectReason::WorkloadSignature("candidate PIC Token JWT").into());
323 }
324
325 if !self.trusted.is_current_checkpoint(&continuity.root.pca) {
328 return Err(RejectReason::UntrustedCheckpoint.into());
329 }
330 let checkpoint: PicPcaPayload = PicPcaCose::from_bytes(&continuity.root.pca)
331 .map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?
332 .payload_unverified()
333 .map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?;
334 checkpoint.validate()?;
335 if let Some(checkpoint_lineage) = checkpoint.lineage_id.as_deref() {
336 match decoded.claims.jti.as_deref() {
337 Some(candidate_jti) if candidate_jti == checkpoint_lineage => {}
338 Some(_) => {
339 return Err(RejectReason::Malformed(
340 "candidate token jti does not match checkpoint lineage_id".to_owned(),
341 )
342 .into());
343 }
344 None => {
345 return Err(RejectReason::Malformed(
346 "candidate token is missing jti for checkpoint lineage_id".to_owned(),
347 )
348 .into());
349 }
350 }
351 }
352 check_exp_matches_checkpoint(&decoded.claims, &checkpoint, "candidate PIC Token JWT")?;
353
354 continuity.check_root_hash()?;
356
357 if transition.position != checkpoint.position + 1 {
359 return Err(RejectReason::PositionProgression.into());
360 }
361
362 if transition.predecessor.predecessor_type != crate::PREDECESSOR_TYPE_PCA {
365 return Err(RejectReason::PredecessorType.into());
366 }
367 if transition.predecessor.hash != artifact_sha256(&continuity.root.pca) {
368 return Err(RejectReason::PredecessorHashMismatch.into());
369 }
370
371 if transition.challenge.previous_challenge != checkpoint.challenge.next_challenge {
373 return Err(RejectReason::ChallengeContinuity.into());
374 }
375 if transition.challenge.next_challenge.is_empty() {
376 return Err(RejectReason::NextChallengeInvalid.into());
377 }
378
379 let attenuations: Attenuations = match &transition.attenuations {
383 Some(wire) => wire.parse()?,
384 None => Attenuations::default(),
385 };
386 let next_authority = materialize(&checkpoint.context_of_authority, &attenuations)?;
387
388 if !self.policy.request_binding(&transition) {
391 return Err(RejectReason::RequestBinding.into());
392 }
393 if !self.policy.conformance(&checkpoint, &transition) {
394 return Err(RejectReason::ContractConformance.into());
395 }
396
397 if !self
400 .order
401 .attenuates(&next_authority, &checkpoint.context_of_authority)
402 {
403 return Err(RejectReason::NonExpansion.into());
404 }
405 if self
406 .revocation
407 .is_revoked(&checkpoint, &continuity.root.pca)
408 {
409 return Err(RejectReason::Revoked.into());
410 }
411 if !self.policy.policy(&checkpoint, &next_authority) {
412 return Err(RejectReason::PolicyDenied.into());
413 }
414
415 let next_checkpoint = PicPcaPayload::new(
418 transition.position,
419 next_authority,
420 transition.challenge.next_challenge.clone(),
421 )
422 .with_optional_lineage_id(checkpoint.lineage_id.clone())
423 .with_optional_expires_at(checkpoint.expires_at);
424 issue_settled(next_checkpoint, self.realm, ctx)
425 }
426}
427
428fn check_token_type(decoded: &DecodedToken) -> Result<(), RejectReason> {
429 if decoded.typ == crate::FORMAT_PIC_TOKEN_JWT {
430 Ok(())
431 } else {
432 Err(RejectReason::Malformed(format!(
433 "PIC Token JWT typ must be {}, got {}",
434 crate::FORMAT_PIC_TOKEN_JWT,
435 decoded.typ
436 )))
437 }
438}
439
440fn check_claims_profile(claims: &PicTokenClaims) -> Result<(), RejectReason> {
441 if claims.profile == crate::PROFILE_0_2 {
442 Ok(())
443 } else {
444 Err(RejectReason::ProfileMismatch {
445 artifact: "pic+jwt",
446 expected: crate::PROFILE_0_2.to_string(),
447 got: claims.profile.clone(),
448 })
449 }
450}
451
452fn check_expected_jws_algorithm(
453 decoded: &DecodedToken,
454 verifier: &dyn ArtifactVerifier,
455 artifact: &'static str,
456) -> Result<(), RejectReason> {
457 let Some(expected) = verifier.expected_jws_algorithm() else {
458 return Ok(());
459 };
460 if decoded.alg == expected {
461 return Ok(());
462 }
463 Err(RejectReason::Malformed(format!(
464 "{artifact} alg must be {expected}, got {}",
465 decoded.alg
466 )))
467}
468
469fn check_expected_cose_algorithm(
470 actual: Option<crate::cose::SigningAlgorithm>,
471 verifier: &dyn ArtifactVerifier,
472 artifact: &'static str,
473) -> Result<(), RejectReason> {
474 let Some(expected) = verifier.expected_cose_algorithm() else {
475 return Ok(());
476 };
477 if actual == Some(expected) {
478 return Ok(());
479 }
480 Err(RejectReason::Malformed(format!(
481 "{artifact} alg must be {expected}, got {}",
482 actual
483 .map(|algorithm| algorithm.to_string())
484 .unwrap_or_else(|| "None".to_owned())
485 )))
486}
487
488fn check_exp_matches_checkpoint(
489 claims: &PicTokenClaims,
490 checkpoint: &PicPcaPayload,
491 artifact: &'static str,
492) -> Result<(), RejectReason> {
493 let Some(expires_at) = checkpoint.expires_at else {
494 return Ok(());
495 };
496 match claims.exp {
497 Some(exp) if exp == expires_at => Ok(()),
498 Some(exp) => Err(RejectReason::Malformed(format!(
499 "{artifact} exp does not match pca.expires_at: expected {expires_at}, got {exp}"
500 ))),
501 None => Err(RejectReason::Malformed(format!(
502 "{artifact} is missing exp for pca.expires_at"
503 ))),
504 }
505}