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>, 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 pub fn from_cbor(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>> {
182 ciborium::from_reader(bytes)
183 }
184
185 pub fn to_json(&self) -> Result<String, serde_json::Error> {
187 serde_json::to_string(self)
188 }
189
190 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
192 serde_json::to_string_pretty(self)
193 }
194
195 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
197 serde_json::from_str(json)
198 }
199
200 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#[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 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 pub fn ops(mut self, ops: Vec<String>) -> Self {
232 self.ops = ops;
233 self
234 }
235
236 pub fn executor(mut self, binding: ExecutorBinding) -> Self {
238 self.executor = Some(binding);
239 self
240 }
241
242 pub fn constraints(mut self, constraints: Constraints) -> Self {
244 self.constraints = Some(constraints);
245 self
246 }
247
248 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 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 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}