use aitp_core::{Aid, Timestamp};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Tct {
pub version: String,
pub jti: Uuid,
pub issuer: Aid,
pub subject: Aid,
pub audience: Aid,
pub issued_at: Timestamp,
pub expires_at: Timestamp,
pub grants: Vec<String>,
pub binding: TctBinding,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct TctBinding {
pub cnf: String,
}
#[cfg(feature = "experimental-renewal")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct TctRenewalPayload {
pub current_tct: TctEnvelope,
pub pop_nonce: String,
pub pop_signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct TctEnvelope {
pub tct: Tct,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_aid() -> Aid {
Aid::from_ed25519(&[0u8; 32])
}
fn sample_tct() -> Tct {
Tct {
version: "aitp/0.1".into(),
jti: Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
issuer: sample_aid(),
subject: sample_aid(),
audience: sample_aid(),
issued_at: Timestamp(1_700_000_000),
expires_at: Timestamp(1_700_003_600),
grants: vec!["demo.echo".into()],
binding: TctBinding {
cnf: "A".repeat(43),
},
signature: "A".repeat(86),
}
}
#[test]
fn round_trip_minimal_tct() {
let t = sample_tct();
let s = serde_json::to_string(&t).unwrap();
let back: Tct = serde_json::from_str(&s).unwrap();
assert_eq!(back, t);
}
#[test]
fn rejects_unknown_top_level_field() {
let mut v = serde_json::to_value(sample_tct()).unwrap();
v.as_object_mut().unwrap().insert("rogue".into(), json!(1));
let err = serde_json::from_value::<Tct>(v).unwrap_err();
assert!(err.to_string().contains("rogue"), "got: {}", err);
}
#[test]
fn rejects_unknown_binding_field() {
let mut v = serde_json::to_value(sample_tct()).unwrap();
v["binding"]
.as_object_mut()
.unwrap()
.insert("rogue".into(), json!(1));
let err = serde_json::from_value::<Tct>(v).unwrap_err();
assert!(err.to_string().contains("rogue"), "got: {}", err);
}
#[test]
fn extensions_and_evidence_ref_are_not_fields() {
let mut v = serde_json::to_value(sample_tct()).unwrap();
v.as_object_mut()
.unwrap()
.insert("extensions".into(), json!({}));
assert!(serde_json::from_value::<Tct>(v.clone()).is_err());
let mut v = serde_json::to_value(sample_tct()).unwrap();
v.as_object_mut().unwrap().insert(
"evidence_ref".into(),
json!({"sha256": "x", "description": "y"}),
);
assert!(serde_json::from_value::<Tct>(v).is_err());
}
#[test]
fn tct_envelope_round_trips() {
let env = TctEnvelope { tct: sample_tct() };
let s = serde_json::to_string(&env).unwrap();
let back: TctEnvelope = serde_json::from_str(&s).unwrap();
assert_eq!(back, env);
}
}