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 claims.jti = ctx.jti.clone();
180 let token = sign_token(&claims, realm)?;
181
182 Ok(SettledIssue {
183 token,
184 pca_bytes,
185 checkpoint,
186 continuity_bytes,
187 })
188}
189
190pub struct SettlementAuthority<'a> {
196 pub trusted: &'a dyn TrustedCheckpoint,
198 pub por: &'a dyn PorValidator,
200 pub revocation: &'a dyn RevocationCheck,
202 pub policy: &'a dyn SettlementPolicy,
204 pub order: &'a dyn AttenuationOrder,
206 pub realm: &'a dyn ArtifactSigner,
208}
209
210impl SettlementAuthority<'_> {
211 pub fn settle(
217 &self,
218 candidate_token: &str,
219 ctx: &SettlementContext,
220 ) -> Result<SettledIssue, ContinuityError> {
221 let decoded = decode_token(candidate_token)
224 .map_err(|e| RejectReason::Malformed(format!("candidate token: {e}")))?;
225 check_token_type(&decoded)?;
226 check_claims_profile(&decoded.claims)?;
227 let continuity_bytes = decoded
228 .claims
229 .root_bytes()
230 .map_err(|e| RejectReason::Malformed(format!("pic.root: {e}")))?;
231
232 let continuity_cose = PicContinuityCose::from_bytes(&continuity_bytes)
235 .map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
236 let continuity: PicContinuityPayload = continuity_cose
237 .payload_unverified()
238 .map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
239 continuity.check_profile()?;
240 if continuity.root.pca.is_empty() || continuity.root.pca_hash.is_empty() {
241 return Err(RejectReason::Malformed("empty continuity root".into()).into());
242 }
243
244 let transition_bytes = continuity.candidate_transition()?.to_vec();
246
247 let transition_cose = PicTransitionCose::from_bytes(&transition_bytes)
249 .map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
250 let transition: PicTransitionPayload = transition_cose
251 .payload_unverified()
252 .map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
253 transition.check_profile()?;
254
255 let por = &transition.proof_of_relationship;
257 if por.por_type != self.por.accepted_type() {
258 return Err(RejectReason::PorType(por.por_type.clone()).into());
259 }
260 if por.evidence.is_empty() {
261 return Err(RejectReason::PorRejected("empty evidence".into()).into());
262 }
263
264 let workload = self.por.validate(por)?;
267
268 transition_cose
271 .verify_with(|data, sig| {
272 if workload.verify(data, sig) {
273 Ok(())
274 } else {
275 Err(crate::cose::CoseError::VerificationFailed)
276 }
277 })
278 .map_err(|_| RejectReason::WorkloadSignature("PIC Continuity Transition COSE"))?;
279 continuity_cose
280 .verify_with(|data, sig| {
281 if workload.verify(data, sig) {
282 Ok(())
283 } else {
284 Err(crate::cose::CoseError::VerificationFailed)
285 }
286 })
287 .map_err(|_| RejectReason::WorkloadSignature("candidate PIC Continuity COSE"))?;
288 if !workload.verify(&decoded.signing_input, &decoded.signature) {
289 return Err(RejectReason::WorkloadSignature("candidate PIC Token JWT").into());
290 }
291
292 if !self.trusted.is_current_checkpoint(&continuity.root.pca) {
295 return Err(RejectReason::UntrustedCheckpoint.into());
296 }
297 let checkpoint: PicPcaPayload = PicPcaCose::from_bytes(&continuity.root.pca)
298 .map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?
299 .payload_unverified()
300 .map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?;
301 checkpoint.validate()?;
302
303 continuity.check_root_hash()?;
305
306 if transition.position != checkpoint.position + 1 {
308 return Err(RejectReason::PositionProgression.into());
309 }
310
311 if transition.predecessor.predecessor_type != crate::PREDECESSOR_TYPE_PCA {
314 return Err(RejectReason::PredecessorType.into());
315 }
316 if transition.predecessor.hash != artifact_sha256(&continuity.root.pca) {
317 return Err(RejectReason::PredecessorHashMismatch.into());
318 }
319
320 if transition.challenge.previous_challenge != checkpoint.challenge.next_challenge {
322 return Err(RejectReason::ChallengeContinuity.into());
323 }
324 if transition.challenge.next_challenge.is_empty() {
325 return Err(RejectReason::NextChallengeInvalid.into());
326 }
327
328 let attenuations: Attenuations = match &transition.attenuations {
332 Some(wire) => wire.parse()?,
333 None => Attenuations::default(),
334 };
335 let next_authority = materialize(&checkpoint.context_of_authority, &attenuations)?;
336
337 if !self.policy.request_binding(&transition) {
340 return Err(RejectReason::RequestBinding.into());
341 }
342 if !self.policy.conformance(&checkpoint, &transition) {
343 return Err(RejectReason::ContractConformance.into());
344 }
345
346 if !self
349 .order
350 .attenuates(&next_authority, &checkpoint.context_of_authority)
351 {
352 return Err(RejectReason::NonExpansion.into());
353 }
354 if self
355 .revocation
356 .is_revoked(&checkpoint, &continuity.root.pca)
357 {
358 return Err(RejectReason::Revoked.into());
359 }
360 if !self.policy.policy(&checkpoint, &next_authority) {
361 return Err(RejectReason::PolicyDenied.into());
362 }
363
364 let next_checkpoint = PicPcaPayload::new(
367 transition.position,
368 next_authority,
369 transition.challenge.next_challenge.clone(),
370 );
371 issue_settled(next_checkpoint, self.realm, ctx)
372 }
373}
374
375fn check_token_type(decoded: &DecodedToken) -> Result<(), RejectReason> {
376 if decoded.typ == crate::FORMAT_PIC_TOKEN_JWT {
377 Ok(())
378 } else {
379 Err(RejectReason::Malformed(format!(
380 "PIC Token JWT typ must be {}, got {}",
381 crate::FORMAT_PIC_TOKEN_JWT,
382 decoded.typ
383 )))
384 }
385}
386
387fn check_claims_profile(claims: &PicTokenClaims) -> Result<(), RejectReason> {
388 if claims.profile == crate::PROFILE_0_2 {
389 Ok(())
390 } else {
391 Err(RejectReason::ProfileMismatch {
392 artifact: "pic+jwt",
393 expected: crate::PROFILE_0_2.to_string(),
394 got: claims.profile.clone(),
395 })
396 }
397}