Skip to main content

pic_continuity/
prover.rs

1/*
2 * Copyright Nitro Agility S.r.l.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! PIC Prover (Profile 0.2, centralized settlement).
18//!
19//! Builds one workload-signed advancement candidate from the current trusted
20//! checkpoint, per the Prover procedure of the specification:
21//!
22//! 1. validate the predecessor continuity state;
23//! 2. establish exactly one causal predecessor (`predecessor.hash` over the
24//!    exact signed PIC PCA COSE bytes);
25//! 3. carry the Proof of Relationship required by the profile;
26//! 4. keep or attenuate the predecessor materialized authority — the
27//!    candidate never states the resulting authority, the settlement
28//!    authority materializes it;
29//! 5. bind the concrete request when required;
30//! 6. produce the candidate artifacts: Transition COSE → candidate
31//!    Continuity COSE → candidate PIC Token JWT, all signed by the same
32//!    PoR-bound workload key.
33
34use crate::artifacts::token::{PicTokenClaims, sign_token};
35use crate::artifacts::{
36    AttenuationsWire, BitmapAttenuation, ContractAdditions, PicContinuityPayload, PicPcaCose,
37    PicPcaPayload, PicTransitionCose, PicTransitionPayload, Predecessor, ProofOfRelationship,
38    TransitionChallenge, artifact_sha256,
39};
40use crate::authority::attenuation::{Attenuations, materialize};
41use crate::error::{ContinuityError, RejectReason};
42use crate::trust::{ArtifactSigner, ArtifactVerifier};
43
44/// What the workload asks the next checkpoint to be.
45#[derive(Debug, Clone, Default)]
46pub struct CandidateRequest {
47    /// Requested attenuations (empty = keep the authority unchanged).
48    pub attenuations: Attenuations,
49    /// Fresh challenge material for the next transition.
50    pub next_challenge: Vec<u8>,
51    /// Proof of Relationship evidence for this hop.
52    pub proof_of_relationship: Option<ProofOfRelationship>,
53    /// Optional request/execution binding digest.
54    pub request_digest: Option<Vec<u8>>,
55    /// Optional executor evidence, when the deployment requires it.
56    pub executor_evidence: Option<ciborium::Value>,
57    /// Candidate JWT claims. `iss` is optional identity metadata in the
58    /// centralized profile; `aud` conventionally names the settlement
59    /// authority.
60    pub iss: Option<String>,
61    /// Candidate JWT audience; conventionally the settlement authority.
62    pub aud: Option<String>,
63    /// Candidate JWT issued-at (seconds since the Unix epoch).
64    pub iat: Option<i64>,
65}
66
67/// The three workload-signed candidate artifacts.
68#[derive(Debug, Clone)]
69pub struct CandidateArtifacts {
70    /// The candidate PIC Token JWT (compact JWS), ready to be submitted as
71    /// the RFC 8693 `subject_token`.
72    pub token: String,
73    /// Exact signed candidate PIC Continuity COSE bytes.
74    pub continuity_bytes: Vec<u8>,
75    /// Exact signed PIC Continuity Transition COSE bytes.
76    pub transition_bytes: Vec<u8>,
77    /// The transition payload as built.
78    pub transition: PicTransitionPayload,
79}
80
81/// Builds a candidate advancement from the exact signed bytes of the
82/// current trusted PIC PCA COSE checkpoint.
83///
84/// When `realm` is provided, the predecessor checkpoint signature is
85/// verified first (the Prover applies the Verifier procedure to its
86/// predecessor); otherwise the payload is parsed and semantically validated
87/// only, which fits workloads that trust their checkpoint delivery channel.
88pub fn build_candidate(
89    current_pca_bytes: &[u8],
90    request: CandidateRequest,
91    workload: &dyn ArtifactSigner,
92    realm: Option<&dyn ArtifactVerifier>,
93) -> Result<CandidateArtifacts, ContinuityError> {
94    // 1. Validate the predecessor continuity state.
95    let pca_cose = PicPcaCose::from_bytes(current_pca_bytes)?;
96    let checkpoint: PicPcaPayload = match realm {
97        Some(v) => pca_cose.verify_with(|data, sig| {
98            if v.verify(data, sig) {
99                Ok(())
100            } else {
101                Err(crate::cose::CoseError::VerificationFailed)
102            }
103        })?,
104        None => pca_cose.payload_unverified()?,
105    };
106    checkpoint.validate()?;
107
108    if request.next_challenge.is_empty() {
109        return Err(RejectReason::NextChallengeInvalid.into());
110    }
111    let por = request
112        .proof_of_relationship
113        .ok_or_else(|| RejectReason::PorRejected("proof_of_relationship is required".into()))?;
114
115    // 4. The Prover checks its own attenuations against the predecessor: an
116    //    invalid candidate would be rejected at settlement anyway.
117    materialize(&checkpoint.context_of_authority, &request.attenuations)?;
118
119    // 2-5. Assemble the transition: exactly one predecessor, challenge
120    //      continuity, attenuations, PoR, optional bindings.
121    let attenuations_wire = to_wire(&request.attenuations);
122    let transition = PicTransitionPayload {
123        profile: crate::PROFILE_0_2.to_string(),
124        position: checkpoint.position + 1,
125        predecessor: Predecessor {
126            predecessor_type: crate::PREDECESSOR_TYPE_PCA.to_string(),
127            hash: artifact_sha256(current_pca_bytes),
128        },
129        challenge: TransitionChallenge {
130            previous_challenge: checkpoint.challenge.next_challenge.clone(),
131            next_challenge: request.next_challenge,
132        },
133        attenuations: attenuations_wire,
134        proof_of_relationship: por,
135        request_digest: request.request_digest,
136        executor_evidence: request.executor_evidence,
137    };
138
139    // 6. Sign the three candidate artifacts with the same workload key.
140    let transition_cose: PicTransitionCose = crate::cose::CoseSigned::sign_with(
141        &transition,
142        workload.kid(),
143        workload.cose_algorithm(),
144        |data| workload.sign(data),
145    )?;
146    let transition_bytes = transition_cose.to_bytes()?;
147
148    let continuity =
149        PicContinuityPayload::candidate(current_pca_bytes.to_vec(), transition_bytes.clone());
150    let continuity_cose: crate::artifacts::PicContinuityCose = crate::cose::CoseSigned::sign_with(
151        &continuity,
152        workload.kid(),
153        workload.cose_algorithm(),
154        |data| workload.sign(data),
155    )?;
156    let continuity_bytes = continuity_cose.to_bytes()?;
157
158    let mut claims = PicTokenClaims::for_continuity(&continuity_bytes);
159    claims.iss = request.iss;
160    claims.aud = request.aud;
161    claims.iat = request.iat;
162    claims.exp = checkpoint.expires_at;
163    claims.jti = checkpoint.lineage_id.clone();
164    let token = sign_token(&claims, workload)?;
165
166    Ok(CandidateArtifacts {
167        token,
168        continuity_bytes,
169        transition_bytes,
170        transition,
171    })
172}
173
174fn to_wire(attenuations: &Attenuations) -> Option<AttenuationsWire> {
175    if attenuations.is_empty() {
176        return None;
177    }
178    Some(AttenuationsWire {
179        identity_context: attenuations
180            .identity_context
181            .as_ref()
182            .map(|b| BitmapAttenuation {
183                remove_bitmap: b.bytes().to_vec(),
184            }),
185        invariants: attenuations.invariants.as_ref().map(|b| BitmapAttenuation {
186            remove_bitmap: b.bytes().to_vec(),
187        }),
188        execution_contract: if attenuations.execution_contract_additions.is_empty() {
189            None
190        } else {
191            Some(ContractAdditions {
192                additions: attenuations.execution_contract_additions.clone(),
193            })
194        },
195    })
196}