1use crate::pca::{Constraints, ExecutorBinding};
34use serde::{Deserialize, Serialize};
35
36mod 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
74pub struct ExecutorAttestation {
75 #[serde(rename = "type")]
77 pub attestation_type: String,
78
79 #[serde(with = "serde_bytes")]
82 pub credential: Vec<u8>,
83
84 #[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 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 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 pub fn has_pop(&self) -> bool {
126 self.pop.is_some()
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135pub struct Successor {
136 pub ops: Vec<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub executor: Option<ExecutorBinding>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub constraints: Option<Constraints>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub struct PocPayload {
159 #[serde(with = "serde_bytes")]
162 pub predecessor: Vec<u8>,
163
164 pub successor: Successor,
166
167 pub attestations: Vec<ExecutorAttestation>,
170}
171
172impl PocPayload {
173 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 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 pub fn to_json(&self) -> Result<String, serde_json::Error> {
185 serde_json::to_string(self)
186 }
187
188 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
190 serde_json::to_string_pretty(self)
191 }
192
193 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
195 serde_json::from_str(json)
196 }
197
198 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#[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 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 pub fn ops(mut self, ops: Vec<String>) -> Self {
230 self.ops = ops;
231 self
232 }
233
234 pub fn executor(mut self, binding: ExecutorBinding) -> Self {
236 self.executor = Some(binding);
237 self
238 }
239
240 pub fn constraints(mut self, constraints: Constraints) -> Self {
242 self.constraints = Some(constraints);
243 self
244 }
245
246 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 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 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}