pic_pca/
poc.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//! PoC (Proof of Continuity) payload model.
18//!
19//! Defines the PoC data structure for CBOR serialization within COSE_Sign1 envelope.
20//! Based on PIC Spec v0.2.
21//!
22//! Key changes from v0.1:
23//! - `proof.poi` replaced by `attestations[]` (Executor Attestation array)
24//! - `proof.pop` is now per-attestation (inside each attestation that requires it)
25//! - `proof.challenge` moved to COSE protected header
26//! - `proof.key_material` removed (key is extracted from attestation credential)
27//!
28//! The PoC proves causal continuity by demonstrating that the executor:
29//! 1. Holds a valid predecessor PCA
30//! 2. Can attest its identity via one or more attestations
31//! 3. Requests authority that is a subset of the predecessor's
32
33use crate::pca::{Constraints, ExecutorBinding};
34use serde::{Deserialize, Serialize};
35
36/// Custom serializer for `Option<Vec<u8>>` with serde_bytes.
37mod optional_bytes {
38    use serde::{Deserialize, Deserializer, Serialize, Serializer};
39
40    pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
41    where
42        S: Serializer,
43    {
44        match value {
45            Some(bytes) => serde_bytes::Bytes::new(bytes).serialize(serializer),
46            None => serializer.serialize_none(),
47        }
48    }
49
50    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(deserializer)?;
55        Ok(opt.map(|b| b.into_vec()))
56    }
57}
58
59/// Executor Attestation - a verifiable document attesting executor properties.
60///
61/// Replaces the old `ProofOfIdentity` with a more flexible structure that
62/// supports multiple attestation types and per-attestation PoP.
63///
64/// The `attestation_type` is a string to allow extensibility. Common values include:
65/// - `"spiffe_svid"` - SPIFFE SVID (X.509), typically requires PoP
66/// - `"vp"` - Verifiable Presentation, PoP implicit in VP signature
67/// - `"tee_quote"` - TEE Quote (SGX, TDX, SEV), hardware-bound
68/// - `"jwt"` - JWT token
69/// - `"x509"` - Generic X.509 certificate
70///
71/// The PoP (when present) MUST sign `hash(protected_header + payload)` to bind
72/// the attestation to this specific PoC context, preventing replay attacks.
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
74pub struct ExecutorAttestation {
75    /// Attestation type (extensible string, e.g., "spiffe_svid", "vp", "tee_quote")
76    #[serde(rename = "type")]
77    pub attestation_type: String,
78
79    /// The credential bytes (X.509 cert, VP, TEE quote, JWT, etc.)
80    /// Contains or references the public key for verification.
81    #[serde(with = "serde_bytes")]
82    pub credential: Vec<u8>,
83
84    /// Proof of Possession - signature over hash(protected + payload).
85    /// Present only if the attestation type requires it.
86    /// The PoP binds this attestation to this specific PoC context.
87    #[serde(
88        default,
89        skip_serializing_if = "Option::is_none",
90        with = "optional_bytes"
91    )]
92    pub pop: Option<Vec<u8>>,
93}
94
95impl ExecutorAttestation {
96    /// Creates a new attestation without PoP.
97    ///
98    /// Use this for attestation types where PoP is implicit (e.g., VP)
99    /// or not applicable (e.g., TEE quote).
100    pub fn new(attestation_type: impl Into<String>, credential: Vec<u8>) -> Self {
101        Self {
102            attestation_type: attestation_type.into(),
103            credential,
104            pop: None,
105        }
106    }
107
108    /// Creates a new attestation with PoP.
109    ///
110    /// Use this for attestation types that require proof of possession
111    /// (e.g., SPIFFE SVID, X.509 cert, JWT+DPoP).
112    pub fn with_pop(
113        attestation_type: impl Into<String>,
114        credential: Vec<u8>,
115        pop: Vec<u8>,
116    ) -> Self {
117        Self {
118            attestation_type: attestation_type.into(),
119            credential,
120            pop: Some(pop),
121        }
122    }
123
124    /// Returns true if this attestation has a PoP.
125    pub fn has_pop(&self) -> bool {
126        self.pop.is_some()
127    }
128}
129
130/// Successor - proposed authority for the next hop.
131///
132/// Must satisfy monotonicity: `ops ⊆ predecessor.ops`.
133/// Constraints must also be monotonically restricted.
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135pub struct Successor {
136    /// Requested operations (must be subset of predecessor)
137    pub ops: Vec<String>,
138    /// Next executor binding (if known at submission time)
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub executor: Option<ExecutorBinding>,
141    /// Restricted constraints (must be subset of predecessor)
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub constraints: Option<Constraints>,
144}
145
146/// PoC Payload - the CBOR content signed by the executor with COSE_Sign1.
147///
148/// COSE_Sign1 structure:
149/// ```text
150/// protected: { alg, kid, challenge }  <- challenge in header for freshness
151/// payload: { predecessor, successor, attestations }
152/// signature: ...
153/// ```
154///
155/// The `kid` in the protected header identifies which key was used to sign
156/// this PoC. The key can be resolved from one of the attestations.
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub struct PocPayload {
159    /// Predecessor PCA as raw COSE_Sign1 bytes.
160    /// Stored as bytes to preserve original signature for verification.
161    #[serde(with = "serde_bytes")]
162    pub predecessor: Vec<u8>,
163
164    /// Proposed authority for next hop
165    pub successor: Successor,
166
167    /// Executor attestations (replaces PoI).
168    /// Multiple attestations can be provided (identity, environment, capabilities).
169    pub attestations: Vec<ExecutorAttestation>,
170}
171
172impl PocPayload {
173    /// Serializes to CBOR bytes.
174    pub fn to_cbor(&self) -> Result<Vec<u8>, ciborium::ser::Error<std::io::Error>> {
175        let mut buf = Vec::new();
176        ciborium::into_writer(self, &mut buf)?;
177        Ok(buf)
178    }
179
180    /// Deserializes from CBOR bytes.
181    pub fn from_cbor(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>> {
182        ciborium::from_reader(bytes)
183    }
184
185    /// Serializes to JSON string.
186    pub fn to_json(&self) -> Result<String, serde_json::Error> {
187        serde_json::to_string(self)
188    }
189
190    /// Serializes to pretty-printed JSON string.
191    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
192        serde_json::to_string_pretty(self)
193    }
194
195    /// Deserializes from JSON string.
196    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
197        serde_json::from_str(json)
198    }
199
200    /// Finds an attestation by type.
201    pub fn find_attestation(&self, attestation_type: &str) -> Option<&ExecutorAttestation> {
202        self.attestations
203            .iter()
204            .find(|a| a.attestation_type == attestation_type)
205    }
206}
207
208/// Builder for creating PoC payloads.
209#[derive(Debug, Clone)]
210pub struct PocBuilder {
211    predecessor: Vec<u8>,
212    ops: Vec<String>,
213    executor: Option<ExecutorBinding>,
214    constraints: Option<Constraints>,
215    attestations: Vec<ExecutorAttestation>,
216}
217
218impl PocBuilder {
219    /// Creates a new builder with the predecessor PCA bytes.
220    pub fn new(predecessor_cose_bytes: Vec<u8>) -> Self {
221        Self {
222            predecessor: predecessor_cose_bytes,
223            ops: Vec::new(),
224            executor: None,
225            constraints: None,
226            attestations: Vec::new(),
227        }
228    }
229
230    /// Sets the requested operations (must be subset of predecessor).
231    pub fn ops(mut self, ops: Vec<String>) -> Self {
232        self.ops = ops;
233        self
234    }
235
236    /// Sets the next executor binding.
237    pub fn executor(mut self, binding: ExecutorBinding) -> Self {
238        self.executor = Some(binding);
239        self
240    }
241
242    /// Sets the constraints.
243    pub fn constraints(mut self, constraints: Constraints) -> Self {
244        self.constraints = Some(constraints);
245        self
246    }
247
248    /// Adds an attestation without PoP.
249    pub fn attestation(mut self, attestation_type: impl Into<String>, credential: Vec<u8>) -> Self {
250        self.attestations
251            .push(ExecutorAttestation::new(attestation_type, credential));
252        self
253    }
254
255    /// Adds an attestation with PoP.
256    pub fn attestation_with_pop(
257        mut self,
258        attestation_type: impl Into<String>,
259        credential: Vec<u8>,
260        pop: Vec<u8>,
261    ) -> Self {
262        self.attestations.push(ExecutorAttestation::with_pop(
263            attestation_type,
264            credential,
265            pop,
266        ));
267        self
268    }
269
270    /// Builds the PoC payload, returning an error if required fields are missing.
271    pub fn build(self) -> Result<PocPayload, &'static str> {
272        if self.ops.is_empty() {
273            return Err("Ops cannot be empty");
274        }
275
276        if self.attestations.is_empty() {
277            return Err("At least one attestation is required");
278        }
279
280        Ok(PocPayload {
281            predecessor: self.predecessor,
282            successor: Successor {
283                ops: self.ops,
284                executor: self.executor,
285                constraints: self.constraints,
286            },
287            attestations: self.attestations,
288        })
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::pca::{Executor, ExecutorBinding, PcaPayload, TemporalConstraints};
296
297    fn sample_predecessor_bytes() -> Vec<u8> {
298        let pca = PcaPayload {
299            hop: 0,
300            p_0: "https://idp.example.com/users/alice".into(),
301            ops: vec!["read:/user/*".into(), "write:/user/*".into()],
302            executor: Executor {
303                binding: ExecutorBinding::new().with("org", "acme"),
304            },
305            provenance: None,
306            constraints: None,
307        };
308        pca.to_cbor().unwrap()
309    }
310
311    #[test]
312    fn test_poc_cbor_roundtrip() {
313        let poc = PocPayload {
314            predecessor: sample_predecessor_bytes(),
315            successor: Successor {
316                ops: vec!["read:/user/*".into()],
317                executor: Some(ExecutorBinding::new().with("namespace", "prod")),
318                constraints: Some(Constraints {
319                    temporal: Some(TemporalConstraints {
320                        iat: None,
321                        exp: Some("2025-12-11T10:30:00Z".into()),
322                        nbf: None,
323                    }),
324                }),
325            },
326            attestations: vec![
327                ExecutorAttestation::with_pop(
328                    "spiffe_svid",
329                    vec![0x01, 0x02, 0x03],
330                    vec![0x04, 0x05, 0x06],
331                ),
332                ExecutorAttestation::new("tee_quote", vec![0x07, 0x08, 0x09]),
333            ],
334        };
335
336        let cbor = poc.to_cbor().unwrap();
337        let decoded = PocPayload::from_cbor(&cbor).unwrap();
338
339        assert_eq!(poc, decoded);
340        assert_eq!(decoded.successor.ops, vec!["read:/user/*"]);
341        assert_eq!(decoded.attestations.len(), 2);
342    }
343
344    #[test]
345    fn test_attestation_type_is_string() {
346        let attestation = ExecutorAttestation::new("custom_type", vec![0x01]);
347        assert_eq!(attestation.attestation_type, "custom_type");
348
349        let attestation = ExecutorAttestation::new("spiffe_svid", vec![0x01]);
350        assert_eq!(attestation.attestation_type, "spiffe_svid");
351    }
352
353    #[test]
354    fn test_attestation_has_pop() {
355        let with_pop = ExecutorAttestation::with_pop("x509", vec![0x01], vec![0x02]);
356        assert!(with_pop.has_pop());
357
358        let without_pop = ExecutorAttestation::new("vp", vec![0x01]);
359        assert!(!without_pop.has_pop());
360    }
361
362    #[test]
363    fn test_find_attestation() {
364        let poc = PocPayload {
365            predecessor: sample_predecessor_bytes(),
366            successor: Successor {
367                ops: vec!["read:/user/*".into()],
368                executor: None,
369                constraints: None,
370            },
371            attestations: vec![
372                ExecutorAttestation::new("spiffe_svid", vec![0x01]),
373                ExecutorAttestation::new("tee_quote", vec![0x02]),
374            ],
375        };
376
377        assert!(poc.find_attestation("spiffe_svid").is_some());
378        assert!(poc.find_attestation("tee_quote").is_some());
379        assert!(poc.find_attestation("vp").is_none());
380    }
381
382    #[test]
383    fn test_poc_builder() {
384        let poc = PocBuilder::new(sample_predecessor_bytes())
385            .ops(vec!["read:/user/*".into()])
386            .executor(ExecutorBinding::new().with("namespace", "prod"))
387            .attestation_with_pop("spiffe_svid", vec![0x01, 0x02], vec![0x03, 0x04])
388            .attestation("tee_quote", vec![0x05, 0x06])
389            .build()
390            .unwrap();
391
392        assert_eq!(poc.successor.ops, vec!["read:/user/*"]);
393        assert!(poc.successor.executor.is_some());
394        assert_eq!(poc.attestations.len(), 2);
395    }
396
397    #[test]
398    fn test_poc_builder_empty_attestations_fails() {
399        let result = PocBuilder::new(sample_predecessor_bytes())
400            .ops(vec!["read:/user/*".into()])
401            .build();
402
403        assert!(result.is_err());
404        assert_eq!(result.unwrap_err(), "At least one attestation is required");
405    }
406
407    #[test]
408    fn test_poc_builder_empty_ops_fails() {
409        let result = PocBuilder::new(sample_predecessor_bytes())
410            .attestation("vp", vec![0x01])
411            .build();
412
413        assert!(result.is_err());
414        assert_eq!(result.unwrap_err(), "Ops cannot be empty");
415    }
416
417    #[test]
418    fn test_monotonicity_example() {
419        let poc = PocBuilder::new(sample_predecessor_bytes())
420            .ops(vec!["read:/user/*".into()])
421            .attestation("vp", vec![0x01])
422            .build()
423            .unwrap();
424
425        assert_eq!(poc.successor.ops.len(), 1);
426    }
427
428    #[test]
429    fn test_json_roundtrip() {
430        let poc = PocPayload {
431            predecessor: sample_predecessor_bytes(),
432            successor: Successor {
433                ops: vec!["read:/user/*".into()],
434                executor: None,
435                constraints: None,
436            },
437            attestations: vec![ExecutorAttestation::new(
438                "vp",
439                b"eyJhbGciOiJFUzI1NiJ9...".to_vec(),
440            )],
441        };
442
443        let json = poc.to_json().unwrap();
444        let decoded = PocPayload::from_json(&json).unwrap();
445
446        assert_eq!(poc, decoded);
447    }
448
449    #[test]
450    fn test_multiple_attestation_types() {
451        let poc = PocBuilder::new(sample_predecessor_bytes())
452            .ops(vec!["read:/user/*".into()])
453            .attestation_with_pop("spiffe_svid", vec![0x01], vec![0x02])
454            .attestation("vp", vec![0x03])
455            .attestation("tee_quote", vec![0x04])
456            .attestation_with_pop("custom_attestation", vec![0x05], vec![0x06])
457            .build()
458            .unwrap();
459
460        assert_eq!(poc.attestations.len(), 4);
461        assert!(poc.find_attestation("spiffe_svid").unwrap().has_pop());
462        assert!(!poc.find_attestation("vp").unwrap().has_pop());
463        assert!(!poc.find_attestation("tee_quote").unwrap().has_pop());
464        assert!(
465            poc.find_attestation("custom_attestation")
466                .unwrap()
467                .has_pop()
468        );
469    }
470}