Skip to main content

pic_continuity/artifacts/
continuity.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 Continuity COSE (`pic-continuity+cose`): the signed continuity
18//! container carrying a trusted PCA checkpoint and either no proposed
19//! transitions (settled, `null`) or exactly one proposed transition
20//! (candidate).
21
22use super::{artifact_sha256, check_profile};
23use crate::cose::CoseSigned;
24use crate::error::RejectReason;
25use serde::{Deserialize, Serialize};
26
27/// The current trusted checkpoint carried by a Continuity: exact signed
28/// PIC PCA COSE bytes and their SHA-256.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct ContinuityRoot {
31    /// SHA-256 of `pca`. Verifiers recompute it from `pca` and never trust
32    /// the stored value ([`PicContinuityPayload::check_root_hash`]).
33    #[serde(with = "serde_bytes")]
34    pub pca_hash: Vec<u8>,
35    /// Exact signed bytes of the current PIC PCA COSE checkpoint.
36    #[serde(with = "serde_bytes")]
37    pub pca: Vec<u8>,
38}
39
40/// PIC Continuity COSE payload.
41///
42/// `transitions` is always semantically present: `None` (CBOR null) means
43/// settled; a candidate carries exactly one workload-signed PIC Continuity
44/// Transition COSE.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct PicContinuityPayload {
47    /// PIC profile identifier; must equal [`crate::PROFILE_0_2`].
48    pub profile: String,
49    /// The current trusted checkpoint (exact bytes plus digest).
50    pub root: ContinuityRoot,
51    /// `None` (CBOR null) for a settled state; exactly one workload-signed
52    /// PIC Continuity Transition COSE (exact bytes) for a candidate.
53    pub transitions: Option<Vec<serde_bytes::ByteBuf>>,
54}
55
56impl PicContinuityPayload {
57    fn root_for(exact_pca_bytes: Vec<u8>) -> ContinuityRoot {
58        ContinuityRoot {
59            pca_hash: artifact_sha256(&exact_pca_bytes),
60            pca: exact_pca_bytes,
61        }
62    }
63
64    /// A settled Continuity: `transitions = null`.
65    pub fn settled(exact_pca_bytes: Vec<u8>) -> Self {
66        Self {
67            profile: crate::PROFILE_0_2.to_string(),
68            root: Self::root_for(exact_pca_bytes),
69            transitions: None,
70        }
71    }
72
73    /// A candidate Continuity: exactly one proposed transition.
74    pub fn candidate(exact_pca_bytes: Vec<u8>, exact_transition_bytes: Vec<u8>) -> Self {
75        Self {
76            profile: crate::PROFILE_0_2.to_string(),
77            root: Self::root_for(exact_pca_bytes),
78            transitions: Some(vec![serde_bytes::ByteBuf::from(exact_transition_bytes)]),
79        }
80    }
81
82    /// `true` when `transitions` is null (settled state).
83    pub fn is_settled(&self) -> bool {
84        self.transitions.is_none()
85    }
86
87    /// For a settled Continuity, `transitions` must be null.
88    pub fn require_settled(&self) -> Result<(), RejectReason> {
89        if self.is_settled() {
90            Ok(())
91        } else {
92            Err(RejectReason::SettledCarriesTransitions)
93        }
94    }
95
96    /// For a candidate, returns the exact bytes of the single transition.
97    pub fn candidate_transition(&self) -> Result<&[u8], RejectReason> {
98        match &self.transitions {
99            Some(list) if list.len() == 1 => Ok(list[0].as_ref()),
100            Some(list) => Err(RejectReason::TransitionCount(list.len())),
101            None => Err(RejectReason::TransitionCount(0)),
102        }
103    }
104
105    /// Recomputes and checks `root.pca_hash` against the exact `root.pca`
106    /// bytes. Verifiers MUST recompute; the stored digest is never trusted.
107    pub fn check_root_hash(&self) -> Result<(), RejectReason> {
108        if artifact_sha256(&self.root.pca) == self.root.pca_hash {
109            Ok(())
110        } else {
111            Err(RejectReason::PcaHashMismatch)
112        }
113    }
114
115    /// Rejects the payload unless `profile` is [`crate::PROFILE_0_2`].
116    pub fn check_profile(&self) -> Result<(), RejectReason> {
117        check_profile("pic-continuity+cose", &self.profile)
118    }
119}
120
121/// COSE-signed Continuity.
122pub type PicContinuityCose = CoseSigned<PicContinuityPayload>;
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn continuity_settled_vs_candidate() {
130        let pca_bytes = b"exact-signed-pca-bytes".to_vec();
131
132        let settled = PicContinuityPayload::settled(pca_bytes.clone());
133        assert!(settled.is_settled());
134        assert!(settled.require_settled().is_ok());
135        assert!(settled.check_root_hash().is_ok());
136        assert_eq!(
137            settled.candidate_transition().unwrap_err(),
138            RejectReason::TransitionCount(0)
139        );
140
141        let candidate =
142            PicContinuityPayload::candidate(pca_bytes.clone(), b"transition-bytes".to_vec());
143        assert!(!candidate.is_settled());
144        assert_eq!(
145            candidate.require_settled().unwrap_err(),
146            RejectReason::SettledCarriesTransitions
147        );
148        assert_eq!(
149            candidate.candidate_transition().unwrap(),
150            b"transition-bytes"
151        );
152
153        // Two transitions are unrepresentable through the constructors and
154        // rejected when arriving from the wire.
155        let mut two = candidate.clone();
156        two.transitions = Some(vec![
157            serde_bytes::ByteBuf::from(b"t1".to_vec()),
158            serde_bytes::ByteBuf::from(b"t2".to_vec()),
159        ]);
160        assert_eq!(
161            two.candidate_transition().unwrap_err(),
162            RejectReason::TransitionCount(2)
163        );
164    }
165
166    #[test]
167    fn root_hash_recomputed_not_trusted() {
168        let mut settled = PicContinuityPayload::settled(b"pca".to_vec());
169        settled.root.pca_hash[0] ^= 0xFF;
170        assert_eq!(
171            settled.check_root_hash().unwrap_err(),
172            RejectReason::PcaHashMismatch
173        );
174    }
175
176    #[test]
177    fn transitions_null_is_explicit_on_the_wire() {
178        let settled = PicContinuityPayload::settled(b"pca".to_vec());
179        let mut buf = Vec::new();
180        ciborium::into_writer(&settled, &mut buf).unwrap();
181        let decoded: PicContinuityPayload = ciborium::from_reader(buf.as_slice()).unwrap();
182        assert!(decoded.is_settled());
183    }
184}