HELIX OS — Data Integrity & Audit Ledger Specification
Formal Technical Proof of System Immutability for FDA/EMA Regulatory Bodies
Document ID: HELIX-COMP-0001
Classification: Normative — Regulatory Submission Grade
Revision: 2.1.0
Status: Approved for Submission
Effective Date: 2026-01-15
Next Review: 2027-01-15
Authors: Compliance Engineering & Cryptography Group, HELIX OS
Applies To: helix-ledger ≥ 2.4, helix-core ≥ 2.4
Regulatory Frameworks Addressed:
| Framework | Applicability |
|---|---|
| 21 CFR Part 11 | Electronic records and electronic signatures — FDA (United States) |
| EU Annex 11 | Computerised systems — EMA (European Union) |
| ICH Q10 | Pharmaceutical quality system — data integrity requirements |
| GAMP 5 (2nd Ed.) | Risk-based approach to compliant GxP computerized systems |
| ISO 27001:2022 | Information security management — cryptographic controls (§8.24) |
| NIST SP 800-57 | Recommendation for key management — Ed25519 key lifecycle |
Preface for Regulatory Reviewers.
This document constitutes the cryptographic and engineering proof that HELIX OS cannot produce, modify, or delete an electronic record without leaving a mathematically verifiable trace. The claims made herein are not policy assertions. They are mathematical consequences of the algorithms and data structures described. Each section identifies the specific regulatory citation it addresses and the specific cryptographic property that satisfies it.
Where implementation source code is referenced, it is available in the HELIX OS Software Bill of Materials (SBOM) and the validated source code archive submitted under this IND/BLA package. All cryptographic primitives used are NIST-approved or are pending approval under NIST PQC standardization (noted where applicable).
Table of Contents
- Regulatory Mapping
- System Architecture Overview
- The Blake3 Hash Chain
- Persistence Layer — WAL Strategy
- Ed25519 Digital Signature Protocol
- Batch Record Schema
- Merkle Mountain Range — O(log n) Proof of Inclusion
- 21 CFR Part 11 Compliance Matrix
- Threat Model & Attack Surface Analysis
- Cryptographic Primitive Inventory
- Appendix A — Reference Implementation
- Appendix B — Test Vectors
1. Regulatory Mapping
Each section of this specification maps to one or more specific regulatory citations. This table is the primary navigation aid for regulatory reviewers who need to locate the technical evidence for a specific compliance assertion.
| Regulatory Requirement | Citation | Addressed In |
|---|---|---|
| Audit trail must be computer-generated | 21 CFR §11.10(e) | §3, §4 |
| Records must be protected from modification and deletion | 21 CFR §11.10(c) | §3.2, §4.1 |
| Electronic signatures must be linked to their records | 21 CFR §11.70 | §5.2 |
| System must detect invalid or altered records | 21 CFR §11.10(d) | §3.5, §7.4 |
| Audit trail entries must include date, time, and operator identity | 21 CFR §11.10(e) | §6.1, §6.3 |
| Sequential record entries must be unambiguously ordered | EU Annex 11 §9 | §3.2 |
| Data integrity controls must prevent deletion | EU Annex 11 §12.4 | §4.1 |
| System must support accurate and complete copies for inspection | 21 CFR §11.10(b) | §7.3, §7.4 |
| Persons who sign must be identified | 21 CFR §11.50 | §5.1, §6.1 |
| Signature manifestation must be included in human-readable form | 21 CFR §11.50(a) | §6.2 |
| Original record must accompany the signature | 21 CFR §11.70 | §5.2 |
| Data governance controls (ALCOA+) | ICH Q10 §1.5 | §3, §5, §6 |
ALCOA+ Attestation. The HELIX OS ledger is designed to satisfy all six ALCOA+ data integrity attributes as defined by FDA's 2018 Data Integrity and Compliance With Drug CGMP guidance: Attributable (§5), Legible (§6.2), Contemporaneous (§3.3 timestamp policy), Original (§4.1 WAL), Accurate (§3.5 chain verification), Complete (§7 MMR), Consistent (§4.3 power-failure proof), Enduring (§4.4 retention), Available (§7.3 proof export).
2. System Architecture Overview
The integrity layer is composed of four independent subsystems that together provide defense-in-depth data integrity. A failure of any single subsystem is detectable by the others.
┌─────────────────────────────────────────────────────────────────────────────┐
│ HELIX OS Integrity Subsystem │
│ │
│ Event Source │
│ (Instrument / UI / API) │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Entry Serializer │ │
│ │ · Canonical byte encoding (no field reordering, no optional fields) │ │
│ │ · TAI-64N timestamp (nanosecond precision, leap-second aware) │ │
│ │ · Actor identity binding (LDAP DN + session token hash) │ │
│ └─────────────────────────────┬─────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼────────────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Blake3 │ │ Ed25519 │ │ MMR Accumulator│ │
│ │ Hash │ │ Signer │ │ (leaf append) │ │
│ │ Chain │ │ │ │ │ │
│ └─────┬─────┘ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │ │
│ └──────────────────────┼────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ WAL Entry (binary) │ │
│ │ entry_hash │ ← Blake3 chain link │
│ │ signature │ ← Ed25519 over entry_hash │
│ │ mmr_leaf_index │ ← position in MMR │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ O_DIRECT + O_SYNC │ │
│ │ WAL Writer │ │
│ │ (NVMe, direct I/O) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
The four subsystems are designed so that no single subsystem failure produces a false negative:
- The hash chain detects any modification, insertion, or deletion of entries, but cannot identify the actor who performed it.
- The digital signatures identify the actor (the HELIX OS node key) and bind the entry content to that identity, but do not prove ordering.
- The MMR provides O(log n) proofs of inclusion for any individual entry, enabling efficient auditor verification without replaying the entire chain.
- The WAL with O_DIRECT/O_SYNC ensures that no entry that has been acknowledged to a caller is lost due to OS buffer cache volatility or power failure.
3. The Blake3 Hash Chain
3.1 Algorithm Selection Rationale
Regulatory citation: 21 CFR §11.10(c) — Records must be protected from modification.
Why Blake3 and not SHA-256 or SHA-3.
SHA-256 remains cryptographically sound for collision resistance. The selection of Blake3 over SHA-256 is not a security argument — it is a performance argument with no security regression. At HELIX OS's target event rates (>10,000 ledger entries per second during peak bioreactor operations), SHA-256 becomes a measurable bottleneck in the ledger write path on CPU cores that lack SHA-NI extensions. Blake3's SIMD-parallel construction delivers 14–25 GB/s on a modern AVX-512 core versus SHA-256's 1.5–4 GB/s. The hash chain write overhead target is <50µs per entry; SHA-256 satisfies this at current rates, but Blake3 provides a >5× headroom margin for future event rate growth.
Formal security properties of Blake3 relevant to this application:
| Property | Blake3 Guarantee | Regulatory Relevance |
|---|---|---|
| Collision resistance | 128-bit security (2^128 work to find collision) | Two distinct entries cannot have the same hash |
| Second-preimage resistance | 128-bit security | A modified entry cannot be made to match the original hash |
| Preimage resistance | 128-bit security | The entry content cannot be reconstructed from its hash alone |
| Length extension immunity | Immune (BLAKE construction) | Hash(entry) cannot be extended to Hash(entry ∥ attacker_data) without detection |
| Keyed mode | 256-bit key support | Used for HMAC-equivalent authentication on site-specific chain roots |
Blake3 is a NIST-recognized hash function. It is derived from BLAKE2, which was a finalist in the SHA-3 competition. NIST has not issued guidance against its use for non-digital-signature applications. The HELIX OS SBOM documents this selection and its rationale for regulatory review.
3.2 Chain Construction
The hash chain is defined by the following recurrence:
Entry₀: H₀ = Blake3( serialize(Entry₀) ∥ Genesis_Context )
Entryₙ: Hₙ = Blake3( serialize(Entryₙ) ∥ Hₙ₋₁ )
Where:
serialize(Entryₙ)is the canonical byte encoding defined in §3.3.∥denotes byte concatenation.Genesis_Contextis defined in §3.4.Blake3(·)produces a 32-byte (256-bit) digest.
The critical property: Given Hₙ and Hₙ₋₁, the verifier can confirm that Entryₙ has not been modified since Hₙ was computed, and that Entryₙ is the entry that was appended immediately after Entryₙ₋₁. Modifying any entry at position k invalidates all hashes from Hₖ through Hₙ. Inserting an entry between positions k and k+1 invalidates all hashes from Hₖ₊₁ onward. Deleting entry k and re-linking is detectable because the adversary cannot produce Hₖ' such that Hₖ' = Blake3(serialize(Entryₖ₊₁) ∥ Hₖ₋₁) without solving the second-preimage problem.
// helix-ledger/src/chain.rs
use blake3::Hasher;
pub const CHAIN_HASH_BYTES: usize = 32;
/// Computes the chain hash for entry N, given the entry's canonical bytes
/// and the hash of entry N-1.
///
/// This is the core of the Blake3 hash chain. It must be called in strict
/// sequential order. Calling it out of order, or with the wrong prev_hash,
/// produces a hash that will fail chain verification.
///
/// # Parameters
/// - `entry_bytes`: The canonical serialization of entry N (see §3.3).
/// - `prev_hash`: H_{n-1} — the entry hash of the immediately preceding entry.
/// For the genesis entry, this is the genesis context hash (§3.4).
///
/// # Returns
/// H_n — the 32-byte Blake3 digest binding this entry to the chain.
pub fn compute_entry_hash(
entry_bytes: &[u8],
prev_hash: &[u8; CHAIN_HASH_BYTES],
) -> [u8; CHAIN_HASH_BYTES] {
let mut hasher = Hasher::new();
// Domain separation prefix — prevents hash collisions between different
// HELIX OS hash chain contexts (entry chain vs. MMR vs. key commitment).
hasher.update(b"HELIX:LEDGER:ENTRY:v2\x00");
// The entry content comes first, then the previous hash.
// Order matters: reversing this would still be a valid chain, but it must
// be consistent across all implementations. This order is normative.
hasher.update(entry_bytes);
hasher.update(prev_hash);
*hasher.finalize().as_bytes()
}
/// Verify that a sequence of entries forms a valid chain.
///
/// # Returns
/// - `Ok(final_hash)` if all entries verify correctly.
/// - `Err(ChainError::BrokenAt(n))` if the chain breaks at position n.
pub fn verify_chain(
entries: &[(Vec<u8>, [u8; 32])], // (canonical_bytes, claimed_hash)
genesis_context: &[u8; 32],
) -> Result<[u8; 32], ChainError> {
let mut prev_hash = *genesis_context;
for (n, (entry_bytes, claimed_hash)) in entries.iter().enumerate() {
let computed = compute_entry_hash(entry_bytes, &prev_hash);
if &computed != claimed_hash {
return Err(ChainError::BrokenAt {
position: n as u64,
computed,
claimed: *claimed_hash,
});
}
prev_hash = computed;
}
Ok(prev_hash)
}
3.3 Canonical Entry Serialization
The serialization format must be canonical. A canonical format is one where every valid entry has exactly one valid byte representation. Non-canonical formats (e.g., JSON with variable field ordering or whitespace) allow adversaries to produce two byte sequences that parse to the same logical entry but hash differently, undermining the chain's integrity proof.
HELIX OS uses Protocol Buffers with deterministic serialization mode as the canonical format. Protobuf's deterministic flag (available in the Rust prost crate via EncodeOptions::deterministic(true)) ensures:
- Map fields are sorted by key.
- Unknown fields are preserved in canonical order.
- The same
LedgerEntrymessage always produces the same byte sequence on any platform.
The fields included in the canonical serialization are a strict subset of the full LedgerEntry message. The fields entry_hash, prev_hash, and signature are excluded from canonical serialization (they are derived fields, not content fields). All other fields are included.
// Canonical serialization covers these fields only:
// (entry_hash, prev_hash, signature are derived — excluded from hash input)
message LedgerEntryContent {
uint64 sequence_number = 1; // Monotonic, never reused
fixed64 timestamp_tai64n_hi = 2; // TAI-64N high 64 bits (seconds since TAI epoch)
uint32 timestamp_tai64n_lo = 3; // TAI-64N low 32 bits (nanoseconds fraction)
string actor_dn = 4; // LDAP DN of the authenticated actor
bytes session_token_hash = 5; // Blake3(session_token) — 32 bytes
string site_id = 6;
EventType event_type = 7;
bytes event_payload = 8; // Event-type-specific protobuf (see §6)
string software_version = 9; // helix-ledger SemVer string
bytes node_public_key = 10; // Ed25519 public key — 32 bytes
}
Timestamp policy. HELIX OS uses TAI-64N (International Atomic Time, 64-bit with nanosecond extension) for all ledger timestamps. TAI does not have leap seconds and provides a monotonically increasing, unambiguous time representation suitable for strict causal ordering. The conversion from the system's CLOCK_TAI (Linux) or mach_absolute_time + gettimeofday (macOS) to TAI-64N is performed by the helix-time crate and documented in HELIX-ARCH-0003. Wall-clock UTC timestamps are included as a human-readable annotation field but are not part of the canonical hash input — only TAI-64N is.
3.4 Genesis Block
The genesis block establishes the chain's identity. It cannot be modified without invalidating the entire chain. It is computed at site initialization and stored in the HELIX OS keystore.
// helix-ledger/src/genesis.rs
/// Compute the genesis context hash for a new ledger chain.
///
/// The genesis context is the "prev_hash" for the first entry (entry 0).
/// It encodes the site identity, initialization timestamp, and software version,
/// ensuring that ledger chains from different sites or different software versions
/// are cryptographically distinct and cannot be merged or confused.
///
/// This value is computed ONCE at site initialization and MUST be stored
/// in the HELIX OS keystore (hardware-backed where available).
/// It MUST be included in the site's IQ documentation package.
pub fn compute_genesis_context(
site_id: &str,
init_timestamp_tai64n: u128,
software_version: &str,
node_public_key: &[u8; 32],
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"HELIX:LEDGER:GENESIS:v2\x00");
hasher.update(site_id.as_bytes());
hasher.update(b"\x00"); // Field separator — prevents prefix collision
hasher.update(&init_timestamp_tai64n.to_be_bytes());
hasher.update(b"\x00");
hasher.update(software_version.as_bytes());
hasher.update(b"\x00");
hasher.update(node_public_key);
*hasher.finalize().as_bytes()
}
Genesis block storage requirements:
- The genesis context hash MUST be stored in at minimum two physically separate locations: the primary NVMe partition and the site's Hardware Security Module (HSM) or Trusted Platform Module (TPM).
- It MUST be printed in human-readable hex and included in the site's IQ execution report.
- An FDA inspector can independently verify that a ledger chain belongs to a specific site by recomputing the genesis context from the site's IQ documentation and comparing it to the genesis entry's
prev_hash.
3.5 Chain Verification Protocol
The chain verification tool is helix ledger verify. It is designed to be run by an external auditor with read-only access to the ledger files and the published genesis context. It requires no special HELIX OS credentials or software beyond the open-source helix-verify binary (statically linked, no dependencies).
# Full chain verification from genesis
helix ledger verify \
--ledger-dir /var/helix/ledger \
--genesis-hash 0x3f9a1b2c... \ # From IQ execution report
--report /tmp/chain-verify-$(date +%Y%m%d).pdf \
--report-format fda-inspection
# Output format:
# ┌─────────────────────────────────────────────────────────────────────┐
# │ HELIX OS Chain Verification Report │
# │ Generated: 2026-01-15T09:42:17Z │
# │ Ledger directory: /var/helix/ledger │
# │ Genesis hash: 3f9a1b2c... │
# │ Chain length: 4,827,193 entries │
# │ First entry: 2026-01-01T00:00:00Z (TAI) │
# │ Last entry: 2026-01-15T09:42:16Z (TAI) │
# │ ───────────────────────────────────────────────────────────────── │
# │ Chain integrity: VERIFIED ✓ │
# │ Signature validity: ALL VALID ✓ (4,827,193/4,827,193) │
# │ MMR root: b7c3d9e2... │
# │ Dropped entry count: 0 │
# │ ───────────────────────────────────────────────────────────────── │
# │ This report was produced by helix-verify v2.4.1 (SHA-256: ...) │
# └─────────────────────────────────────────────────────────────────────┘
4. Persistence Layer — WAL Strategy
4.1 O_DIRECT and O_SYNC Requirements
Regulatory citation: EU Annex 11 §12.4 — Data must be protected against loss.
The problem with the OS page cache. When a process writes to a file using standard write(2), the data is written to the operating system's page cache — volatile memory. The OS flushes it to disk at its convenience, typically within 30 seconds. If the system loses power in that 30-second window, the page cache is lost and the write never reaches the storage device. The application believed the write succeeded (the write(2) call returned successfully), but the data is gone. For a pharmaceutical audit trail, this is an unacceptable failure mode.
The O_DIRECT + O_SYNC solution. HELIX OS opens the WAL file with O_DIRECT | O_SYNC on Linux and F_NOCACHE | O_SYNC on macOS:
O_DIRECT: Bypasses the page cache entirely. Data goes directly from the application's aligned buffer to the storage device's DMA engine. Thewrite(2)call does not return until the data has been transferred to the device's write buffer.O_SYNC: Additionally requires that the storage device acknowledge that the data has been written to non-volatile storage (e.g., the NVMe controller has flushed its volatile DRAM write cache to the NAND flash) beforewrite(2)returns.
When write(2) returns Ok(n) with O_DIRECT | O_SYNC, the written bytes are on non-volatile storage. A power failure after this point cannot cause data loss.
// helix-ledger/src/wal/writer.rs
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
const O_DIRECT: i32 = 0o40000; // Linux-specific
const O_SYNC: i32 = 0o4010000; // Linux-specific (O_SYNC = O_DSYNC | __O_SYNC)
/// Open the WAL segment file for writing with durability guarantees.
///
/// The returned file descriptor has O_DIRECT | O_SYNC set.
/// Every write to this file that returns Ok(n) is guaranteed to be
/// on non-volatile storage.
///
/// # Requirements for O_DIRECT compliance
/// All writes must satisfy:
/// 1. The write buffer must be aligned to 512 bytes (sector size) or 4096 bytes
/// (logical block size, preferred for modern NVMe devices).
/// 2. The write offset must be a multiple of the logical block size.
/// 3. The write length must be a multiple of the logical block size.
///
/// HELIX OS satisfies these requirements by:
/// 1. Allocating write buffers with `posix_memalign(4096)` (see `AlignedBuffer`).
/// 2. Maintaining the WAL write position at a 4096-byte boundary at all times.
/// 3. Padding WAL entries to a multiple of 4096 bytes (see §4.2).
pub fn open_wal_segment(path: &std::path::Path) -> std::io::Result<std::fs::File> {
OpenOptions::new()
.write(true)
.create(true)
.append(false) // We manage the write position manually for O_DIRECT alignment
.custom_flags(O_DIRECT | O_SYNC)
.open(path)
}
/// A heap-allocated buffer aligned to 4096 bytes for O_DIRECT writes.
///
/// Standard Vec<u8> does not guarantee alignment. This type uses
/// `std::alloc::alloc` with `Layout::from_size_align(size, 4096)` to
/// ensure the buffer base address is a multiple of 4096.
pub struct AlignedBuffer {
ptr: std::ptr::NonNull<u8>,
len: usize,
capacity: usize,
}
impl AlignedBuffer {
pub fn with_capacity(capacity: usize) -> Self {
// Round up to 4096-byte boundary
let aligned_cap = (capacity + 4095) & !4095;
let layout = std::alloc::Layout::from_size_align(aligned_cap, 4096).unwrap();
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
let ptr = std::ptr::NonNull::new(ptr).expect("allocation failed");
AlignedBuffer { ptr, len: 0, capacity: aligned_cap }
}
/// Fill remaining bytes with the WAL padding byte (0xAB — chosen to be
/// visually distinct in hex dumps, facilitating manual inspection).
pub fn pad_to_block_boundary(&mut self) {
let target = (self.len + 4095) & !4095;
while self.len < target {
unsafe { self.ptr.as_ptr().add(self.len).write(0xAB) };
self.len += 1;
}
}
}
4.2 Write-Ahead Log Layout
Each WAL segment file is a sequence of fixed-layout frames. The segment file format is:
Byte Offset Field Size Description
──────────── ───────────────── ────── ─────────────────────────────────────────────
0 Segment Header 4096 B Magic, version, segment ID, genesis hash
4096 Frame 0 Header 128 B Length, CRC-32C, entry hash, frame type
4224 Frame 0 Payload var. Protobuf-encoded LedgerEntry (padded to 4096-B block)
4096 + F0 Frame 1 Header 128 B
... ...
Frame header layout:
// helix-ledger/src/wal/frame.rs
/// WAL frame header. Lives at the start of every 4096-byte-aligned frame.
/// Total size: 128 bytes. Padded to ensure the payload starts at a
/// 128-byte-aligned offset within the 4096-byte block.
#[repr(C, align(128))]
pub struct WalFrameHeader {
/// Magic: 0x48454C49585741 ("HELIXWA" in ASCII) + 0x4C ("L")
pub magic: u64, // offset 0
/// Frame type:
/// 0x01 = LEDGER_ENTRY
/// 0x02 = SEGMENT_BOUNDARY (end of one segment, start of next)
/// 0x03 = KEY_ROTATION (see §5.3)
/// 0xFF = PADDING (ignored during replay)
pub frame_type: u8, // offset 8
/// Compression applied to the payload. 0 = none (always 0 for GMP-mode WAL;
/// compression is applied during compaction, not during WAL write).
pub compression: u8, // offset 9
pub _reserved: [u8; 6], // offset 10
/// Length of the payload in bytes (before padding to block boundary).
pub payload_len: u32, // offset 16
/// Length of the padded payload (multiple of 4096).
pub padded_payload_len: u32, // offset 20
/// CRC-32C over: frame header bytes 0..23 + payload bytes (unpadded).
/// Computed before padding is applied. Validated during WAL replay.
pub frame_crc32c: u32, // offset 24
/// The entry hash (H_n) from the Blake3 chain. Stored here for fast
/// seeking during chain verification without deserializing the payload.
pub entry_hash: [u8; 32], // offset 28
/// The sequence number of this entry. Monotonic, no gaps.
pub sequence_number: u64, // offset 60
/// TAI-64N timestamp (high word, seconds).
pub timestamp_tai64n_hi: u64, // offset 68
/// TAI-64N timestamp (low word, nanoseconds fraction).
pub timestamp_tai64n_lo: u32, // offset 76
pub _pad: [u8; 48], // offset 80, size 48
// total: 128 bytes
}
pub const WAL_FRAME_MAGIC: u64 = 0x48454C4958574143; // "HELIXWAC"
4.3 Power-Failure Durability Proof
Claim: No ledger entry that has been acknowledged to a HELIX OS caller (i.e., the API has returned success) can be lost due to power failure.
Proof by WAL write sequence:
Caller invokes ledger.write(entry)
│
▼
1. Serialize entry to AlignedBuffer (canonical bytes + padding)
│
▼
2. Compute Blake3 chain hash: H_n = Blake3(entry_bytes ∥ H_{n-1})
│
▼
3. Compute Ed25519 signature: sig = Sign(node_privkey, H_n)
│
▼
4. Construct WalFrameHeader (includes H_n, sig, CRC-32C)
│
▼
5. pwrite(wal_fd, header_buffer, 128, current_offset) ← O_DIRECT | O_SYNC
│ (returns only after NVMe acknowledges non-volatile write)
│
▼
6. pwrite(wal_fd, payload_buffer, padded_len, current_offset + 128) ← O_DIRECT | O_SYNC
│ (returns only after NVMe acknowledges non-volatile write)
│
▼
7. Advance current_offset and update in-memory H_n state
│
▼
8. Return Ok(sequence_number) to caller
Failure scenario analysis:
| Failure Point | Data State on Recovery | Recovery Action |
|---|---|---|
| Power failure before step 5 | Frame header not written. WAL truncated at previous frame. | No action needed. Caller never received acknowledgment. The un-written entry is re-submitted. |
| Power failure between steps 5 and 6 | Frame header written, payload not written. CRC validation fails on header-only frame during replay. | WAL replay detects frame_crc32c mismatch. Truncates WAL to last valid frame. Caller re-submits. |
| Power failure after step 6, before step 8 | Both header and payload are on non-volatile storage. | WAL replay succeeds. The entry is recovered. If the caller re-submits (because it didn't receive acknowledgment), the duplicate is detected via sequence_number deduplication and silently discarded. |
| Power failure after step 8 | Entry is fully durable. No recovery needed. | — |
The critical guarantee is at step 6. The O_SYNC flag ensures that pwrite does not return until the NVMe controller confirms the data is in non-volatile NAND, not merely in the controller's DRAM write cache. This guarantee holds for NVMe devices with properly implemented Force Unit Access (FUA) semantics, which all enterprise-grade NVMe drives support.
⚠️ Hardware Qualification Note. Consumer-grade SSDs (particularly SATA SSDs and early NVMe drives) may falsely report write completion while data remains in a volatile DRAM cache. HELIX OS deployment in a GMP environment requires the NVMe device to be on the validated hardware configuration list (HELIX-HW-COMPAT-LIST, included in the VSR). All listed devices have been tested for
O_SYNCdurability under simulated power failure using theltp-stresstest suite with a modified power-cut harness.
4.4 Log Rotation and Compaction
WAL segments are rotated when they reach 256 MB (configurable via helix.toml). Rotation is atomic: the new segment header is written and fsync'd before the old segment is closed.
During compaction (an offline process, never run on the primary WAL path), WAL segments are merged into immutable ledger archives using Zstandard compression at level 19. Compacted archives are:
- Verified for chain integrity before the source WAL segment is archived.
- Written with the full
O_SYNCguarantee to the archive partition. - Recorded with their SHA-256 digest in the segment index (a separate append-only index file).
- The source WAL segment is retained for a configurable retention period (default: 2 years) before deletion, which itself requires a two-party authorization entry in the ledger.
No WAL segment deletion is possible without a corresponding signed, hash-chained ledger entry authorizing it. An attempt to delete a WAL file without this authorization entry is detectable by the chain verifier as a gap in the sequence_number space.
5. Ed25519 Digital Signature Protocol
5.1 Key Generation and Management
Regulatory citation: 21 CFR §11.50 — Signed electronic records must identify the signer.
Key hierarchy. HELIX OS uses a two-level key hierarchy:
┌──────────────────────────────────────────────────────────────────┐
│ Level 0: Site Root Key (SRK) │
│ · Ed25519 key pair │
│ · Stored EXCLUSIVELY in site HSM (FIPS 140-2 Level 3 minimum) │
│ · Never exported to disk in any form │
│ · Used ONLY to certify Level 1 node keys │
│ · Key ceremony documented in HELIX-KM-0001 │
└──────────────────────────┬───────────────────────────────────────┘
│ Signs
▼
┌──────────────────────────────────────────────────────────────────┐
│ Level 1: Node Signing Key (NSK) │
│ · Ed25519 key pair per HELIX OS node │
│ · Private key stored in TPM 2.0 (preferred) or encrypted file │
│ (AES-256-GCM, key derived from HSM-backed secret) │
│ · Public key certified by SRK (certificate stored in keystore) │
│ · Used for: every individual ledger entry signature │
│ · Rotation period: 12 months maximum (configurable down to 1) │
└──────────────────────────────────────────────────────────────────┘
Key generation procedure:
# Performed during site initialization, in the presence of QA personnel.
# All output is recorded in the HELIX Key Ceremony Record (HELIX-KM-0001).
# Step 1: Generate the Node Signing Key in the TPM
helix keystore generate-nsk \
--tpm-device /dev/tpm0 \
--key-id "NSK-RTP-SITE-01-$(date +%Y%m%d)" \
--certify-with-srk \
--output-cert /etc/helix/keystore/nsk-cert.pem
# Step 2: Verify the NSK certificate chain back to the SRK
helix keystore verify-nsk \
--cert /etc/helix/keystore/nsk-cert.pem \
--srk-pubkey /etc/helix/keystore/srk-public.pem
# Step 3: Record the NSK public key fingerprint in the site's IQ documentation
helix keystore fingerprint \
--cert /etc/helix/keystore/nsk-cert.pem
# Output: SHA-256:xx:yy:zz:... (must be recorded manually in IQ report by two authorized personnel)
5.2 Per-Entry Signature Protocol
Every ledger entry is signed with the Node Signing Key. The signature is over the entry's chain hash (H_n), not the raw entry bytes. This choice is deliberate:
- Signing
H_nrather thanentry_bytesmeans the signature implicitly covers all prior entries in the chain (becauseH_nis a function ofH_{n-1}, which is a function of all preceding entries). An adversary who replaces entrykmust also forge the signature at every subsequent entry, which requires the NSK private key. - Signing
H_nis computationally efficient — Ed25519 signing of 32 bytes is significantly faster than signing a variable-length entry payload.
The signed message is constructed as:
SignedMessage = "HELIX:SIG:LEDGER:v2\x00" ∥ H_n ∥ sequence_number_bytes ∥ timestamp_tai64n_bytes
The domain separation prefix "HELIX:SIG:LEDGER:v2\x00" prevents cross-protocol signature confusion (a signature intended for a HELIX ledger entry cannot be replayed as a HELIX key rotation signature or any other signed context in the system).
// helix-ledger/src/signing.rs
use ed25519_dalek::{Signer, SigningKey, Signature};
/// Compute the signed message for a ledger entry.
///
/// The message is constructed to include domain separation, the chain hash,
/// and temporal context (sequence number and timestamp). This ensures that
/// the signature is unique per entry even if two entries happen to have
/// identical content and identical chain hashes (which is not possible with
/// a correct implementation, but is guarded against for defense-in-depth).
pub fn construct_signed_message(
entry_hash: &[u8; 32],
sequence_number: u64,
timestamp_tai64n_hi: u64,
timestamp_tai64n_lo: u32,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(64);
msg.extend_from_slice(b"HELIX:SIG:LEDGER:v2\x00");
msg.extend_from_slice(entry_hash);
msg.extend_from_slice(&sequence_number.to_be_bytes());
msg.extend_from_slice(×tamp_tai64n_hi.to_be_bytes());
msg.extend_from_slice(×tamp_tai64n_lo.to_be_bytes());
msg
}
/// Sign a ledger entry hash with the Node Signing Key.
///
/// In production, the `signing_key` is loaded from the TPM via the
/// `helix-tpm` crate, which performs the signing operation inside the TPM
/// and returns only the 64-byte signature. The private key never leaves
/// the TPM boundary.
///
/// In test environments, the signing key is a software key generated
/// by `helix keystore generate-nsk --software-fallback` and stored
/// encrypted at `/etc/helix/keystore/nsk-software.key`.
pub fn sign_entry(
signing_key: &SigningKey,
entry_hash: &[u8; 32],
sequence_number: u64,
timestamp_tai64n_hi: u64,
timestamp_tai64n_lo: u32,
) -> Signature {
let msg = construct_signed_message(
entry_hash,
sequence_number,
timestamp_tai64n_hi,
timestamp_tai64n_lo,
);
signing_key.sign(&msg)
}
5.3 Key Rotation Without Chain Discontinuity
Key rotation is a mandatory operational event. The HELIX OS keystore enforces a maximum key lifetime of 12 months (configurable to a shorter period). When the NSK approaches expiration, the system:
- Generates a new NSK (NSK₂) in the TPM.
- Has the SRK (in the HSM) sign a Key Rotation Certificate binding NSK₁ (outgoing) to NSK₂ (incoming), with an effective timestamp.
- Writes a
KEY_ROTATIONframe to the WAL (frame type0x03). This frame is part of the hash chain — its hash is computed from its canonical bytes and the previous entry's hash. It is signed by both NSK₁ (the outgoing key, proving it authorized the rotation) and NSK₂ (the incoming key, proving the new key is the legitimate successor). - All subsequent entries are signed by NSK₂.
A chain verifier encountering a KEY_ROTATION frame:
- Verifies the frame's NSK₁ signature (must be valid at the rotation timestamp).
- Verifies the frame's NSK₂ signature (must be valid at the rotation timestamp).
- Verifies the Key Rotation Certificate from the SRK.
- Continues chain verification using NSK₂ for all subsequent entries.
This protocol ensures there is no window where the chain is unsigned or where a key rotation can be introduced by an adversary. An adversary who compromises NSK₁ after rotation has occurred cannot insert retroactive entries after the KEY_ROTATION frame, because those entries would need to be signed by NSK₂, which the adversary does not have.
5.4 Signature Verification
// helix-verify/src/verify.rs (the open-source auditor tool — no HELIX license required)
use ed25519_dalek::{Verifier, VerifyingKey, Signature};
/// Verify the Ed25519 signature on a single ledger entry.
///
/// This function is part of `helix-verify`, the standalone auditor tool.
/// It has no dependencies on helix-core or any proprietary code.
/// It requires only: the entry hash, signature, sequence number, timestamp,
/// and the node public key (available in the entry's own fields and in
/// the Key Rotation Certificate chain).
pub fn verify_entry_signature(
verifying_key: &VerifyingKey,
entry_hash: &[u8; 32],
signature_bytes: &[u8; 64],
sequence_number: u64,
timestamp_tai64n_hi: u64,
timestamp_tai64n_lo: u32,
) -> Result<(), SignatureError> {
let signature = Signature::from_bytes(signature_bytes);
let msg = crate::signing::construct_signed_message(
entry_hash,
sequence_number,
timestamp_tai64n_hi,
timestamp_tai64n_lo,
);
verifying_key.verify(&msg, &signature)
.map_err(|e| SignatureError::InvalidSignature {
sequence: sequence_number,
reason: e.to_string(),
})
}
6. Batch Record Schema
6.1 Protobuf Schema Definition
The BatchRecordEntry is the primary record type for GMP batch operations. It is stored as the event_payload field of a LedgerEntry when event_type = BATCH_OPERATION.
// proto/helix/ledger/v2/batch_record.proto
// buf.build/helix-os/helix/helix/ledger/v2/batch_record.proto
syntax = "proto3";
package helix.ledger.v2;
option java_package = "io.helixos.ledger.v2";
option go_package = "github.com/helix-os/helix/gen/go/ledger/v2";
import "google/protobuf/timestamp.proto";
import "helix/ledger/v2/common.proto";
// ─────────────────────────────────────────────────────────────────────────────
// Top-level LedgerEntry — every entry in the chain
// ─────────────────────────────────────────────────────────────────────────────
message LedgerEntry {
// ── Identity & Ordering ────────────────────────────────────────────────────
// Monotonically increasing. No gaps. No reuse. Scoped to this site's chain.
uint64 sequence_number = 1;
// TAI-64N timestamp (seconds component, TAI epoch = 1970-01-01T00:00:00 TAI)
uint64 timestamp_tai64n_seconds = 2;
// TAI-64N timestamp (nanoseconds fraction, 0..999_999_999)
uint32 timestamp_tai64n_nanos = 3;
// Human-readable UTC approximation (for display only — NOT used for ordering)
google.protobuf.Timestamp wall_clock_utc = 4;
// ── Actor Identity ─────────────────────────────────────────────────────────
// Full LDAP distinguished name of the authenticated actor.
// Example: "CN=Jane Smith,OU=Scientists,DC=rtpbio,DC=local"
// For system-generated events (HAL, background jobs): "CN=HELIX-SYSTEM,..."
string actor_dn = 5;
// Blake3 hash of the actor's session token at the time of the event.
// 32 bytes. Allows correlation of entries within a session without
// storing the session token itself (which is a credential).
bytes session_token_hash = 6;
// ── Provenance ─────────────────────────────────────────────────────────────
string site_id = 7;
string software_version = 8;
// Ed25519 public key of the node that created this entry. 32 bytes.
// This is the Node Signing Key (NSK) active at the time of signing.
bytes node_public_key = 9;
// ── Event Payload ──────────────────────────────────────────────────────────
EventType event_type = 10;
oneof payload {
BatchOperationPayload batch_operation = 20;
ParameterChangePayload parameter_change = 21;
DeviationPayload deviation = 22;
UserActionPayload user_action = 23;
SystemEventPayload system_event = 24;
KeyRotationPayload key_rotation = 25;
}
// ── Cryptographic Fields ───────────────────────────────────────────────────
// These fields are NOT included in the canonical serialization for hashing.
// They are derived from the canonical fields and stored for verification efficiency.
// H_n: Blake3 hash of canonical(this entry) ∥ H_{n-1}
bytes entry_hash = 100;
// H_{n-1}: The entry hash of the immediately preceding entry.
// Stored here for verification without requiring the previous entry.
bytes prev_hash = 101;
// Ed25519 signature over the signed message (see §5.2). 64 bytes.
bytes signature = 102;
// Index of this entry as a leaf in the MMR (see §7).
uint64 mmr_leaf_index = 103;
}
// ─────────────────────────────────────────────────────────────────────────────
// Batch Operation Payload
// ─────────────────────────────────────────────────────────────────────────────
message BatchOperationPayload {
// Unique batch identifier. Format: {SITE}-{PRODUCT_CODE}-{YEAR}-{SEQUENCE}
// Example: "RTP01-mAb-2026-0042"
string batch_id = 1;
// Product code (must match the Approved Master Batch Record product code)
string product_code = 2;
// Version of the Master Batch Record this batch was executed against
string mbr_version = 3;
// The specific operation within the batch
BatchOperationType operation_type = 4;
// Bioreactor or unit operation identifier
string unit_id = 5;
// ── Measurement Context (for parameter-setting operations) ─────────────────
// The parameter being set or recorded
string parameter_name = 6;
// The value, as a string. Numeric values use the canonical decimal representation
// with sufficient precision to distinguish adjacent float64 values.
// Example: "36.800000000000001" NOT "36.8" (for temperature in Celsius)
string parameter_value = 7;
// Unit of measure (UCUM string). Example: "Cel", "pH", "L/h", "RPM"
string parameter_unit = 8;
// The in-specification range at the time of this operation.
// Null if this is a non-measured operation (e.g., a batch disposition).
SpecificationRange acceptable_range = 9;
// Whether this parameter was within specification at the time of recording.
// True = in spec. False = out of spec (triggers mandatory deviation initiation).
bool in_specification = 10;
// ── Rationale & References ────────────────────────────────────────────────
// Free-text rationale required for: out-of-spec values, changes to setpoints,
// and any operation flagged as requiring justification in the MBR.
// Maximum 2000 characters. Sanitized to remove control characters.
string rationale = 11;
// SOP references. Array of SOP identifiers relevant to this operation.
// Example: ["SOP-BIO-042 Rev 3", "SOP-CELL-017 Rev 1"]
repeated string sop_references = 12;
// Sequence number of the previous entry for this batch_id.
// Provides a per-batch linked list within the global hash chain,
// enabling efficient batch-scoped reconstruction without full chain traversal.
uint64 prev_batch_entry_sequence = 13;
// Sequence number of the in-progress batch record (the entry that opened
// this batch). Always 0 for the BATCH_OPENED operation type itself.
uint64 batch_open_sequence = 14;
}
// ─────────────────────────────────────────────────────────────────────────────
// Supporting Types
// ─────────────────────────────────────────────────────────────────────────────
enum EventType {
EVENT_TYPE_UNSPECIFIED = 0;
BATCH_OPERATION = 1;
PARAMETER_CHANGE = 2;
DEVIATION_INITIATED = 3;
DEVIATION_CLOSED = 4;
USER_LOGIN = 5;
USER_LOGOUT = 6;
ELECTRONIC_SIGNATURE = 7;
SYSTEM_STARTUP = 8;
SYSTEM_SHUTDOWN = 9;
KEY_ROTATION = 10;
AUDIT_QUERY = 11; // Queries to the ledger are themselves logged
DATA_EXPORT = 12;
WAL_SEGMENT_ROTATION = 13;
CALIBRATION_RECORD = 14;
}
enum BatchOperationType {
BATCH_OP_UNSPECIFIED = 0;
BATCH_OPENED = 1;
BATCH_CLOSED = 2;
PARAMETER_RECORDED = 3;
SETPOINT_CHANGED = 4;
SAMPLE_COLLECTED = 5;
MATERIAL_ADDED = 6;
IPC_SAMPLE_RESULT = 7; // In-process control
BATCH_DISPOSITION = 8; // Release / Reject / Quarantine
ELECTRONIC_BATCH_REVIEW = 9;
}
message SpecificationRange {
string lower_limit = 1; // Canonical decimal string
string upper_limit = 2;
string limit_type = 3; // "ACTION" | "ALERT" | "TARGET"
}
6.2 JSON-LD Representation
For regulatory submission and human-readable export, LedgerEntry messages are rendered as JSON-LD using the HELIX OS ontology context. JSON-LD provides unambiguous semantic meaning to each field, enabling automated processing by regulatory agency systems.
// Example: A temperature setpoint change during bioreactor culture
// Exported via: helix ledger export --format json-ld --sequence 4827193
{
"@context": {
"@vocab": "https://ontology.helix-os.io/ledger/v2#",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"prov": "http://www.w3.org/ns/prov#",
"schema": "https://schema.org/",
"gmp": "https://ontology.helix-os.io/gmp/v1#",
"sequenceNumber": { "@type": "xsd:unsignedLong" },
"timestampTai": { "@type": "xsd:string" },
"wallClockUtc": { "@type": "xsd:dateTime" },
"entryHash": { "@type": "xsd:hexBinary" },
"prevHash": { "@type": "xsd:hexBinary" },
"signature": { "@type": "xsd:hexBinary" },
"nodePublicKey": { "@type": "xsd:hexBinary" }
},
"@type": "LedgerEntry",
"@id": "urn:helix:RTP-SITE-01:entry:4827193",
"sequenceNumber": 4827193,
"timestampTai": "4000000067889a20_0000000007a1b2c3",
"wallClockUtc": "2026-01-15T09:42:16.128339395Z",
"prov:wasAttributedTo": {
"@type": "prov:Agent",
"schema:identifier": "CN=Jane Smith,OU=Process Scientists,DC=rtpbio,DC=local",
"prov:actedOnBehalfOf": "CN=HELIX-SYSTEM,OU=ServiceAccounts,DC=rtpbio,DC=local"
},
"sessionTokenHash": "3f9a1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a",
"siteId": "RTP-SITE-01",
"softwareVersion": "2.4.1",
"nodePublicKey": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"eventType": "BATCH_OPERATION",
"gmp:batchOperation": {
"@type": "gmp:SetpointChange",
"gmp:batchId": "RTP01-mAb-2026-0042",
"gmp:productCode": "mAb-DRUG-003",
"gmp:mbrVersion": "MBR-mAb-003-Rev-4",
"gmp:operationType": "SETPOINT_CHANGED",
"gmp:unitId": "BRX-07",
"gmp:parameterName": "temperature.setpoint",
"gmp:previousValue": "36.8",
"gmp:newValue": "37.2",
"gmp:parameterUnit": "Cel",
"gmp:acceptableRange": {
"gmp:lowerLimit": "36.0",
"gmp:upperLimit": "38.0",
"gmp:limitType": "ACTION"
},
"gmp:inSpecification": true,
"gmp:rationale": "Compensating for measured growth rate decline (0.22 h⁻¹ vs target 0.28 h⁻¹). Action per SOP-BIO-042 §3.4 Rev 3.",
"gmp:sopReferences": [
"SOP-BIO-042 Rev 3",
"SOP-CELL-017 Rev 1"
],
"gmp:prevBatchEntrySequence": 4826001,
"gmp:batchOpenSequence": 4800000
},
"entryHash": "2bc7e8d3f9a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9",
"prevHash": "8d3fa91cb5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8",
"signature": "a1b2c3d4e5f6...64_bytes_hex...f0a1b2c3d4e5",
"mmrLeafIndex": 4827193
}
6.3 Field-Level Constraints
The following constraints are enforced by the HELIX OS ledger writer and validated by helix-verify during chain verification. An entry that violates any constraint is rejected before writing.
| Field | Constraint | Enforcement |
|---|---|---|
sequence_number | Strictly monotonic. No gaps. No reuse. Must equal prev_sequence_number + 1. | Runtime: compared against in-memory last-sequence counter |
timestamp_tai64n | Must be ≥ prev_timestamp_tai64n. (Timestamps are non-decreasing; monotonic clock ensures this.) | Runtime: compared against in-memory last-timestamp |
timestamp_tai64n | Must be within 5 seconds of the current system TAI clock. | Runtime: checked against CLOCK_TAI |
actor_dn | Must match the LDAP DN of an authenticated, active session in the HELIX OS session store. | Runtime: session validation |
session_token_hash | Must be 32 bytes (Blake3 output). | Schema: proto field length constraint |
parameter_value | Numeric values must be representable as IEEE 754 double-precision float. Must not be NaN or Inf. | Runtime: parsed and validated before serialization |
rationale | Required (non-empty) for: in_specification = false, operation_type = SETPOINT_CHANGED, any deviation operation. | Runtime: business rule validation |
entry_hash | Must equal Blake3(canonical(entry) ∥ prev_hash). | Runtime: computed and verified before WAL write |
signature | Must be a valid Ed25519 signature over the signed message (§5.2) by the current NSK. | Runtime: verified immediately after signing |
mmr_leaf_index | Must equal the MMR's current leaf count before appending this entry. | Runtime: checked against MMR state |
7. Merkle Mountain Range — O(log n) Proof of Inclusion
7.1 MMR Structure and Motivation
Regulatory citation: 21 CFR §11.10(b) — The system must support accurate and complete copies for inspection.
Why a Merkle Mountain Range and not a standard Merkle tree?
A standard binary Merkle tree requires all leaves to be known in advance to construct the root. This is incompatible with an append-only ledger where new entries arrive continuously. A Merkle Mountain Range (MMR) is a Merkle tree variant designed for efficient append-only operation:
- Appending a new leaf is O(log n).
- Computing an inclusion proof for any leaf is O(log n).
- Verifying an inclusion proof is O(log n).
- The MMR root (the "MMR commitment") can be computed at any point and represents a cryptographic commitment to all entries up to that point.
Regulatory application. An FDA inspector who wants to verify that a specific batch record entry (e.g., the pH setpoint change at 14:32:07 UTC on batch RTP01-mAb-2026-0042) was genuinely part of the audit trail at the time of the inspection can:
- Receive the entry's content (from HELIX OS or from the site's records).
- Receive the MMR inclusion proof (a list of O(log n) sibling hashes).
- Receive the MMR root at the time of the batch release (a single 32-byte hash, published to the site's ledger commitment registry).
- Verify the proof in <1ms using
helix-verify. No chain replay required.
7.2 Node Appending Algorithm
An MMR is a list of perfect binary Merkle trees (called "peaks") whose sizes are powers of two. When a new leaf is appended, it merges with existing same-height trees until no two trees have the same height.
Example MMR after 11 leaves (binary 1011):
Tree heights: 3 1 0
Peak nodes: H7 H9 L10
(H7 covers L0–L7, H9 covers L8–L9, L10 is a lone leaf)
Appending leaf 11:
New leaf L11 merges with L10 → new node H11 (height 1)
H11 merges with H9 → new node H13 (height 2)
H13 does NOT merge with H7 (height 3 ≠ height 2)
After 12 leaves:
Tree heights: 3 2
Peak nodes: H7 H13
The node hashing function uses Blake3 with domain separation:
// helix-ledger/src/mmr/mod.rs
use blake3::Hasher;
/// Hash for a leaf node (a ledger entry).
/// The leaf hash is Blake3(entry_hash) — we hash the entry hash, not the entry
/// content, to decouple MMR construction from entry serialization.
pub fn mmr_leaf_hash(entry_hash: &[u8; 32]) -> [u8; 32] {
let mut h = Hasher::new();
h.update(b"HELIX:MMR:LEAF:v2\x00");
h.update(entry_hash);
*h.finalize().as_bytes()
}
/// Hash for an internal (parent) node.
/// Takes left and right child hashes; the position index prevents
/// second-preimage attacks where an adversary swaps the tree structure.
pub fn mmr_node_hash(
left: &[u8; 32],
right: &[u8; 32],
node_index: u64,
) -> [u8; 32] {
let mut h = Hasher::new();
h.update(b"HELIX:MMR:NODE:v2\x00");
h.update(&node_index.to_be_bytes());
h.update(left);
h.update(right);
*h.finalize().as_bytes()
}
/// Compute the MMR "bagging" root — a single hash committing to all peaks.
/// The root is computed by hashing the peaks from right to left.
/// This is the value published to the commitment registry.
pub fn mmr_root(peaks: &[[u8; 32]], total_leaf_count: u64) -> [u8; 32] {
let mut h = Hasher::new();
h.update(b"HELIX:MMR:ROOT:v2\x00");
h.update(&total_leaf_count.to_be_bytes());
// Peaks are hashed right-to-left (from smallest tree to largest)
// to ensure that the root changes when a new peak is added on the right
// but not when existing left peaks merge.
for peak in peaks.iter().rev() {
h.update(peak);
}
*h.finalize().as_bytes()
}
pub struct MmrState {
/// All nodes, indexed by their MMR position (not leaf position).
/// The MMR uses 1-based indexing: node at position 1, 2, 3, ...
pub nodes: Vec<[u8; 32]>,
/// The current leaf count.
pub leaf_count: u64,
}
impl MmrState {
/// Append a new leaf to the MMR.
///
/// Returns the MMR position of the newly appended leaf.
/// This position is stored in the ledger entry's `mmr_leaf_index` field.
pub fn append(&mut self, entry_hash: &[u8; 32]) -> u64 {
let leaf_hash = mmr_leaf_hash(entry_hash);
let leaf_pos = self.nodes.len() as u64 + 1;
self.nodes.push(leaf_hash);
self.leaf_count += 1;
// Merge sibling nodes of equal height
let mut current_hash = leaf_hash;
let mut height = 0u64;
loop {
let sibling_pos = leaf_pos - (1 << (height + 1)) + 1;
if sibling_pos == 0 || sibling_pos > self.nodes.len() as u64 {
break;
}
let sibling_height = mmr_node_height(sibling_pos);
if sibling_height != height {
break;
}
let sibling_hash = self.nodes[(sibling_pos - 1) as usize];
let parent_pos = self.nodes.len() as u64 + 1;
let parent_hash = mmr_node_hash(&sibling_hash, ¤t_hash, parent_pos);
self.nodes.push(parent_hash);
current_hash = parent_hash;
height += 1;
}
leaf_pos
}
}
/// Compute the height of an MMR node at position `pos` (1-based).
/// This is determined by counting trailing ones in `pos`'s binary representation.
pub fn mmr_node_height(pos: u64) -> u64 {
let mut p = pos;
let mut h = 0;
while (p & 1) == 1 {
p >>= 1;
h += 1;
}
h
}
7.3 Proof of Inclusion Construction
// helix-ledger/src/mmr/proof.rs
/// An inclusion proof for a single leaf in the MMR.
///
/// Given this proof and the MMR root at a specific point in time,
/// a verifier can confirm that the leaf (identified by `leaf_entry_hash`)
/// was present in the MMR without replaying the entire chain.
#[derive(Debug, Clone)]
pub struct MmrInclusionProof {
/// The entry hash of the leaf being proven.
pub leaf_entry_hash: [u8; 32],
/// The MMR position of the leaf (1-based). Corresponds to `mmr_leaf_index`
/// in the LedgerEntry.
pub leaf_mmr_position: u64,
/// The total leaf count of the MMR at the time this proof was generated.
/// Used to determine the peak structure during verification.
pub mmr_leaf_count_at_proof: u64,
/// The sibling hashes needed to recompute the path from leaf to root.
/// Ordered from leaf-level siblings up to peak-level siblings.
/// Length is O(log n) — specifically, ⌈log₂(n)⌉ + number of peaks.
pub proof_hashes: Vec<[u8; 32]>,
/// The MMR root at the time this proof was generated.
/// This is what the verifier compares against the published commitment.
pub mmr_root_at_proof: [u8; 32],
}
impl MmrState {
/// Generate an inclusion proof for the leaf at `leaf_entry_hash`.
///
/// Complexity: O(log n) — collects sibling hashes along the path
/// from the leaf to the peak, then collects all other peaks.
pub fn generate_proof(
&self,
leaf_mmr_position: u64,
) -> Result<MmrInclusionProof, MmrError> {
if leaf_mmr_position == 0 || leaf_mmr_position > self.nodes.len() as u64 {
return Err(MmrError::LeafNotFound(leaf_mmr_position));
}
let mut proof_hashes = Vec::new();
let mut current_pos = leaf_mmr_position;
// Phase 1: Collect sibling hashes from leaf up to the peak of its subtree
let mut height = 0u64;
loop {
let sibling_pos = if is_left_child(current_pos, height) {
// Left child: sibling is to the right
current_pos + sibling_offset(height)
} else {
// Right child: sibling is to the left
current_pos - sibling_offset(height)
};
if sibling_pos > self.nodes.len() as u64 {
break; // Reached the peak of this subtree
}
proof_hashes.push(self.nodes[(sibling_pos - 1) as usize]);
// Move to parent
let parent_pos = if is_left_child(current_pos, height) {
current_pos + parent_offset(height)
} else {
current_pos + 1 // Right child's parent is always current + 1
};
current_pos = parent_pos;
height += 1;
}
// Phase 2: Collect all other peaks (for "bagging" the root)
let peaks = self.current_peaks();
for peak in &peaks {
if peak.position != current_pos {
proof_hashes.push(peak.hash);
}
}
let root = mmr_root(
&peaks.iter().map(|p| p.hash).collect::<Vec<_>>(),
self.leaf_count,
);
Ok(MmrInclusionProof {
leaf_entry_hash: self.nodes[(leaf_mmr_position - 1) as usize],
leaf_mmr_position,
mmr_leaf_count_at_proof: self.leaf_count,
proof_hashes,
mmr_root_at_proof: root,
})
}
}
fn sibling_offset(height: u64) -> u64 { (1 << (height + 1)) - 1 }
fn parent_offset(height: u64) -> u64 { 1 << (height + 1) }
fn is_left_child(pos: u64, height: u64) -> bool {
// A node is a left child if the bit at position (height+1) in its
// 1-based position is 0.
(pos >> (height + 1)) & 1 == 0
}
7.4 Proof Verification Algorithm
// helix-verify/src/mmr.rs (standalone verifier — no HELIX proprietary code)
/// Verify an MMR inclusion proof.
///
/// This is the function an FDA auditor would call to confirm that a specific
/// ledger entry (identified by its entry_hash) was genuinely present in the
/// HELIX OS audit trail at the time the MMR root was published.
///
/// # Arguments
/// - `proof`: The inclusion proof (received from HELIX OS export or site records)
/// - `published_root`: The MMR root from the site's commitment registry
/// (an immutable external record — e.g., published to the site's blockchain
/// or time-stamped by a qualified TSA per RFC 3161)
///
/// # Returns
/// - `Ok(())` if the proof is valid and the leaf is confirmed present
/// - `Err(VerifyError::RootMismatch)` if the computed root does not match
/// the published root — indicating tampering or proof corruption
pub fn verify_inclusion_proof(
proof: &MmrInclusionProof,
published_root: &[u8; 32],
) -> Result<(), VerifyError> {
// Step 1: Reconstruct the leaf hash from the entry hash
let mut current_hash = mmr_leaf_hash(&proof.leaf_entry_hash);
let mut current_pos = proof.leaf_mmr_position;
let n_peaks = peak_count(proof.mmr_leaf_count_at_proof);
let n_path_hashes = proof.proof_hashes.len() - n_peaks;
// Step 2: Walk up the path from leaf to peak, recomputing parent hashes
for (step, &sibling_hash) in proof.proof_hashes[..n_path_hashes].iter().enumerate() {
let height = step as u64;
let parent_hash = if is_left_child(current_pos, height) {
let parent_pos = current_pos + parent_offset(height);
mmr_node_hash(¤t_hash, &sibling_hash, parent_pos)
} else {
let parent_pos = current_pos + 1;
mmr_node_hash(&sibling_hash, ¤t_hash, parent_pos)
};
current_pos = parent_pos_of(current_pos, height);
current_hash = parent_hash;
}
// Step 3: Reconstruct the MMR root by "bagging" all peaks
// The peaks are: current_hash (our peak) + the remaining proof hashes (other peaks)
let mut all_peaks: Vec<[u8; 32]> = vec![current_hash];
all_peaks.extend_from_slice(&proof.proof_hashes[n_path_hashes..]);
let computed_root = mmr_root(&all_peaks, proof.mmr_leaf_count_at_proof);
// Step 4: Compare against the published root
if &computed_root != published_root {
return Err(VerifyError::RootMismatch {
computed: computed_root,
published: *published_root,
leaf_position: proof.leaf_mmr_position,
});
}
Ok(())
}
fn peak_count(leaf_count: u64) -> usize {
leaf_count.count_ones() as usize
}
7.5 MMR Root Commitment Protocol
The MMR root is a single 32-byte hash that cryptographically commits to the entire ledger contents up to a given point. Publishing this root to an immutable external record transforms it from an internal consistency check into an externally verifiable, tamper-evident commitment.
HELIX OS supports three external commitment mechanisms:
Option A — RFC 3161 Timestamp Authority (TSA). The MMR root is submitted to a qualified TSA (e.g., DigiCert, GlobalSign) which returns a signed timestamp token. The token proves the MMR root existed at a specific UTC time and was signed by a trusted third party. This is the recommended mechanism for sites that do not operate their own blockchain infrastructure.
# Generate and publish the MMR root to a TSA (runs automatically via helix-ledger-daemon)
# Manual invocation for debugging or regulatory submission:
helix ledger commit-root \
--method rfc3161 \
--tsa-url http://timestamp.digicert.com \
--output /var/helix/commitments/$(date +%Y%m%d_%H%M%S)_mmr_root.tsr
# The .tsr file contains:
# - The MMR root (32 bytes)
# - The leaf count at the time of commitment
# - The TSA's digital signature
# - The TSA's certificate chain
# This file is immutable and can be independently verified by any RFC 3161 client.
Option B — Permissioned Blockchain (Hyperledger Fabric). For enterprise sites with Hyperledger Fabric infrastructure, the MMR root is written to a dedicated audit channel. The blockchain's immutability and distributed consensus provide Byzantine-fault-tolerant tamper evidence.
Option C — Cross-Site Merkle Root Exchange. For pharmaceutical companies operating multiple HELIX OS sites, sites periodically exchange MMR roots with each other. An independent third-party can verify that the roots at each site are mutually consistent, preventing a scenario where a single site's records are modified without the others' knowledge.
Commitment frequency. By default, HELIX OS commits the MMR root every 15 minutes (configurable). Sites under active batch operations may reduce this to 1 minute. The commitment frequency is a balance between TSA call overhead and the window during which a hypothetical tamper event could go unrecorded.
8. 21 CFR Part 11 Compliance Matrix
| 21 CFR Part 11 Section | Requirement | HELIX OS Implementation | Section in This Document |
|---|---|---|---|
| §11.10(a) | Validation of systems to ensure accuracy, reliability, consistent intended performance, and the ability to discern invalid or altered records | IQ/OQ/PQ protocol, chain verification tool, CRC-32C on every frame | §3.5, §4.3 |
| §11.10(b) | Ability to generate accurate and complete copies of records in both human readable and electronic form | JSON-LD export, PDF batch record export, helix-verify standalone | §6.2, §7.3 |
| §11.10(c) | Protection of records to enable their accurate and ready retrieval throughout the records retention period | Append-only WAL, O_DIRECT/O_SYNC, no-delete policy requiring ledger entry | §4.1, §4.4 |
| §11.10(d) | Limiting system access to authorized individuals | LDAP/AD authentication, RBAC, session binding in every ledger entry | §6.1 |
| §11.10(e) | Use of secure, computer-generated, time-stamped audit trails to independently record the date and time of operator entries and actions that create, modify, or delete electronic records | Blake3 hash chain, TAI-64N timestamps, monotonic sequence numbers | §3, §3.3 |
| §11.10(f) | Use of operational system checks to enforce permitted sequencing of steps and events | Sequence number monotonicity enforcement, batch record state machine | §6.3 |
| §11.10(g) | Use of authority checks to ensure only authorized individuals can use the system, electronically sign a record, access the operation or computer system input or output device, alter a record | LDAP roles, per-operation authorization in business logic layer | §5.1 |
| §11.10(h) | Use of device (e.g., terminal) checks to determine validity of source of data input or operational instruction | Node Signing Key certificate chain, site_id in every entry | §5.1 |
| §11.10(k) | Use of appropriate controls over systems documentation | Document ID, revision, effective date, change control via ledger entry | This document header |
| §11.30 | Controls for open systems | TLS 1.3 for all network communications, mTLS for instrument connections | HELIX-ARCH-0004 |
| §11.50(a) | Signed electronic records shall contain information associated with the signing that clearly indicates: (1) the printed name of the signer, (2) the date and time when the signature was executed, (3) the meaning (e.g., review, approval, responsibility) of the signature | JSON-LD prov:wasAttributedTo, wallClockUtc, eventType | §6.2 |
| §11.50(b) | The items in §11.50(a) shall be subject to the same controls as for electronic records | All three fields are part of the canonical hash input | §3.3 |
| §11.70 | Electronic signatures and handwritten signatures executed to electronic records shall be linked to their respective electronic records to ensure the signatures cannot be excised, copied, or otherwise transferred to falsify an electronic record | Ed25519 signature over entry_hash which is bound to entry content via Blake3 chain | §5.2 |
9. Threat Model & Attack Surface Analysis
Adversary model. HELIX OS's integrity guarantees are designed to be robust against:
-
Insider threat (low-privilege): An authenticated operator attempts to modify a past record to cover up a process deviation. Defense: Append-only ledger; hash chain invalidation; no
UPDATE/DELETEAPI. -
Insider threat (administrator): A system administrator with root access to the HELIX OS server attempts to modify WAL files directly. Defense: Hash chain verification detects any byte modification. Ed25519 signatures require the NSK private key (in TPM, not accessible to root). MMR root commitments to external TSA are immutable.
-
Software supply chain attack: A modified HELIX OS binary omits ledger entries for certain events. Defense: The
helix-verifytool is open-source and standalone. It detects gaps insequence_number. The SBOM and binary SHA-256 in the VSR allow verification of the deployed binary. -
Cryptographic compromise of Blake3: An adversary finds a collision in Blake3 to produce two entries with the same hash. Defense: 128-bit collision resistance. No practical attack exists or is anticipated in the relevant threat horizon. The Ed25519 signature over the entry hash provides an additional independent layer.
-
Ed25519 key compromise: An adversary obtains the NSK private key. Defense: Key is stored in TPM (cannot be extracted). Even with key compromise, retrospective forgery of past entries is impossible because: (a) the hash chain requires re-hashing all subsequent entries, and (b) the MMR root commitments to the TSA are immutable and would not match the re-hashed chain.
-
Storage device manipulation: An adversary with physical access swaps the NVMe drive. Defense: The genesis context hash (bound to site ID and initialization timestamp) is stored separately in the HSM. The chain will not verify against a different site's genesis.
Residual risk. The system does not protect against:
- An adversary who simultaneously controls the HSM, the TPM, all TSA commitment records, and has the cooperation of the backup site. This is outside the threat model of any single organization's IT security controls.
- Quantum computing attacks on Ed25519 (Grover's algorithm provides 64-bit security against current Ed25519 for signature forgery). HELIX OS's roadmap includes migration to CRYSTALS-Dilithium (NIST PQC winner) in v3.1. The hash chain uses Blake3 which provides 128-bit quantum security via Grover's theorem.
10. Cryptographic Primitive Inventory
All cryptographic primitives used by HELIX OS are documented here for regulatory review and SBOM inclusion.
| Primitive | Algorithm | Implementation | Version | NIST Status | Key Size / Output |
|---|---|---|---|---|---|
| Hash Chain | Blake3 | blake3 crate (Rust) | 1.5.x | NIST-recognized (BLAKE2 SHA-3 finalist lineage) | 256-bit output |
| Digital Signature | Ed25519 | ed25519-dalek crate | 2.x | NIST FIPS 186-5 (EdDSA approved) | 256-bit key, 512-bit signature |
| MMR Node Hash | Blake3 (keyed domain-separated) | blake3 crate | 1.5.x | Same as above | 256-bit output |
| Frame CRC | CRC-32C (Castagnoli) | crc32c crate | 1.x | N/A (integrity, not security) | 32-bit |
| WAL Encryption at Rest | AES-256-GCM | ring crate | 0.17.x | NIST FIPS 197, SP 800-38D | 256-bit key |
| Session Token Hash | Blake3 | blake3 crate | 1.5.x | See above | 256-bit output |
| TSA Timestamp Token | SHA-256 (within RFC 3161) | OpenSSL via TSA | Per TSA | NIST FIPS 180-4 | 256-bit |
| TLS (API / Instrument) | TLS 1.3, AES-256-GCM-SHA384 | rustls | 0.23.x | NIST SP 800-52 Rev 2 | Per TLS 1.3 |
Dependency audit. All crates listed above are included in the HELIX OS SBOM (CycloneDX 1.5 format, /opt/helix/docs/sbom.json). Their source code hashes are verified during the IQ procedure. No network access is required during cryptographic operations — all primitives are statically linked.
11. Appendix A — Reference Implementation
The helix-verify standalone auditor tool is published under the MIT license at github.com/helix-os/helix-verify. It has no proprietary dependencies. An FDA inspector can compile it from source and verify a HELIX OS ledger chain using only:
- The WAL files (or a copy thereof)
- The genesis context hash (from the site's IQ documentation)
- The NSK public key certificate chain (from the site's keystore export)
- The TSA timestamp tokens (for MMR root commitment verification)
# Building helix-verify from source (Rust toolchain required, no HELIX license)
git clone https://github.com/helix-os/helix-verify.git
cd helix-verify
cargo build --release
# SHA-256 of compiled binary must match the value in the VSR
./target/release/helix-verify \
--wal-dir /path/to/wal/copy \
--genesis-hash <hex from IQ report> \
--nsk-cert /path/to/nsk-cert.pem \
--tsa-tokens /path/to/tsr/files/ \
--full-report
12. Appendix B — Test Vectors
These test vectors allow independent verification that a HELIX OS implementation's cryptographic functions are correct. Any conformant implementation must produce these exact outputs.
Blake3 Chain Hash Test Vector
Input:
Domain prefix: "HELIX:LEDGER:ENTRY:v2\x00" (22 bytes)
entry_bytes: 0x0000000000000001 (sequence_number = 1, big-endian u64)
(followed by the full canonical protobuf for a minimal test entry)
prev_hash: 0x0000000000000000000000000000000000000000000000000000000000000000
Expected output H₁:
3f9a1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a
(32 bytes, hex)
Full test vectors including Ed25519 signature vectors, MMR construction vectors for 1, 2, 3, 7, 8, and 11 leaves, and WAL frame CRC vectors are maintained in
helix-verify/tests/vectors/HELIX-TV-0001.json. This file is part of the regulatory submission package.
End of HELIX-COMP-0001.
This document is controlled under the HELIX OS Document Management System. Revisions require approval from the Lead Compliance Engineer and the Quality Director. The revision history is maintained in git log --follow docs/compliance/HELIX-COMP-0001.md and in the HELIX OS change control ledger.
Questions regarding this specification for regulatory submissions should be directed to regulatory@helix-os.io.