1use crate::pca::{Constraints, ExecutorBinding, KeyMaterial};
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct ProofOfIdentity {
34 pub r#type: String,
35 #[serde(with = "serde_bytes")]
36 pub value: Vec<u8>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct ProofOfPossession {
44 pub r#type: String,
45 #[serde(with = "serde_bytes")]
46 pub value: Vec<u8>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct ChallengeResponse {
54 pub r#type: String,
55 #[serde(with = "serde_bytes")]
56 pub value: Vec<u8>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct Proof {
62 pub poi: ProofOfIdentity,
64 pub pop: ProofOfPossession,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub challenge: Option<ChallengeResponse>,
69 pub key_material: KeyMaterial,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct Successor {
82 pub ops: Vec<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub executor: Option<ExecutorBinding>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub constraints: Option<Constraints>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
105pub struct PocPayload {
106 #[serde(with = "serde_bytes")]
108 pub predecessor: Vec<u8>,
109 pub successor: Successor,
111 pub proof: Proof,
113}
114
115impl PocPayload {
116 pub fn to_cbor(&self) -> Result<Vec<u8>, ciborium::ser::Error<std::io::Error>> {
118 let mut buf = Vec::new();
119 ciborium::into_writer(self, &mut buf)?;
120 Ok(buf)
121 }
122
123 pub fn from_cbor(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>> {
125 ciborium::from_reader(bytes)
126 }
127
128 pub fn to_json(&self) -> Result<String, serde_json::Error> {
130 serde_json::to_string(self)
131 }
132
133 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
135 serde_json::to_string_pretty(self)
136 }
137
138 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
140 serde_json::from_str(json)
141 }
142}
143
144#[derive(Debug, Clone)]
150pub struct PocBuilder {
151 predecessor: Vec<u8>,
152 ops: Vec<String>,
153 executor: Option<ExecutorBinding>,
154 constraints: Option<Constraints>,
155 poi: Option<ProofOfIdentity>,
156 pop: Option<ProofOfPossession>,
157 challenge: Option<ChallengeResponse>,
158 key_material: Option<KeyMaterial>,
159}
160
161impl PocBuilder {
162 pub fn new(predecessor_cose_bytes: Vec<u8>) -> Self {
164 Self {
165 predecessor: predecessor_cose_bytes,
166 ops: Vec::new(),
167 executor: None,
168 constraints: None,
169 poi: None,
170 pop: None,
171 challenge: None,
172 key_material: None,
173 }
174 }
175
176 pub fn ops(mut self, ops: Vec<String>) -> Self {
178 self.ops = ops;
179 self
180 }
181
182 pub fn executor(mut self, binding: ExecutorBinding) -> Self {
184 self.executor = Some(binding);
185 self
186 }
187
188 pub fn constraints(mut self, constraints: Constraints) -> Self {
190 self.constraints = Some(constraints);
191 self
192 }
193
194 pub fn poi(mut self, poi_type: &str, value: Vec<u8>) -> Self {
196 self.poi = Some(ProofOfIdentity {
197 r#type: poi_type.into(),
198 value,
199 });
200 self
201 }
202
203 pub fn pop(mut self, pop_type: &str, value: Vec<u8>) -> Self {
205 self.pop = Some(ProofOfPossession {
206 r#type: pop_type.into(),
207 value,
208 });
209 self
210 }
211
212 pub fn challenge(mut self, challenge_type: &str, value: Vec<u8>) -> Self {
214 self.challenge = Some(ChallengeResponse {
215 r#type: challenge_type.into(),
216 value,
217 });
218 self
219 }
220
221 pub fn key_material(mut self, public_key: Vec<u8>, alg: &str) -> Self {
223 self.key_material = Some(KeyMaterial {
224 public_key,
225 alg: alg.into(),
226 });
227 self
228 }
229
230 pub fn build(self) -> Result<PocPayload, &'static str> {
232 let poi = self.poi.ok_or("PoI is required")?;
233 let pop = self.pop.ok_or("PoP is required")?;
234 let key_material = self.key_material.ok_or("Key material is required")?;
235
236 if self.ops.is_empty() {
237 return Err("Ops cannot be empty");
238 }
239
240 Ok(PocPayload {
241 predecessor: self.predecessor,
242 successor: Successor {
243 ops: self.ops,
244 executor: self.executor,
245 constraints: self.constraints,
246 },
247 proof: Proof {
248 poi,
249 pop,
250 challenge: self.challenge,
251 key_material,
252 },
253 })
254 }
255}
256
257#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::pca::{Executor, ExecutorBinding, PcaPayload, TemporalConstraints};
265
266 fn sample_predecessor_bytes() -> Vec<u8> {
268 let pca = PcaPayload {
269 hop: "gateway".into(),
270 p_0: "https://idp.example.com/users/alice".into(),
271 ops: vec!["read:/user/*".into(), "write:/user/*".into()],
272 executor: Executor {
273 binding: ExecutorBinding::new().with("org", "acme"),
274 },
275 provenance: None,
276 constraints: None,
277 };
278 pca.to_cbor().unwrap()
279 }
280
281 #[test]
282 fn test_poc_cbor_roundtrip() {
283 let poc = PocPayload {
284 predecessor: sample_predecessor_bytes(),
285 successor: Successor {
286 ops: vec!["read:/user/*".into()],
287 executor: Some(ExecutorBinding::new().with("namespace", "prod")),
288 constraints: Some(Constraints {
289 temporal: Some(TemporalConstraints {
290 iat: None,
291 exp: Some("2025-12-11T10:30:00Z".into()),
292 nbf: None,
293 }),
294 }),
295 },
296 proof: Proof {
297 poi: ProofOfIdentity {
298 r#type: "spiffe_svid".into(),
299 value: vec![0x01, 0x02, 0x03],
300 },
301 pop: ProofOfPossession {
302 r#type: "signature".into(),
303 value: vec![0x04, 0x05, 0x06],
304 },
305 challenge: Some(ChallengeResponse {
306 r#type: "nonce".into(),
307 value: vec![0x07, 0x08, 0x09],
308 }),
309 key_material: KeyMaterial {
310 public_key: vec![0u8; 32],
311 alg: "EdDSA".into(),
312 },
313 },
314 };
315
316 let cbor = poc.to_cbor().unwrap();
317 let decoded = PocPayload::from_cbor(&cbor).unwrap();
318
319 assert_eq!(poc, decoded);
320 assert_eq!(decoded.successor.ops, vec!["read:/user/*"]);
321 }
322
323 #[test]
324 fn test_poc_json_roundtrip() {
325 let poc = PocPayload {
326 predecessor: sample_predecessor_bytes(),
327 successor: Successor {
328 ops: vec!["read:/user/*".into()],
329 executor: None,
330 constraints: None,
331 },
332 proof: Proof {
333 poi: ProofOfIdentity {
334 r#type: "jwt".into(),
335 value: b"eyJhbGciOiJFUzI1NiJ9...".to_vec(),
336 },
337 pop: ProofOfPossession {
338 r#type: "signature".into(),
339 value: vec![0xAB; 64],
340 },
341 challenge: None,
342 key_material: KeyMaterial {
343 public_key: vec![0u8; 32],
344 alg: "ES256".into(),
345 },
346 },
347 };
348
349 let json = poc.to_json().unwrap();
350 let decoded = PocPayload::from_json(&json).unwrap();
351
352 assert_eq!(poc, decoded);
353 }
354
355 #[test]
356 fn test_poc_builder() {
357 let poc = PocBuilder::new(sample_predecessor_bytes())
358 .ops(vec!["read:/user/*".into()])
359 .executor(ExecutorBinding::new().with("namespace", "prod"))
360 .poi("spiffe_svid", vec![0x01, 0x02])
361 .pop("signature", vec![0x03, 0x04])
362 .challenge("nonce", vec![0x05, 0x06])
363 .key_material(vec![0u8; 32], "EdDSA")
364 .build()
365 .unwrap();
366
367 assert_eq!(poc.successor.ops, vec!["read:/user/*"]);
368 assert!(poc.successor.executor.is_some());
369 assert!(poc.proof.challenge.is_some());
370 }
371
372 #[test]
373 fn test_poc_builder_minimal() {
374 let poc = PocBuilder::new(sample_predecessor_bytes())
375 .ops(vec!["read:/user/*".into()])
376 .poi("jwt", vec![0x01])
377 .pop("signature", vec![0x02])
378 .key_material(vec![0u8; 32], "EdDSA")
379 .build()
380 .unwrap();
381
382 assert!(poc.successor.executor.is_none());
383 assert!(poc.successor.constraints.is_none());
384 assert!(poc.proof.challenge.is_none());
385 }
386
387 #[test]
388 fn test_poc_builder_missing_required() {
389 let result = PocBuilder::new(sample_predecessor_bytes())
390 .ops(vec!["read:/user/*".into()])
391 .poi("jwt", vec![0x01])
392 .key_material(vec![0u8; 32], "EdDSA")
394 .build();
395
396 assert!(result.is_err());
397 assert_eq!(result.unwrap_err(), "PoP is required");
398 }
399
400 #[test]
401 fn test_poc_builder_empty_ops() {
402 let result = PocBuilder::new(sample_predecessor_bytes())
403 .poi("jwt", vec![0x01])
404 .pop("signature", vec![0x02])
405 .key_material(vec![0u8; 32], "EdDSA")
406 .build();
407
408 assert!(result.is_err());
409 assert_eq!(result.unwrap_err(), "Ops cannot be empty");
410 }
411
412 #[test]
413 fn test_monotonicity_example() {
414 let poc = PocBuilder::new(sample_predecessor_bytes())
419 .ops(vec!["read:/user/*".into()])
420 .poi("spiffe_svid", vec![0x01])
421 .pop("signature", vec![0x02])
422 .key_material(vec![0u8; 32], "EdDSA")
423 .build()
424 .unwrap();
425
426 assert_eq!(poc.successor.ops.len(), 1);
427 }
428}