use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use crate::ActionType;
pub const TRANSCRIPT_SCHEMA_VERSION: &str = "0.1";
pub const GENESIS_HASH: &str =
"sha256:0000000000000000000000000000000000000000000000000000000000000000";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CeremonyInfo {
pub fingerprint: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceInfo {
pub fingerprint: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub parameters: BTreeMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub materials: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParticipantRecord {
pub role_id: String,
pub role_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub person: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageComponent {
pub hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageManifest {
pub version: String,
pub filesystem: ImageComponent,
pub binary: ImageComponent,
#[serde(skip_serializing_if = "Option::is_none")]
pub pcr: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitrdComponent {
pub hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verified: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitrdMeasurements {
pub filesystem: InitrdComponent,
pub ceremony: InitrdComponent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinaryInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
pub version: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TranscriptStatus {
Completed,
Interrupted,
InProgress,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventOutcome {
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StepEvidence {
#[serde(flatten)]
pub data: BTreeMap<String, serde_json::Value>,
}
impl StepEvidence {
pub fn new() -> Self {
Self {
data: BTreeMap::new(),
}
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) {
self.data.insert(key.into(), value.into());
}
#[must_use]
pub fn with(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
self.insert(key, value);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum EventData {
CeremonyStart {
schema_version: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
dry_run: bool,
ceremony: CeremonyInfo,
#[serde(skip_serializing_if = "Option::is_none")]
instance: Option<InstanceInfo>,
binary: BinaryInfo,
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<ImageManifest>,
#[serde(skip_serializing_if = "Option::is_none")]
initrd: Option<InitrdMeasurements>,
#[serde(skip_serializing_if = "Option::is_none")]
environment: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
participants: Vec<ParticipantRecord>,
started_at: DateTime<Utc>,
},
Step {
step_id: String,
action: ActionType,
#[serde(skip_serializing_if = "Option::is_none")]
role: Option<String>,
started_at: DateTime<Utc>,
completed_at: DateTime<Utc>,
outcome: EventOutcome,
evidence: StepEvidence,
},
EvidenceAdd {
path: String,
hash: String,
size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
mime: Option<String>,
},
ArtifactProduce {
source: String,
path: String,
hash: String,
size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
mime: Option<String>,
},
Deviation {
reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
step_id: Option<String>,
recorded_at: DateTime<Utc>,
},
CeremonyComplete {
completed_at: DateTime<Utc>,
status: TranscriptStatus,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainedEvent {
pub prev: String,
pub data: EventData,
pub hash: String,
}
#[derive(Debug, Clone)]
pub struct ArtifactVerification {
pub source: String,
pub path: String,
pub expected_hash: String,
pub verified: bool,
pub error: Option<String>,
}
#[non_exhaustive]
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum VerificationResult {
Valid {
fingerprint: String,
status: TranscriptStatus,
dry_run: bool,
artifacts: Vec<ArtifactVerification>,
image: Option<ImageManifest>,
initrd: Option<InitrdMeasurements>,
tpm_nonce: Option<String>,
tpm_pcrs: BTreeMap<String, String>,
},
Invalid {
expected: String,
computed: String,
},
Incomplete {
status: TranscriptStatus,
events_count: usize,
},
}
impl std::fmt::Display for VerificationResult {
#[allow(clippy::too_many_lines)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VerificationResult::Valid {
fingerprint,
status,
dry_run,
artifacts,
image,
initrd,
tpm_nonce,
tpm_pcrs,
} => {
if *dry_run {
writeln!(
f,
"WARNING: This is a DRY RUN transcript - no actual cryptographic operations were performed"
)?;
}
writeln!(f, "Transcript integrity verified.")?;
writeln!(f, " Status: {status:?}")?;
writeln!(f, " Fingerprint: {fingerprint}")?;
if let Some(img) = image {
writeln!(f, "\nImage (build-time):")?;
writeln!(f, " Version: {}", img.version)?;
writeln!(f, " Filesystem: {}", img.filesystem.hash)?;
writeln!(f, " Binary: {}", img.binary.hash)?;
}
if let Some(imd) = initrd {
writeln!(f, "\nInitrd measurements (boot-time, pre-mount):")?;
let fs_hash = imd.filesystem.hash.as_deref().unwrap_or("(none)");
let fs_verified = match imd.filesystem.verified {
Some(true) => " [verified]",
Some(false) => " [not verified]",
None => "",
};
writeln!(f, " Filesystem: {fs_hash}{fs_verified}")?;
let cer_hash = imd.ceremony.hash.as_deref().unwrap_or("(none)");
writeln!(f, " Ceremony: {cer_hash}")?;
}
if let (Some(img), Some(imd)) = (image, initrd) {
writeln!(f, "\nCross-checks:")?;
if let Some(ref fs_hash) = imd.filesystem.hash {
let matches = fs_hash == &img.filesystem.hash;
writeln!(
f,
" initrd.filesystem == image.filesystem: {}",
if matches { "match" } else { "MISMATCH" }
)?;
}
match imd.filesystem.verified {
Some(true) => writeln!(f, " initrd verification gate: passed")?,
Some(false) => {
writeln!(f, " initrd verification gate: FAILED")?;
}
None => {}
}
}
if !tpm_pcrs.is_empty() || tpm_nonce.is_some() {
writeln!(f, "\nTPM attestation:")?;
if let Some(nonce) = tpm_nonce {
writeln!(f, " Nonce: {nonce}")?;
}
for (pcr, val) in tpm_pcrs {
writeln!(f, " PCR {pcr}: {val}")?;
}
} else {
writeln!(
f,
"\nNo hardware attestation recorded (no tpm_attest step in transcript)."
)?;
}
if !artifacts.is_empty() {
let verified_count = artifacts.iter().filter(|a| a.verified).count();
let total = artifacts.len();
writeln!(
f,
"\nArtifact verification: {verified_count}/{total} files verified"
)?;
for artifact in artifacts {
if artifact.verified {
writeln!(f, " \u{2713} {} ({})", artifact.path, artifact.source)?;
} else if let Some(error) = &artifact.error {
writeln!(
f,
" \u{2717} {} ({}): {}",
artifact.path, artifact.source, error
)?;
} else {
writeln!(
f,
" \u{2717} {} ({}): Hash mismatch",
artifact.path, artifact.source
)?;
}
}
}
Ok(())
}
VerificationResult::Invalid { expected, computed } => write!(
f,
"TAMPERED: Fingerprint mismatch!\n Expected: {expected}\n Computed: {computed}"
),
VerificationResult::Incomplete {
status,
events_count,
} => write!(
f,
"Incomplete transcript (no final fingerprint). Status: {status:?}. Events recorded: {events_count}"
),
}
}
}
#[derive(Debug)]
pub struct ParsedTranscript {
pub events: Vec<ChainedEvent>,
pub fingerprint: String,
pub dry_run: bool,
pub status: TranscriptStatus,
}