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}