1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! Delta Attestation Serialization & Hash Chaining
//!
//! Provides cryptographically strict canonicalization for runtime state deltas.
//! Ensures identical evidence always computes to the identical rolling hash,
//! preventing consensus fractures across the verifier federation.
use crate::digest::{DigestAlgorithm, TypedDigest};
use crate::runtime_attestation::{RuntimeMeasurement, RuntimeMeasurementDomain};
use alloc::vec::Vec;
use sha2::{Digest, Sha256};
use sha3::Sha3_256;
/// A strict, canonical representation of incremental changes in runtime state.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DeltaAttestation {
/// The sequence number of the state immediately preceding this delta.
pub previous_sequence: u64,
/// The sequence number of the new state resulting from this delta.
pub current_sequence: u64,
/// Measurements of processes/files added since the previous sequence.
pub added_measurements: Vec<RuntimeMeasurement>,
/// Measurements of processes/files removed since the previous sequence.
pub removed_measurements: Vec<RuntimeMeasurement>,
/// The cryptographically linked rolling hash `H(prev_hash || canonical_delta)`.
pub rolling_hash: TypedDigest,
}
#[derive(serde::Serialize)]
struct CanonicalPayload<'a> {
prev_seq: u64,
curr_seq: u64,
added: &'a [RuntimeMeasurement],
removed: &'a [RuntimeMeasurement],
prev_hash: &'a [u8],
}
impl DeltaAttestation {
/// Constructs a new `DeltaAttestation` by strictly canonicalizing inputs
/// and chaining against the previous rolling hash.
pub fn new_chained(
previous_sequence: u64,
current_sequence: u64,
mut added_measurements: Vec<RuntimeMeasurement>,
mut removed_measurements: Vec<RuntimeMeasurement>,
previous_hash: &TypedDigest,
algo: DigestAlgorithm,
) -> Result<Self, &'static str> {
if current_sequence <= previous_sequence {
return Err("current sequence must be greater than previous sequence");
}
// ── Canonical Sorting (Issue 3 Fix) ──────────────────────────────
//
// Two verifiers receiving the same events in different orders must
// produce identical delta roots. Sorting by domain discriminant first
// partitions events by their trust domain (IMA, TPM, process state,
// etc.), then sorts deterministically within each domain.
//
// Order: domain (by tag repr) → digest.value (lexicographic) → measurement_id
let domain_tag = |m: &RuntimeMeasurement| -> u8 {
match m.domain {
RuntimeMeasurementDomain::KernelModule => 0,
RuntimeMeasurementDomain::Executable => 1,
RuntimeMeasurementDomain::Library => 2,
RuntimeMeasurementDomain::Container => 3,
RuntimeMeasurementDomain::Process => 4,
RuntimeMeasurementDomain::Filesystem => 5,
}
};
added_measurements.sort_by(|a, b| {
domain_tag(a)
.cmp(&domain_tag(b))
.then_with(|| a.digest.value.cmp(&b.digest.value))
.then_with(|| a.measurement_id.cmp(&b.measurement_id))
});
removed_measurements.sort_by(|a, b| {
domain_tag(a)
.cmp(&domain_tag(b))
.then_with(|| a.digest.value.cmp(&b.digest.value))
.then_with(|| a.measurement_id.cmp(&b.measurement_id))
});
let payload = CanonicalPayload {
prev_seq: previous_sequence,
curr_seq: current_sequence,
added: &added_measurements,
removed: &removed_measurements,
prev_hash: &previous_hash.value,
};
let mut cbor_buf = Vec::new();
ciborium::into_writer(&payload, &mut cbor_buf)
.map_err(|_| "failed to canonicalize delta to CBOR")?;
let rolling_hash = match algo {
DigestAlgorithm::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(&cbor_buf);
TypedDigest {
algorithm: DigestAlgorithm::Sha256,
value: hasher.finalize().into(),
}
}
DigestAlgorithm::Sha3_256 => {
let mut hasher = Sha3_256::new();
hasher.update(&cbor_buf);
TypedDigest {
algorithm: DigestAlgorithm::Sha3_256,
value: hasher.finalize().into(),
}
}
};
Ok(Self {
previous_sequence,
current_sequence,
added_measurements,
removed_measurements,
rolling_hash,
})
}
}