use crate::pca::{Constraints, ExecutorBinding, KeyMaterial};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProofOfIdentity {
pub r#type: String,
#[serde(with = "serde_bytes")]
pub value: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProofOfPossession {
pub r#type: String,
#[serde(with = "serde_bytes")]
pub value: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChallengeResponse {
pub r#type: String,
#[serde(with = "serde_bytes")]
pub value: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Proof {
pub poi: ProofOfIdentity,
pub pop: ProofOfPossession,
#[serde(skip_serializing_if = "Option::is_none")]
pub challenge: Option<ChallengeResponse>,
pub key_material: KeyMaterial,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Successor {
pub ops: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub executor: Option<ExecutorBinding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<Constraints>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PocPayload {
#[serde(with = "serde_bytes")]
pub predecessor: Vec<u8>,
pub successor: Successor,
pub proof: Proof,
}
impl PocPayload {
pub fn to_cbor(&self) -> Result<Vec<u8>, ciborium::ser::Error<std::io::Error>> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf)?;
Ok(buf)
}
pub fn from_cbor(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>> {
ciborium::from_reader(bytes)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
#[derive(Debug, Clone)]
pub struct PocBuilder {
predecessor: Vec<u8>,
ops: Vec<String>,
executor: Option<ExecutorBinding>,
constraints: Option<Constraints>,
poi: Option<ProofOfIdentity>,
pop: Option<ProofOfPossession>,
challenge: Option<ChallengeResponse>,
key_material: Option<KeyMaterial>,
}
impl PocBuilder {
pub fn new(predecessor_cose_bytes: Vec<u8>) -> Self {
Self {
predecessor: predecessor_cose_bytes,
ops: Vec::new(),
executor: None,
constraints: None,
poi: None,
pop: None,
challenge: None,
key_material: None,
}
}
pub fn ops(mut self, ops: Vec<String>) -> Self {
self.ops = ops;
self
}
pub fn executor(mut self, binding: ExecutorBinding) -> Self {
self.executor = Some(binding);
self
}
pub fn constraints(mut self, constraints: Constraints) -> Self {
self.constraints = Some(constraints);
self
}
pub fn poi(mut self, poi_type: &str, value: Vec<u8>) -> Self {
self.poi = Some(ProofOfIdentity {
r#type: poi_type.into(),
value,
});
self
}
pub fn pop(mut self, pop_type: &str, value: Vec<u8>) -> Self {
self.pop = Some(ProofOfPossession {
r#type: pop_type.into(),
value,
});
self
}
pub fn challenge(mut self, challenge_type: &str, value: Vec<u8>) -> Self {
self.challenge = Some(ChallengeResponse {
r#type: challenge_type.into(),
value,
});
self
}
pub fn key_material(mut self, public_key: Vec<u8>, alg: &str) -> Self {
self.key_material = Some(KeyMaterial {
public_key,
alg: alg.into(),
});
self
}
pub fn build(self) -> Result<PocPayload, &'static str> {
let poi = self.poi.ok_or("PoI is required")?;
let pop = self.pop.ok_or("PoP is required")?;
let key_material = self.key_material.ok_or("Key material is required")?;
if self.ops.is_empty() {
return Err("Ops cannot be empty");
}
Ok(PocPayload {
predecessor: self.predecessor,
successor: Successor {
ops: self.ops,
executor: self.executor,
constraints: self.constraints,
},
proof: Proof {
poi,
pop,
challenge: self.challenge,
key_material,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pca::{Executor, ExecutorBinding, PcaPayload, TemporalConstraints};
fn sample_predecessor_bytes() -> Vec<u8> {
let pca = PcaPayload {
hop: "gateway".into(),
p_0: "https://idp.example.com/users/alice".into(),
ops: vec!["read:/user/*".into(), "write:/user/*".into()],
executor: Executor {
binding: ExecutorBinding::new().with("org", "acme"),
},
provenance: None,
constraints: None,
};
pca.to_cbor().unwrap()
}
#[test]
fn test_poc_cbor_roundtrip() {
let poc = PocPayload {
predecessor: sample_predecessor_bytes(),
successor: Successor {
ops: vec!["read:/user/*".into()],
executor: Some(ExecutorBinding::new().with("namespace", "prod")),
constraints: Some(Constraints {
temporal: Some(TemporalConstraints {
iat: None,
exp: Some("2025-12-11T10:30:00Z".into()),
nbf: None,
}),
}),
},
proof: Proof {
poi: ProofOfIdentity {
r#type: "spiffe_svid".into(),
value: vec![0x01, 0x02, 0x03],
},
pop: ProofOfPossession {
r#type: "signature".into(),
value: vec![0x04, 0x05, 0x06],
},
challenge: Some(ChallengeResponse {
r#type: "nonce".into(),
value: vec![0x07, 0x08, 0x09],
}),
key_material: KeyMaterial {
public_key: vec![0u8; 32],
alg: "EdDSA".into(),
},
},
};
let cbor = poc.to_cbor().unwrap();
let decoded = PocPayload::from_cbor(&cbor).unwrap();
assert_eq!(poc, decoded);
assert_eq!(decoded.successor.ops, vec!["read:/user/*"]);
}
#[test]
fn test_poc_json_roundtrip() {
let poc = PocPayload {
predecessor: sample_predecessor_bytes(),
successor: Successor {
ops: vec!["read:/user/*".into()],
executor: None,
constraints: None,
},
proof: Proof {
poi: ProofOfIdentity {
r#type: "jwt".into(),
value: b"eyJhbGciOiJFUzI1NiJ9...".to_vec(),
},
pop: ProofOfPossession {
r#type: "signature".into(),
value: vec![0xAB; 64],
},
challenge: None,
key_material: KeyMaterial {
public_key: vec![0u8; 32],
alg: "ES256".into(),
},
},
};
let json = poc.to_json().unwrap();
let decoded = PocPayload::from_json(&json).unwrap();
assert_eq!(poc, decoded);
}
#[test]
fn test_poc_builder() {
let poc = PocBuilder::new(sample_predecessor_bytes())
.ops(vec!["read:/user/*".into()])
.executor(ExecutorBinding::new().with("namespace", "prod"))
.poi("spiffe_svid", vec![0x01, 0x02])
.pop("signature", vec![0x03, 0x04])
.challenge("nonce", vec![0x05, 0x06])
.key_material(vec![0u8; 32], "EdDSA")
.build()
.unwrap();
assert_eq!(poc.successor.ops, vec!["read:/user/*"]);
assert!(poc.successor.executor.is_some());
assert!(poc.proof.challenge.is_some());
}
#[test]
fn test_poc_builder_minimal() {
let poc = PocBuilder::new(sample_predecessor_bytes())
.ops(vec!["read:/user/*".into()])
.poi("jwt", vec![0x01])
.pop("signature", vec![0x02])
.key_material(vec![0u8; 32], "EdDSA")
.build()
.unwrap();
assert!(poc.successor.executor.is_none());
assert!(poc.successor.constraints.is_none());
assert!(poc.proof.challenge.is_none());
}
#[test]
fn test_poc_builder_missing_required() {
let result = PocBuilder::new(sample_predecessor_bytes())
.ops(vec!["read:/user/*".into()])
.poi("jwt", vec![0x01])
.key_material(vec![0u8; 32], "EdDSA")
.build();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "PoP is required");
}
#[test]
fn test_poc_builder_empty_ops() {
let result = PocBuilder::new(sample_predecessor_bytes())
.poi("jwt", vec![0x01])
.pop("signature", vec![0x02])
.key_material(vec![0u8; 32], "EdDSA")
.build();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Ops cannot be empty");
}
#[test]
fn test_monotonicity_example() {
let poc = PocBuilder::new(sample_predecessor_bytes())
.ops(vec!["read:/user/*".into()])
.poi("spiffe_svid", vec![0x01])
.pop("signature", vec![0x02])
.key_material(vec![0u8; 32], "EdDSA")
.build()
.unwrap();
assert_eq!(poc.successor.ops.len(), 1);
}
}