Skip to main content

pic_continuity/
trust.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//! Host-supplied trust boundaries.
18//!
19//! The Prover and Verifier are pure: key material, trusted checkpoint state,
20//! revocation, and policy all enter through these traits. Defaults are
21//! provided where the specification makes a check optional or
22//! deployment-defined.
23
24use crate::artifacts::{PicPcaPayload, PicTransitionPayload};
25use crate::authority::indexed::IndexedAuthorityMap;
26use crate::cose::{CoseError, SigningAlgorithm};
27
28// ---------------------------------------------------------------------------
29// Key material
30// ---------------------------------------------------------------------------
31
32/// Signs PIC artifacts (COSE payloads and the JWS envelope) with one key.
33///
34/// Implemented by workloads (candidate artifacts) and by the settlement
35/// authority (checkpoints and settled artifacts).
36pub trait ArtifactSigner {
37    /// Key identifier (SPIFFE ID, DID, URL, …) placed in the COSE protected
38    /// header.
39    fn kid(&self) -> &str;
40    /// COSE algorithm this signer produces.
41    fn cose_algorithm(&self) -> SigningAlgorithm;
42    /// JOSE `alg` value for JWS signatures (for example `"EdDSA"`).
43    fn jws_algorithm(&self) -> &str;
44    /// Signs raw bytes.
45    fn sign(&self, data: &[u8]) -> Result<Vec<u8>, CoseError>;
46}
47
48/// Verifies a raw signature over raw bytes with one key.
49pub trait ArtifactVerifier {
50    /// `true` when `signature` is a valid signature over `data`.
51    fn verify(&self, data: &[u8], signature: &[u8]) -> bool;
52
53    /// The JOSE `alg` this verifier expects for JWS artifacts, when known.
54    fn expected_jws_algorithm(&self) -> Option<&'static str> {
55        None
56    }
57
58    /// The COSE algorithm this verifier expects for COSE artifacts, when
59    /// known.
60    fn expected_cose_algorithm(&self) -> Option<SigningAlgorithm> {
61        None
62    }
63}
64
65#[cfg(feature = "ed25519")]
66mod ed25519_impl {
67    use super::*;
68    use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
69
70    /// Ed25519 [`ArtifactSigner`].
71    #[derive(Debug, Clone)]
72    pub struct Ed25519Signer {
73        key: SigningKey,
74        kid: String,
75    }
76
77    impl Ed25519Signer {
78        /// Wraps a signing key with its key identifier.
79        pub fn new(key: SigningKey, kid: impl Into<String>) -> Self {
80            Self {
81                key,
82                kid: kid.into(),
83            }
84        }
85
86        /// The public key matching this signer.
87        pub fn verifying_key(&self) -> VerifyingKey {
88            self.key.verifying_key()
89        }
90    }
91
92    impl ArtifactSigner for Ed25519Signer {
93        fn kid(&self) -> &str {
94            &self.kid
95        }
96        fn cose_algorithm(&self) -> SigningAlgorithm {
97            SigningAlgorithm::EdDSA
98        }
99        fn jws_algorithm(&self) -> &str {
100            "EdDSA"
101        }
102        fn sign(&self, data: &[u8]) -> Result<Vec<u8>, CoseError> {
103            Ok(self.key.sign(data).to_bytes().to_vec())
104        }
105    }
106
107    /// Ed25519 [`ArtifactVerifier`].
108    #[derive(Debug, Clone)]
109    pub struct Ed25519Verifier {
110        key: VerifyingKey,
111    }
112
113    impl Ed25519Verifier {
114        /// Wraps a public key.
115        pub fn new(key: VerifyingKey) -> Self {
116            Self { key }
117        }
118    }
119
120    impl ArtifactVerifier for Ed25519Verifier {
121        fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
122            let Ok(sig) = Signature::from_slice(signature) else {
123                return false;
124            };
125            self.key.verify(data, &sig).is_ok()
126        }
127
128        fn expected_jws_algorithm(&self) -> Option<&'static str> {
129            Some("EdDSA")
130        }
131
132        fn expected_cose_algorithm(&self) -> Option<SigningAlgorithm> {
133            Some(SigningAlgorithm::EdDSA)
134        }
135    }
136}
137
138#[cfg(feature = "ed25519")]
139pub use ed25519_impl::{Ed25519Signer, Ed25519Verifier};
140
141// ---------------------------------------------------------------------------
142// Settlement-side trust state
143// ---------------------------------------------------------------------------
144
145/// The settlement authority's knowledge of currently trusted checkpoints.
146///
147/// `root.pca` of a candidate must be the **exact signed bytes** of a
148/// currently trusted PIC PCA COSE checkpoint; the store answers that
149/// question. Terminating a checkpoint here is how revocation of a lineage
150/// branch is realized operationally.
151pub trait TrustedCheckpoint {
152    /// `true` when `exact_pca_bytes` are byte-for-byte a currently trusted
153    /// checkpoint.
154    fn is_current_checkpoint(&self, exact_pca_bytes: &[u8]) -> bool;
155}
156
157/// Revocation state lookup. The default accepts everything; deployments
158/// enable it per the PIC Revocation Specification.
159pub trait RevocationCheck {
160    /// `true` when the continuity state rooted at this checkpoint is revoked.
161    fn is_revoked(&self, checkpoint: &PicPcaPayload, exact_pca_bytes: &[u8]) -> bool;
162}
163
164/// No revocation configured.
165#[derive(Debug, Clone, Copy, Default)]
166pub struct NoRevocation;
167
168impl RevocationCheck for NoRevocation {
169    fn is_revoked(&self, _checkpoint: &PicPcaPayload, _exact_pca_bytes: &[u8]) -> bool {
170        false
171    }
172}
173
174/// Deployment policy hooks evaluated during settlement. Every method
175/// defaults to accepting, matching the checks the specification marks as
176/// profile- or deployment-required.
177pub trait SettlementPolicy {
178    /// Request/execution binding validation, when the deployment requires
179    /// it. Receives the whole transition (including `request_digest`).
180    fn request_binding(&self, _transition: &PicTransitionPayload) -> bool {
181        true
182    }
183
184    /// Executor evidence / execution-contract conformance validation, when
185    /// required.
186    fn conformance(&self, _checkpoint: &PicPcaPayload, _transition: &PicTransitionPayload) -> bool {
187        true
188    }
189
190    /// Local policy over the materialized successor authority.
191    fn policy(&self, _checkpoint: &PicPcaPayload, _next_authority: &IndexedAuthorityMap) -> bool {
192        true
193    }
194}
195
196/// The permissive default policy.
197#[derive(Debug, Clone, Copy, Default)]
198pub struct DefaultPolicy;
199
200impl SettlementPolicy for DefaultPolicy {}
201
202/// An in-memory [`TrustedCheckpoint`] store, useful for tests and simple
203/// single-process deployments.
204#[derive(Debug, Clone, Default)]
205pub struct InMemoryCheckpoints {
206    current: Vec<Vec<u8>>,
207}
208
209impl InMemoryCheckpoints {
210    /// An empty store: nothing is trusted yet.
211    pub fn new() -> Self {
212        Self::default()
213    }
214
215    /// Marks exact checkpoint bytes as currently trusted.
216    pub fn insert(&mut self, exact_pca_bytes: Vec<u8>) {
217        self.current.push(exact_pca_bytes);
218    }
219
220    /// Replaces a superseded checkpoint with its successor.
221    pub fn replace(&mut self, old_exact_pca_bytes: &[u8], new_exact_pca_bytes: Vec<u8>) {
222        self.current.retain(|b| b != old_exact_pca_bytes);
223        self.current.push(new_exact_pca_bytes);
224    }
225}
226
227impl TrustedCheckpoint for InMemoryCheckpoints {
228    fn is_current_checkpoint(&self, exact_pca_bytes: &[u8]) -> bool {
229        self.current.iter().any(|b| b == exact_pca_bytes)
230    }
231}