use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::badge::BadgeStatus;
use crate::fingerprint::CertFingerprint;
use crate::types::AnsName;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum VerificationTier {
BadgeOnly,
StatusTokenVerified,
FullScitt,
}
impl VerificationTier {
pub fn is_scitt(&self) -> bool {
matches!(self, Self::StatusTokenVerified | Self::FullScitt)
}
pub fn has_receipt(&self) -> bool {
matches!(self, Self::FullScitt)
}
}
impl std::fmt::Display for VerificationTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BadgeOnly => write!(f, "BadgeOnly"),
Self::StatusTokenVerified => write!(f, "StatusTokenVerified"),
Self::FullScitt => write!(f, "FullScitt"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CertType {
#[serde(rename = "X509-DV-SERVER")]
X509DvServer,
#[serde(rename = "X509-OV-CLIENT")]
X509OvClient,
}
impl std::str::FromStr for CertType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"X509-DV-SERVER" => Ok(Self::X509DvServer),
"X509-OV-CLIENT" => Ok(Self::X509OvClient),
other => Err(format!("unknown cert_type: {other}")),
}
}
}
impl std::fmt::Display for CertType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::X509DvServer => write!(f, "X509-DV-SERVER"),
Self::X509OvClient => write!(f, "X509-OV-CLIENT"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CertEntry {
pub fingerprint: CertFingerprint,
pub cert_type: CertType,
}
impl CertEntry {
pub fn new(fingerprint: CertFingerprint, cert_type: CertType) -> Self {
Self {
fingerprint,
cert_type,
}
}
}
impl StatusTokenPayload {
#[allow(clippy::too_many_arguments)]
pub fn new(
agent_id: Uuid,
status: BadgeStatus,
iat: i64,
exp: i64,
ans_name: AnsName,
valid_identity_certs: Vec<CertEntry>,
valid_server_certs: Vec<CertEntry>,
metadata_hashes: BTreeMap<String, String>,
) -> Self {
Self {
agent_id,
status,
iat,
exp,
ans_name,
valid_identity_certs,
valid_server_certs,
metadata_hashes,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StatusTokenPayload {
pub agent_id: Uuid,
pub status: BadgeStatus,
pub iat: i64,
pub exp: i64,
pub ans_name: AnsName,
pub valid_identity_certs: Vec<CertEntry>,
pub valid_server_certs: Vec<CertEntry>,
pub metadata_hashes: BTreeMap<String, String>,
}
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verification_tier_is_scitt() {
assert!(!VerificationTier::BadgeOnly.is_scitt());
assert!(VerificationTier::StatusTokenVerified.is_scitt());
assert!(VerificationTier::FullScitt.is_scitt());
}
#[test]
fn verification_tier_has_receipt() {
assert!(!VerificationTier::BadgeOnly.has_receipt());
assert!(!VerificationTier::StatusTokenVerified.has_receipt());
assert!(VerificationTier::FullScitt.has_receipt());
}
#[test]
fn verification_tier_display() {
assert_eq!(VerificationTier::BadgeOnly.to_string(), "BadgeOnly");
assert_eq!(
VerificationTier::StatusTokenVerified.to_string(),
"StatusTokenVerified"
);
assert_eq!(VerificationTier::FullScitt.to_string(), "FullScitt");
}
#[test]
fn verification_tier_serde_roundtrip() {
for tier in [
VerificationTier::BadgeOnly,
VerificationTier::StatusTokenVerified,
VerificationTier::FullScitt,
] {
let json = serde_json::to_string(&tier).unwrap();
let deserialized: VerificationTier = serde_json::from_str(&json).unwrap();
assert_eq!(tier, deserialized);
}
}
#[test]
fn verification_tier_equality_and_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(VerificationTier::BadgeOnly);
set.insert(VerificationTier::StatusTokenVerified);
set.insert(VerificationTier::FullScitt);
set.insert(VerificationTier::BadgeOnly); assert_eq!(set.len(), 3);
}
#[test]
fn verification_tier_clone_and_copy() {
let tier = VerificationTier::FullScitt;
let cloned = tier;
assert_eq!(tier, cloned); }
#[test]
fn cert_entry_new() {
let fp = CertFingerprint::from_bytes([
0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2,
0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4,
0xe5, 0xf6, 0xa1, 0xb2,
]);
let entry = CertEntry::new(fp.clone(), CertType::X509DvServer);
assert_eq!(entry.fingerprint, fp);
assert_eq!(entry.cert_type, CertType::X509DvServer);
}
#[test]
fn status_token_payload_serde_roundtrip() {
let fp = CertFingerprint::from_bytes([
0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2,
0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa1, 0xb2, 0xc3, 0xd4,
0xe5, 0xf6, 0xa1, 0xb2,
]);
let payload = StatusTokenPayload {
agent_id: Uuid::nil(),
status: BadgeStatus::Active,
iat: 1_700_000_000,
exp: 1_700_003_600,
ans_name: AnsName::parse("ans://v1.0.0.agent.example.com").unwrap(),
valid_identity_certs: vec![CertEntry::new(fp.clone(), CertType::X509OvClient)],
valid_server_certs: vec![CertEntry::new(fp, CertType::X509DvServer)],
metadata_hashes: BTreeMap::from([("key".to_string(), "value".to_string())]),
};
let json = serde_json::to_string(&payload).unwrap();
let deserialized: StatusTokenPayload = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.agent_id, payload.agent_id);
assert_eq!(deserialized.status, payload.status);
assert_eq!(deserialized.iat, payload.iat);
assert_eq!(deserialized.exp, payload.exp);
assert_eq!(deserialized.ans_name, payload.ans_name);
assert_eq!(deserialized.valid_identity_certs.len(), 1);
assert_eq!(deserialized.valid_server_certs.len(), 1);
assert_eq!(deserialized.metadata_hashes.len(), 1);
}
#[test]
fn status_token_payload_empty_cert_arrays() {
let payload = StatusTokenPayload {
agent_id: Uuid::nil(),
status: BadgeStatus::Warning,
iat: 0,
exp: 3600,
ans_name: AnsName::parse("ans://v0.1.0.test.example.com").unwrap(),
valid_identity_certs: vec![],
valid_server_certs: vec![],
metadata_hashes: BTreeMap::new(),
};
let json = serde_json::to_string(&payload).unwrap();
let deserialized: StatusTokenPayload = serde_json::from_str(&json).unwrap();
assert!(deserialized.valid_identity_certs.is_empty());
assert!(deserialized.valid_server_certs.is_empty());
assert!(deserialized.metadata_hashes.is_empty());
}
#[test]
fn status_token_payload_all_statuses() {
for status in [
BadgeStatus::Active,
BadgeStatus::Warning,
BadgeStatus::Deprecated,
BadgeStatus::Expired,
BadgeStatus::Revoked,
] {
let payload = StatusTokenPayload {
agent_id: Uuid::nil(),
status,
iat: 0,
exp: 3600,
ans_name: AnsName::parse("ans://v1.0.0.test.example.com").unwrap(),
valid_identity_certs: vec![],
valid_server_certs: vec![],
metadata_hashes: BTreeMap::new(),
};
let json = serde_json::to_string(&payload).unwrap();
let deserialized: StatusTokenPayload = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.status, status);
}
}
#[test]
fn metadata_hashes_deterministic_ordering() {
let mut map = BTreeMap::new();
map.insert("zebra".to_string(), "hash_z".to_string());
map.insert("alpha".to_string(), "hash_a".to_string());
map.insert("middle".to_string(), "hash_m".to_string());
let keys: Vec<&String> = map.keys().collect();
assert_eq!(keys, vec!["alpha", "middle", "zebra"]);
}
}