use crate::artifacts::token::{DecodedToken, PicTokenClaims, decode_token, sign_token};
use crate::artifacts::{
PicContinuityCose, PicContinuityPayload, PicPcaCose, PicPcaPayload, PicTransitionCose,
PicTransitionPayload, artifact_sha256,
};
use crate::authority::attenuation::{AttenuationOrder, Attenuations, materialize};
use crate::cose::CoseSigned;
use crate::error::{ContinuityError, RejectReason};
use crate::por::PorValidator;
use crate::trust::{
ArtifactSigner, ArtifactVerifier, RevocationCheck, SettlementPolicy, TrustedCheckpoint,
};
#[derive(Debug, Clone)]
pub struct SettledState {
pub claims: PicTokenClaims,
pub continuity: PicContinuityPayload,
pub pca_bytes: Vec<u8>,
pub checkpoint: PicPcaPayload,
}
pub fn verify_settled(
token: &str,
realm: &dyn ArtifactVerifier,
) -> Result<SettledState, ContinuityError> {
let decoded = decode_token(token)?;
check_token_type(&decoded)?;
if !realm.verify(&decoded.signing_input, &decoded.signature) {
return Err(RejectReason::RealmSignature("PIC Token JWT").into());
}
let claims = decoded.claims;
check_claims_profile(&claims)?;
let continuity_bytes = claims.root_bytes()?;
let continuity_cose = PicContinuityCose::from_bytes(&continuity_bytes)?;
let continuity: PicContinuityPayload = continuity_cose
.verify_with(|data, sig| {
if realm.verify(data, sig) {
Ok(())
} else {
Err(crate::cose::CoseError::VerificationFailed)
}
})
.map_err(|_| RejectReason::RealmSignature("PIC Continuity COSE"))?;
continuity.check_profile()?;
continuity.require_settled()?;
continuity.check_root_hash()?;
let pca_bytes = continuity.root.pca.clone();
let pca_cose = PicPcaCose::from_bytes(&pca_bytes)?;
let checkpoint: PicPcaPayload = pca_cose
.verify_with(|data, sig| {
if realm.verify(data, sig) {
Ok(())
} else {
Err(crate::cose::CoseError::VerificationFailed)
}
})
.map_err(|_| RejectReason::RealmSignature("PIC PCA COSE"))?;
checkpoint.validate()?;
Ok(SettledState {
claims,
continuity,
pca_bytes,
checkpoint,
})
}
#[derive(Debug, Clone, Default)]
pub struct SettlementContext {
pub iss: String,
pub sub: Option<String>,
pub aud: Option<String>,
pub iat: Option<i64>,
pub exp: Option<i64>,
pub jti: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SettledIssue {
pub token: String,
pub pca_bytes: Vec<u8>,
pub checkpoint: PicPcaPayload,
pub continuity_bytes: Vec<u8>,
}
pub fn issue_settled(
checkpoint: PicPcaPayload,
realm: &dyn ArtifactSigner,
ctx: &SettlementContext,
) -> Result<SettledIssue, ContinuityError> {
checkpoint.validate()?;
let pca_cose: PicPcaCose =
CoseSigned::sign_with(&checkpoint, realm.kid(), realm.cose_algorithm(), |data| {
realm.sign(data)
})?;
let pca_bytes = pca_cose.to_bytes()?;
let continuity = PicContinuityPayload::settled(pca_bytes.clone());
let continuity_cose: PicContinuityCose =
CoseSigned::sign_with(&continuity, realm.kid(), realm.cose_algorithm(), |data| {
realm.sign(data)
})?;
let continuity_bytes = continuity_cose.to_bytes()?;
let mut claims = PicTokenClaims::for_continuity(&continuity_bytes);
claims.iss = Some(ctx.iss.clone());
claims.sub = ctx.sub.clone();
claims.aud = ctx.aud.clone();
claims.iat = ctx.iat;
claims.exp = ctx.exp;
if let (Some(checkpoint_lineage), Some(context_jti)) = (&checkpoint.lineage_id, &ctx.jti)
&& checkpoint_lineage != context_jti
{
return Err(RejectReason::Malformed(
"settlement context jti does not match pca.lineage_id".to_owned(),
)
.into());
}
claims.jti = ctx.jti.clone().or_else(|| checkpoint.lineage_id.clone());
let token = sign_token(&claims, realm)?;
Ok(SettledIssue {
token,
pca_bytes,
checkpoint,
continuity_bytes,
})
}
pub struct SettlementAuthority<'a> {
pub trusted: &'a dyn TrustedCheckpoint,
pub por: &'a dyn PorValidator,
pub revocation: &'a dyn RevocationCheck,
pub policy: &'a dyn SettlementPolicy,
pub order: &'a dyn AttenuationOrder,
pub realm: &'a dyn ArtifactSigner,
}
impl SettlementAuthority<'_> {
pub fn settle(
&self,
candidate_token: &str,
ctx: &SettlementContext,
) -> Result<SettledIssue, ContinuityError> {
let decoded = decode_token(candidate_token)
.map_err(|e| RejectReason::Malformed(format!("candidate token: {e}")))?;
check_token_type(&decoded)?;
check_claims_profile(&decoded.claims)?;
let continuity_bytes = decoded
.claims
.root_bytes()
.map_err(|e| RejectReason::Malformed(format!("pic.root: {e}")))?;
let continuity_cose = PicContinuityCose::from_bytes(&continuity_bytes)
.map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
let continuity: PicContinuityPayload = continuity_cose
.payload_unverified()
.map_err(|e| RejectReason::Malformed(format!("candidate continuity: {e}")))?;
continuity.check_profile()?;
if continuity.root.pca.is_empty() || continuity.root.pca_hash.is_empty() {
return Err(RejectReason::Malformed("empty continuity root".into()).into());
}
let transition_bytes = continuity.candidate_transition()?.to_vec();
let transition_cose = PicTransitionCose::from_bytes(&transition_bytes)
.map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
let transition: PicTransitionPayload = transition_cose
.payload_unverified()
.map_err(|e| RejectReason::Malformed(format!("transition: {e}")))?;
transition.check_profile()?;
let por = &transition.proof_of_relationship;
if por.por_type != self.por.accepted_type() {
return Err(RejectReason::PorType(por.por_type.clone()).into());
}
if por.evidence.is_empty() {
return Err(RejectReason::PorRejected("empty evidence".into()).into());
}
let workload = self.por.validate(por)?;
transition_cose
.verify_with(|data, sig| {
if workload.verify(data, sig) {
Ok(())
} else {
Err(crate::cose::CoseError::VerificationFailed)
}
})
.map_err(|_| RejectReason::WorkloadSignature("PIC Continuity Transition COSE"))?;
continuity_cose
.verify_with(|data, sig| {
if workload.verify(data, sig) {
Ok(())
} else {
Err(crate::cose::CoseError::VerificationFailed)
}
})
.map_err(|_| RejectReason::WorkloadSignature("candidate PIC Continuity COSE"))?;
if !workload.verify(&decoded.signing_input, &decoded.signature) {
return Err(RejectReason::WorkloadSignature("candidate PIC Token JWT").into());
}
if !self.trusted.is_current_checkpoint(&continuity.root.pca) {
return Err(RejectReason::UntrustedCheckpoint.into());
}
let checkpoint: PicPcaPayload = PicPcaCose::from_bytes(&continuity.root.pca)
.map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?
.payload_unverified()
.map_err(|e| RejectReason::Malformed(format!("checkpoint: {e}")))?;
checkpoint.validate()?;
if let Some(checkpoint_lineage) = checkpoint.lineage_id.as_deref() {
match decoded.claims.jti.as_deref() {
Some(candidate_jti) if candidate_jti == checkpoint_lineage => {}
Some(_) => {
return Err(RejectReason::Malformed(
"candidate token jti does not match checkpoint lineage_id".to_owned(),
)
.into());
}
None => {
return Err(RejectReason::Malformed(
"candidate token is missing jti for checkpoint lineage_id".to_owned(),
)
.into());
}
}
}
continuity.check_root_hash()?;
if transition.position != checkpoint.position + 1 {
return Err(RejectReason::PositionProgression.into());
}
if transition.predecessor.predecessor_type != crate::PREDECESSOR_TYPE_PCA {
return Err(RejectReason::PredecessorType.into());
}
if transition.predecessor.hash != artifact_sha256(&continuity.root.pca) {
return Err(RejectReason::PredecessorHashMismatch.into());
}
if transition.challenge.previous_challenge != checkpoint.challenge.next_challenge {
return Err(RejectReason::ChallengeContinuity.into());
}
if transition.challenge.next_challenge.is_empty() {
return Err(RejectReason::NextChallengeInvalid.into());
}
let attenuations: Attenuations = match &transition.attenuations {
Some(wire) => wire.parse()?,
None => Attenuations::default(),
};
let next_authority = materialize(&checkpoint.context_of_authority, &attenuations)?;
if !self.policy.request_binding(&transition) {
return Err(RejectReason::RequestBinding.into());
}
if !self.policy.conformance(&checkpoint, &transition) {
return Err(RejectReason::ContractConformance.into());
}
if !self
.order
.attenuates(&next_authority, &checkpoint.context_of_authority)
{
return Err(RejectReason::NonExpansion.into());
}
if self
.revocation
.is_revoked(&checkpoint, &continuity.root.pca)
{
return Err(RejectReason::Revoked.into());
}
if !self.policy.policy(&checkpoint, &next_authority) {
return Err(RejectReason::PolicyDenied.into());
}
let next_checkpoint = PicPcaPayload::new(
transition.position,
next_authority,
transition.challenge.next_challenge.clone(),
)
.with_optional_lineage_id(checkpoint.lineage_id.clone());
issue_settled(next_checkpoint, self.realm, ctx)
}
}
fn check_token_type(decoded: &DecodedToken) -> Result<(), RejectReason> {
if decoded.typ == crate::FORMAT_PIC_TOKEN_JWT {
Ok(())
} else {
Err(RejectReason::Malformed(format!(
"PIC Token JWT typ must be {}, got {}",
crate::FORMAT_PIC_TOKEN_JWT,
decoded.typ
)))
}
}
fn check_claims_profile(claims: &PicTokenClaims) -> Result<(), RejectReason> {
if claims.profile == crate::PROFILE_0_2 {
Ok(())
} else {
Err(RejectReason::ProfileMismatch {
artifact: "pic+jwt",
expected: crate::PROFILE_0_2.to_string(),
got: claims.profile.clone(),
})
}
}