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 if !realm.verify(&decoded.signing_input, &decoded.signature) {
74 return Err(RejectReason::RealmSignature("PIC Token JWT").into());
75 }
76 let claims = decoded.claims;
77 check_claims_profile(&claims)?;
78
79 let continuity_bytes = claims.root_bytes()?;
80 let continuity_cose = PicContinuityCose::from_bytes(&continuity_bytes)?;
81 let continuity: PicContinuityPayload = continuity_cose
82 .verify_with(|data, sig| {
83 if realm.verify(data, sig) {
84 Ok(())
85 } else {
86 Err(crate::cose::CoseError::VerificationFailed)
87 }
88 })
89 .map_err(|_| RejectReason::RealmSignature("PIC Continuity COSE"))?;
90 continuity.check_profile()?;
91 continuity.require_settled()?;
92 continuity.check_root_hash()?;
93
94 let pca_bytes = continuity.root.pca.clone();
95 let pca_cose = PicPcaCose::from_bytes(&pca_bytes)?;
96 let checkpoint: PicPcaPayload = pca_cose
97 .verify_with(|data, sig| {
98 if realm.verify(data, sig) {
99 Ok(())
100 } else {
101 Err(crate::cose::CoseError::VerificationFailed)
102 }
103 })
104 .map_err(|_| RejectReason::RealmSignature("PIC PCA COSE"))?;
105 checkpoint.validate()?;
106
107 Ok(SettledState {
108 claims,
109 continuity,
110 pca_bytes,
111 checkpoint,
112 })
113}
114
115#[derive(Debug, Clone, Default)]
121pub struct SettlementContext {
122 pub iss: String,
124 pub sub: Option<String>,
126 pub aud: Option<String>,
128 pub iat: Option<i64>,
130 pub exp: Option<i64>,
132 pub jti: Option<String>,
134}
135
136#[derive(Debug, Clone)]
138pub struct SettledIssue {
139 pub token: String,
141 pub pca_bytes: Vec<u8>,
143 pub checkpoint: PicPcaPayload,
145 pub continuity_bytes: Vec<u8>,
147}
148
149pub fn issue_settled(
154 checkpoint: PicPcaPayload,
155 realm: &dyn ArtifactSigner,
156 ctx: &SettlementContext,
157) -> Result<SettledIssue, ContinuityError> {
158 checkpoint.validate()?;
159
160 let pca_cose: PicPcaCose =
161 CoseSigned::sign_with(&checkpoint, realm.kid(), realm.cose_algorithm(), |data| {
162 realm.sign(data)
163 })?;
164 let pca_bytes = pca_cose.to_bytes()?;
165
166 let continuity = PicContinuityPayload::settled(pca_bytes.clone());
167 let continuity_cose: PicContinuityCose =
168 CoseSigned::sign_with(&continuity, realm.kid(), realm.cose_algorithm(), |data| {
169 realm.sign(data)
170 })?;
171 let continuity_bytes = continuity_cose.to_bytes()?;
172
173 let mut claims = PicTokenClaims::for_continuity(&continuity_bytes);
174 claims.iss = Some(ctx.iss.clone());
175 claims.sub = ctx.sub.clone();
176 claims.aud = ctx.aud.clone();
177 claims.iat = ctx.iat;
178 claims.exp = ctx.exp;
179 if let (Some(checkpoint_lineage), Some(context_jti)) = (&checkpoint.lineage_id, &ctx.jti)
180 && checkpoint_lineage != context_jti
181 {
182 return Err(RejectReason::Malformed(
183 "settlement context jti does not match pca.lineage_id".to_owned(),
184 )
185 .into());
186 }
187 claims.jti = ctx.jti.clone().or_else(|| checkpoint.lineage_id.clone());
188 let token = sign_token(&claims, realm)?;
189
190 Ok(SettledIssue {
191 token,
192 pca_bytes,
193 checkpoint,
194 continuity_bytes,
195 })
196}
197
198pub struct SettlementAuthority<'a> {
204 pub trusted: &'a dyn TrustedCheckpoint,
206 pub por: &'a dyn PorValidator,
208 pub revocation: &'a dyn RevocationCheck,
210 pub policy: &'a dyn SettlementPolicy,
212 pub order: &'a dyn AttenuationOrder,
214 pub realm: &'a dyn ArtifactSigner,
216}
217
218impl SettlementAuthority<'_> {
219 pub fn settle(
225 &self,
226 candidate_token: &str,
227 ctx: &SettlementContext,
228 ) -> Result<SettledIssue, ContinuityError> {
229 let decoded = decode_token(candidate_token)
232 .map_err(|e| RejectReason::Malformed(format!("candidate token: {e}")))?;
233 check_token_type(&decoded)?;
234 check_claims_profile(&decoded.claims)?;
235 let continuity_bytes = decoded
236 .claims
237 .root_bytes()
238 .map_err(|e| RejectReason::Malformed(format!("pic.root: {e}")))?;
239
240 let continuity_cose = PicContinuityCose::from_bytes(&continuity_bytes)
243 .map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
244 let continuity: PicContinuityPayload = continuity_cose
245 .payload_unverified()
246 .map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
247 continuity.check_profile()?;
248 if continuity.root.pca.is_empty() || continuity.root.pca_hash.is_empty() {
249 return Err(RejectReason::Malformed("empty continuity root".into()).into());
250 }
251
252 let transition_bytes = continuity.candidate_transition()?.to_vec();
254
255 let transition_cose = PicTransitionCose::from_bytes(&transition_bytes)
257 .map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
258 let transition: PicTransitionPayload = transition_cose
259 .payload_unverified()
260 .map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
261 transition.check_profile()?;
262
263 let por = &transition.proof_of_relationship;
265 if por.por_type != self.por.accepted_type() {
266 return Err(RejectReason::PorType(por.por_type.clone()).into());
267 }
268 if por.evidence.is_empty() {
269 return Err(RejectReason::PorRejected("empty evidence".into()).into());
270 }
271
272 let workload = self.por.validate(por)?;
275
276 transition_cose
279 .verify_with(|data, sig| {
280 if workload.verify(data, sig) {
281 Ok(())
282 } else {
283 Err(crate::cose::CoseError::VerificationFailed)
284 }
285 })
286 .map_err(|_| RejectReason::WorkloadSignature("PIC Continuity Transition COSE"))?;
287 continuity_cose
288 .verify_with(|data, sig| {
289 if workload.verify(data, sig) {
290 Ok(())
291 } else {
292 Err(crate::cose::CoseError::VerificationFailed)
293 }
294 })
295 .map_err(|_| RejectReason::WorkloadSignature("candidate PIC Continuity COSE"))?;
296 if !workload.verify(&decoded.signing_input, &decoded.signature) {
297 return Err(RejectReason::WorkloadSignature("candidate PIC Token JWT").into());
298 }
299
300 if !self.trusted.is_current_checkpoint(&continuity.root.pca) {
303 return Err(RejectReason::UntrustedCheckpoint.into());
304 }
305 let checkpoint: PicPcaPayload = PicPcaCose::from_bytes(&continuity.root.pca)
306 .map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?
307 .payload_unverified()
308 .map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?;
309 checkpoint.validate()?;
310 if let Some(checkpoint_lineage) = checkpoint.lineage_id.as_deref() {
311 match decoded.claims.jti.as_deref() {
312 Some(candidate_jti) if candidate_jti == checkpoint_lineage => {}
313 Some(_) => {
314 return Err(RejectReason::Malformed(
315 "candidate token jti does not match checkpoint lineage_id".to_owned(),
316 )
317 .into());
318 }
319 None => {
320 return Err(RejectReason::Malformed(
321 "candidate token is missing jti for checkpoint lineage_id".to_owned(),
322 )
323 .into());
324 }
325 }
326 }
327
328 continuity.check_root_hash()?;
330
331 if transition.position != checkpoint.position + 1 {
333 return Err(RejectReason::PositionProgression.into());
334 }
335
336 if transition.predecessor.predecessor_type != crate::PREDECESSOR_TYPE_PCA {
339 return Err(RejectReason::PredecessorType.into());
340 }
341 if transition.predecessor.hash != artifact_sha256(&continuity.root.pca) {
342 return Err(RejectReason::PredecessorHashMismatch.into());
343 }
344
345 if transition.challenge.previous_challenge != checkpoint.challenge.next_challenge {
347 return Err(RejectReason::ChallengeContinuity.into());
348 }
349 if transition.challenge.next_challenge.is_empty() {
350 return Err(RejectReason::NextChallengeInvalid.into());
351 }
352
353 let attenuations: Attenuations = match &transition.attenuations {
357 Some(wire) => wire.parse()?,
358 None => Attenuations::default(),
359 };
360 let next_authority = materialize(&checkpoint.context_of_authority, &attenuations)?;
361
362 if !self.policy.request_binding(&transition) {
365 return Err(RejectReason::RequestBinding.into());
366 }
367 if !self.policy.conformance(&checkpoint, &transition) {
368 return Err(RejectReason::ContractConformance.into());
369 }
370
371 if !self
374 .order
375 .attenuates(&next_authority, &checkpoint.context_of_authority)
376 {
377 return Err(RejectReason::NonExpansion.into());
378 }
379 if self
380 .revocation
381 .is_revoked(&checkpoint, &continuity.root.pca)
382 {
383 return Err(RejectReason::Revoked.into());
384 }
385 if !self.policy.policy(&checkpoint, &next_authority) {
386 return Err(RejectReason::PolicyDenied.into());
387 }
388
389 let next_checkpoint = PicPcaPayload::new(
392 transition.position,
393 next_authority,
394 transition.challenge.next_challenge.clone(),
395 )
396 .with_optional_lineage_id(checkpoint.lineage_id.clone());
397 issue_settled(next_checkpoint, self.realm, ctx)
398 }
399}
400
401fn check_token_type(decoded: &DecodedToken) -> Result<(), RejectReason> {
402 if decoded.typ == crate::FORMAT_PIC_TOKEN_JWT {
403 Ok(())
404 } else {
405 Err(RejectReason::Malformed(format!(
406 "PIC Token JWT typ must be {}, got {}",
407 crate::FORMAT_PIC_TOKEN_JWT,
408 decoded.typ
409 )))
410 }
411}
412
413fn check_claims_profile(claims: &PicTokenClaims) -> Result<(), RejectReason> {
414 if claims.profile == crate::PROFILE_0_2 {
415 Ok(())
416 } else {
417 Err(RejectReason::ProfileMismatch {
418 artifact: "pic+jwt",
419 expected: crate::PROFILE_0_2.to_string(),
420 got: claims.profile.clone(),
421 })
422 }
423}