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>, Box<dyn std::error::Error + Send + Sync>> {
175        cbor4ii::serde::to_vec(Vec::new(), self).map_err(|e| e.into())
176    }
177
178    /// Deserializes from CBOR bytes.
179    pub fn from_cbor(bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
180        cbor4ii::serde::from_slice(bytes).map_err(|e| e.into())
181    }
182
183    /// Serializes to JSON string.
184    pub fn to_json(&self) -> Result<String, serde_json::Error> {
185        serde_json::to_string(self)
186    }
187
188    /// Serializes to pretty-printed JSON string.
189    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
190        serde_json::to_string_pretty(self)
191    }
192
193    /// Deserializes from JSON string.
194    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
195        serde_json::from_str(json)
196    }
197
198    /// Finds an attestation by type.
199    pub fn find_attestation(&self, attestation_type: &str) -> Option<&ExecutorAttestation> {
200        self.attestations
201            .iter()
202            .find(|a| a.attestation_type == attestation_type)
203    }
204}
205
206/// Builder for creating PoC payloads.
207#[derive(Debug, Clone)]
208pub struct PocBuilder {
209    predecessor: Vec<u8>,
210    ops: Vec<String>,
211    executor: Option<ExecutorBinding>,
212    constraints: Option<Constraints>,
213    attestations: Vec<ExecutorAttestation>,
214}
215
216impl PocBuilder {
217    /// Creates a new builder with the predecessor PCA bytes.
218    pub fn new(predecessor_cose_bytes: Vec<u8>) -> Self {
219        Self {
220            predecessor: predecessor_cose_bytes,
221            ops: Vec::new(),
222            executor: None,
223            constraints: None,
224            attestations: Vec::new(),
225        }
226    }
227
228    /// Sets the requested operations (must be subset of predecessor).
229    pub fn ops(mut self, ops: Vec<String>) -> Self {
230        self.ops = ops;
231        self
232    }
233
234    /// Sets the next executor binding.
235    pub fn executor(mut self, binding: ExecutorBinding) -> Self {
236        self.executor = Some(binding);
237        self
238    }
239
240    /// Sets the constraints.
241    pub fn constraints(mut self, constraints: Constraints) -> Self {
242        self.constraints = Some(constraints);
243        self
244    }
245
246    /// Adds an attestation without PoP.
247    pub fn attestation(mut self, attestation_type: impl Into<String>, credential: Vec<u8>) -> Self {
248        self.attestations
249            .push(ExecutorAttestation::new(attestation_type, credential));
250        self
251    }
252
253    /// Adds an attestation with PoP.
254    pub fn attestation_with_pop(
255        mut self,
256        attestation_type: impl Into<String>,
257        credential: Vec<u8>,
258        pop: Vec<u8>,
259    ) -> Self {
260        self.attestations.push(ExecutorAttestation::with_pop(
261            attestation_type,
262            credential,
263            pop,
264        ));
265        self
266    }
267
268    /// Builds the PoC payload, returning an error if required fields are missing.
269    pub fn build(self) -> Result<PocPayload, &'static str> {
270        if self.ops.is_empty() {
271            return Err("Ops cannot be empty");
272        }
273
274        if self.attestations.is_empty() {
275            return Err("At least one attestation is required");
276        }
277
278        Ok(PocPayload {
279            predecessor: self.predecessor,
280            successor: Successor {
281                ops: self.ops,
282                executor: self.executor,
283                constraints: self.constraints,
284            },
285            attestations: self.attestations,
286        })
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::pca::{Executor, ExecutorBinding, PcaPayload, TemporalConstraints};
294
295    fn sample_predecessor_bytes() -> Vec<u8> {
296        let pca = PcaPayload {
297            hop: 0,
298            p_0: "https://idp.example.com/users/alice".into(),
299            ops: vec!["read:/user/*".into(), "write:/user/*".into()],
300            executor: Executor {
301                binding: ExecutorBinding::new().with("org", "acme"),
302            },
303            provenance: None,
304            constraints: None,
305        };
306        pca.to_cbor().unwrap()
307    }
308
309    #[test]
310    fn test_poc_cbor_roundtrip() {
311        let poc = PocPayload {
312            predecessor: sample_predecessor_bytes(),
313            successor: Successor {
314                ops: vec!["read:/user/*".into()],
315                executor: Some(ExecutorBinding::new().with("namespace", "prod")),
316                constraints: Some(Constraints {
317                    temporal: Some(TemporalConstraints {
318                        iat: None,
319                        exp: Some("2025-12-11T10:30:00Z".into()),
320                        nbf: None,
321                    }),
322                }),
323            },
324            attestations: vec![
325                ExecutorAttestation::with_pop(
326                    "spiffe_svid",
327                    vec![0x01, 0x02, 0x03],
328                    vec![0x04, 0x05, 0x06],
329                ),
330                ExecutorAttestation::new("tee_quote", vec![0x07, 0x08, 0x09]),
331            ],
332        };
333
334        let cbor = poc.to_cbor().unwrap();
335        let decoded = PocPayload::from_cbor(&cbor).unwrap();
336
337        assert_eq!(poc, decoded);
338        assert_eq!(decoded.successor.ops, vec!["read:/user/*"]);
339        assert_eq!(decoded.attestations.len(), 2);
340    }
341
342    #[test]
343    fn test_attestation_type_is_string() {
344        let attestation = ExecutorAttestation::new("custom_type", vec![0x01]);
345        assert_eq!(attestation.attestation_type, "custom_type");
346
347        let attestation = ExecutorAttestation::new("spiffe_svid", vec![0x01]);
348        assert_eq!(attestation.attestation_type, "spiffe_svid");
349    }
350
351    #[test]
352    fn test_attestation_has_pop() {
353        let with_pop = ExecutorAttestation::with_pop("x509", vec![0x01], vec![0x02]);
354        assert!(with_pop.has_pop());
355
356        let without_pop = ExecutorAttestation::new("vp", vec![0x01]);
357        assert!(!without_pop.has_pop());
358    }
359
360    #[test]
361    fn test_find_attestation() {
362        let poc = PocPayload {
363            predecessor: sample_predecessor_bytes(),
364            successor: Successor {
365                ops: vec!["read:/user/*".into()],
366                executor: None,
367                constraints: None,
368            },
369            attestations: vec![
370                ExecutorAttestation::new("spiffe_svid", vec![0x01]),
371                ExecutorAttestation::new("tee_quote", vec![0x02]),
372            ],
373        };
374
375        assert!(poc.find_attestation("spiffe_svid").is_some());
376        assert!(poc.find_attestation("tee_quote").is_some());
377        assert!(poc.find_attestation("vp").is_none());
378    }
379
380    #[test]
381    fn test_poc_builder() {
382        let poc = PocBuilder::new(sample_predecessor_bytes())
383            .ops(vec!["read:/user/*".into()])
384            .executor(ExecutorBinding::new().with("namespace", "prod"))
385            .attestation_with_pop("spiffe_svid", vec![0x01, 0x02], vec![0x03, 0x04])
386            .attestation("tee_quote", vec![0x05, 0x06])
387            .build()
388            .unwrap();
389
390        assert_eq!(poc.successor.ops, vec!["read:/user/*"]);
391        assert!(poc.successor.executor.is_some());
392        assert_eq!(poc.attestations.len(), 2);
393    }
394
395    #[test]
396    fn test_poc_builder_empty_attestations_fails() {
397        let result = PocBuilder::new(sample_predecessor_bytes())
398            .ops(vec!["read:/user/*".into()])
399            .build();
400
401        assert!(result.is_err());
402        assert_eq!(result.unwrap_err(), "At least one attestation is required");
403    }
404
405    #[test]
406    fn test_poc_builder_empty_ops_fails() {
407        let result = PocBuilder::new(sample_predecessor_bytes())
408            .attestation("vp", vec![0x01])
409            .build();
410
411        assert!(result.is_err());
412        assert_eq!(result.unwrap_err(), "Ops cannot be empty");
413    }
414
415    #[test]
416    fn test_monotonicity_example() {
417        let poc = PocBuilder::new(sample_predecessor_bytes())
418            .ops(vec!["read:/user/*".into()])
419            .attestation("vp", vec![0x01])
420            .build()
421            .unwrap();
422
423        assert_eq!(poc.successor.ops.len(), 1);
424    }
425
426    #[test]
427    fn test_json_roundtrip() {
428        let poc = PocPayload {
429            predecessor: sample_predecessor_bytes(),
430            successor: Successor {
431                ops: vec!["read:/user/*".into()],
432                executor: None,
433                constraints: None,
434            },
435            attestations: vec![ExecutorAttestation::new(
436                "vp",
437                b"eyJhbGciOiJFUzI1NiJ9...".to_vec(),
438            )],
439        };
440
441        let json = poc.to_json().unwrap();
442        let decoded = PocPayload::from_json(&json).unwrap();
443
444        assert_eq!(poc, decoded);
445    }
446
447    #[test]
448    fn test_multiple_attestation_types() {
449        let poc = PocBuilder::new(sample_predecessor_bytes())
450            .ops(vec!["read:/user/*".into()])
451            .attestation_with_pop("spiffe_svid", vec![0x01], vec![0x02])
452            .attestation("vp", vec![0x03])
453            .attestation("tee_quote", vec![0x04])
454            .attestation_with_pop("custom_attestation", vec![0x05], vec![0x06])
455            .build()
456            .unwrap();
457
458        assert_eq!(poc.attestations.len(), 4);
459        assert!(poc.find_attestation("spiffe_svid").unwrap().has_pop());
460        assert!(!poc.find_attestation("vp").unwrap().has_pop());
461        assert!(!poc.find_attestation("tee_quote").unwrap().has_pop());
462        assert!(
463            poc.find_attestation("custom_attestation")
464                .unwrap()
465                .has_pop()
466        );
467    }
468}