use crate::pca::{Constraints, ExecutorBinding};
use serde::{Deserialize, Serialize};
mod optional_bytes {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(bytes) => serde_bytes::Bytes::new(bytes).serialize(serializer),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(deserializer)?;
Ok(opt.map(|b| b.into_vec()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ExecutorAttestation {
#[serde(rename = "type")]
pub attestation_type: String,
#[serde(with = "serde_bytes")]
pub credential: Vec<u8>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "optional_bytes"
)]
pub pop: Option<Vec<u8>>,
}
impl ExecutorAttestation {
pub fn new(attestation_type: impl Into<String>, credential: Vec<u8>) -> Self {
Self {
attestation_type: attestation_type.into(),
credential,
pop: None,
}
}
pub fn with_pop(
attestation_type: impl Into<String>,
credential: Vec<u8>,
pop: Vec<u8>,
) -> Self {
Self {
attestation_type: attestation_type.into(),
credential,
pop: Some(pop),
}
}
pub fn has_pop(&self) -> bool {
self.pop.is_some()
}
}
#[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 attestations: Vec<ExecutorAttestation>,
}
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)
}
pub fn find_attestation(&self, attestation_type: &str) -> Option<&ExecutorAttestation> {
self.attestations
.iter()
.find(|a| a.attestation_type == attestation_type)
}
}
#[derive(Debug, Clone)]
pub struct PocBuilder {
predecessor: Vec<u8>,
ops: Vec<String>,
executor: Option<ExecutorBinding>,
constraints: Option<Constraints>,
attestations: Vec<ExecutorAttestation>,
}
impl PocBuilder {
pub fn new(predecessor_cose_bytes: Vec<u8>) -> Self {
Self {
predecessor: predecessor_cose_bytes,
ops: Vec::new(),
executor: None,
constraints: None,
attestations: Vec::new(),
}
}
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 attestation(mut self, attestation_type: impl Into<String>, credential: Vec<u8>) -> Self {
self.attestations
.push(ExecutorAttestation::new(attestation_type, credential));
self
}
pub fn attestation_with_pop(
mut self,
attestation_type: impl Into<String>,
credential: Vec<u8>,
pop: Vec<u8>,
) -> Self {
self.attestations.push(ExecutorAttestation::with_pop(
attestation_type,
credential,
pop,
));
self
}
pub fn build(self) -> Result<PocPayload, &'static str> {
if self.ops.is_empty() {
return Err("Ops cannot be empty");
}
if self.attestations.is_empty() {
return Err("At least one attestation is required");
}
Ok(PocPayload {
predecessor: self.predecessor,
successor: Successor {
ops: self.ops,
executor: self.executor,
constraints: self.constraints,
},
attestations: self.attestations,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pca::{Executor, ExecutorBinding, PcaPayload, TemporalConstraints};
fn sample_predecessor_bytes() -> Vec<u8> {
let pca = PcaPayload {
hop: 0,
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,
}),
}),
},
attestations: vec![
ExecutorAttestation::with_pop(
"spiffe_svid",
vec![0x01, 0x02, 0x03],
vec![0x04, 0x05, 0x06],
),
ExecutorAttestation::new("tee_quote", vec![0x07, 0x08, 0x09]),
],
};
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/*"]);
assert_eq!(decoded.attestations.len(), 2);
}
#[test]
fn test_attestation_type_is_string() {
let attestation = ExecutorAttestation::new("custom_type", vec![0x01]);
assert_eq!(attestation.attestation_type, "custom_type");
let attestation = ExecutorAttestation::new("spiffe_svid", vec![0x01]);
assert_eq!(attestation.attestation_type, "spiffe_svid");
}
#[test]
fn test_attestation_has_pop() {
let with_pop = ExecutorAttestation::with_pop("x509", vec![0x01], vec![0x02]);
assert!(with_pop.has_pop());
let without_pop = ExecutorAttestation::new("vp", vec![0x01]);
assert!(!without_pop.has_pop());
}
#[test]
fn test_find_attestation() {
let poc = PocPayload {
predecessor: sample_predecessor_bytes(),
successor: Successor {
ops: vec!["read:/user/*".into()],
executor: None,
constraints: None,
},
attestations: vec![
ExecutorAttestation::new("spiffe_svid", vec![0x01]),
ExecutorAttestation::new("tee_quote", vec![0x02]),
],
};
assert!(poc.find_attestation("spiffe_svid").is_some());
assert!(poc.find_attestation("tee_quote").is_some());
assert!(poc.find_attestation("vp").is_none());
}
#[test]
fn test_poc_builder() {
let poc = PocBuilder::new(sample_predecessor_bytes())
.ops(vec!["read:/user/*".into()])
.executor(ExecutorBinding::new().with("namespace", "prod"))
.attestation_with_pop("spiffe_svid", vec![0x01, 0x02], vec![0x03, 0x04])
.attestation("tee_quote", vec![0x05, 0x06])
.build()
.unwrap();
assert_eq!(poc.successor.ops, vec!["read:/user/*"]);
assert!(poc.successor.executor.is_some());
assert_eq!(poc.attestations.len(), 2);
}
#[test]
fn test_poc_builder_empty_attestations_fails() {
let result = PocBuilder::new(sample_predecessor_bytes())
.ops(vec!["read:/user/*".into()])
.build();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "At least one attestation is required");
}
#[test]
fn test_poc_builder_empty_ops_fails() {
let result = PocBuilder::new(sample_predecessor_bytes())
.attestation("vp", vec![0x01])
.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()])
.attestation("vp", vec![0x01])
.build()
.unwrap();
assert_eq!(poc.successor.ops.len(), 1);
}
#[test]
fn test_json_roundtrip() {
let poc = PocPayload {
predecessor: sample_predecessor_bytes(),
successor: Successor {
ops: vec!["read:/user/*".into()],
executor: None,
constraints: None,
},
attestations: vec![ExecutorAttestation::new(
"vp",
b"eyJhbGciOiJFUzI1NiJ9...".to_vec(),
)],
};
let json = poc.to_json().unwrap();
let decoded = PocPayload::from_json(&json).unwrap();
assert_eq!(poc, decoded);
}
#[test]
fn test_multiple_attestation_types() {
let poc = PocBuilder::new(sample_predecessor_bytes())
.ops(vec!["read:/user/*".into()])
.attestation_with_pop("spiffe_svid", vec![0x01], vec![0x02])
.attestation("vp", vec![0x03])
.attestation("tee_quote", vec![0x04])
.attestation_with_pop("custom_attestation", vec![0x05], vec![0x06])
.build()
.unwrap();
assert_eq!(poc.attestations.len(), 4);
assert!(poc.find_attestation("spiffe_svid").unwrap().has_pop());
assert!(!poc.find_attestation("vp").unwrap().has_pop());
assert!(!poc.find_attestation("tee_quote").unwrap().has_pop());
assert!(
poc.find_attestation("custom_attestation")
.unwrap()
.has_pop()
);
}
}