Skip to content

System Architecture & Engineering

s0 (Sector Zero) is a dual-capability forensic suite that unites two traditionally disjoint disciplines — secure data sanitization and deleted-file recovery — under a single, cryptographically anchored audit framework. Every operation, whether destroying or recovering data, produces an immutable, Ed25519-signed record that can be independently verified by anyone, anywhere, with no server and no trust.

The design principle is mathematical non-repudiation over operational convenience: rather than producing log files that could be edited, every event is hashed into a chain where tampering at any point instantly invalidates every record that follows. s0 is built to answer the hardest question in digital forensics — can you prove it? — with a signature, not a promise.


High-Level Architecture

s0 is organized around three vertical layers that cut across all four functional modules.

graph TB
    subgraph UI["Interface Layer"]
        CLI["s0 CLI<br/><code>linux/cli/s0_cli/main.py</code>"]
        GUI["Web Dashboard<br/><code>web/</code> — FastAPI + Browser"]
        ISO["Bare-Metal Live ISO<br/><code>linux/iso/</code> — Debian Live"]
    end

    subgraph MODULES["Functional Modules"]
        M1["Module 1 — Drive Eraser<br/><code>methods/</code> · <code>wipe.py</code> · <code>devices.py</code>"]
        M2["Module 2 — File &amp; Folder Eraser<br/><code>file_eraser.py</code> · <code>windows/</code> · <code>macos/</code>"]
        M3["Module 3 — File Carver<br/><code>carver/</code> — 5 engines"]
        M4["Module 4 — Audit Ledger<br/><code>audit/</code> — SQLite + SHA-256 chain"]
    end

    subgraph CORE["Cryptographic Core  <code>core/python/s0_core/</code>"]
        CANON["Canonical JSON v1<br/><code>canonical.py</code>"]
        CRYPTO["Ed25519 Sign/Verify<br/><code>crypto.py</code>"]
        CERT["Certificate Builder<br/><code>certificate.py</code>"]
        PDF["PDF + QR Generator<br/><code>pdfgen.py</code>"]
    end

    subgraph VERIFY["Verification Portal  <code>verification-portal/</code>"]
        VP_JS["Ed25519 Verifier<br/><code>verify.js</code> — TweetNaCl"]
        VP_KEYS["Key Pinning<br/><code>keys.json</code>"]
        VP_CRYPTO["Offline Crypto Bundle<br/><code>vendor/crypto-bundle.js</code>"]
    end

    CLI --> M1 & M2 & M3 & M4
    GUI --> M1 & M2 & M3 & M4
    ISO --> CLI

    M1 & M2 & M3 --> CORE
    M4 --> CORE

    CORE --> M4
    CERT --> VP_JS
    VP_KEYS --> VP_JS
    VP_CRYPTO --> VP_JS

    style CORE fill:#00ADB5,color:#222831,stroke:#00ADB5
    style VERIFY fill:#393E46,color:#EEEEEE,stroke:#00ADB5
    style MODULES fill:#222831,color:#EEEEEE,stroke:#393E46
    style UI fill:#222831,color:#EEEEEE,stroke:#393E46

Repository Layout

s0/
├── core/python/s0_core/          # Shared cryptographic core (Ed25519, Canonical JSON, PDF/QR)
│   ├── canonical.py              #   Deterministic JSON serializer — signing contract
│   ├── crypto.py                 #   Ed25519 key-pair management, sign, verify
│   ├── certificate.py            #   Certificate construction and schema validation
│   └── pdfgen.py                 #   ReportLab PDF with embedded QR code
├── linux/cli/s0_cli/             # Linux CLI — primary delivery vehicle
│   ├── main.py                   #   Click entrypoint, subcommand dispatch
│   ├── devices.py                #   Device enumeration (lsblk, sysfs, /proc)
│   ├── wipe.py                   #   Module 1 orchestrator
│   ├── methods/                  #   Module 1 hardware erasure backends
│   │   ├── nvme.py               #     NVMe Sanitize + Format commands
│   │   ├── ata.py                #     ATA Secure Erase (Enhanced + Normal)
│   │   ├── blkdiscard.py         #     BLKDISCARD / TRIM / Unmap
│   │   └── overwrite.py          #     Multi-pass overwrite engine
│   ├── file_eraser.py            #   Module 2 — Linux secure file/folder deletion
│   ├── carver/                   #   Module 3 — forensic carving
│   │   ├── engine.py             #     Dispatcher: routes to correct carver
│   │   ├── signatures.py         #     Magic-byte signature table
│   │   ├── ext4_carver.py        #     ext4 structure parser
│   │   ├── ntfs_carver.py        #     NTFS $MFT parser
│   │   ├── fat_carver.py         #     FAT32 BPB + deleted entry scanner
│   │   ├── exfat_carver.py       #     exFAT VBR + cluster heap walker
│   │   ├── fragmentation.py      #     Non-resident cluster run reassembly
│   │   └── scoring.py            #     Multi-factor confidence scoring
│   └── audit/                    #   Module 4 — blockchain audit ledger
├── windows/                      #   Module 2 Windows (Win32 API, ADS scrubbing, ReFS)
├── macos/                        #   Module 2 macOS (F_FULLFSYNC, xattr, APFS)
├── web/                          #   Unified FastAPI web dashboard (4 tabs)
├── linux/iso/                    #   Debian Live ISO build scripts
└── verification-portal/          #   100% static Ed25519 verifier
    ├── verify.js
    ├── keys.json
    └── vendor/crypto-bundle.js

Module 1 — Secure Drive Eraser

Module 1 translates the NIST SP 800-88 Rev.1 Clear / Purge taxonomy into hardware-native commands. It is not a single algorithm — it is a waterfall of methods tried in strict priority order, each more broadly applicable than the last.

Method Selection Waterfall

flowchart TD
    START(["Device presented to wipe.py"]) --> PROBE["Probe: lsblk + sysfs\nIdentify transport: NVMe / SATA / USB / image"]

    PROBE --> NVMe_Q{"NVMe drive?\nSupports SANITIZE?"}
    NVMe_Q -- "Yes — Block Erase" --> M1["① NVMe Sanitize — Block Erase\nPURGE tier · Cryptographic assurance"]
    NVMe_Q -- "Yes — Crypto Erase" --> M2["② NVMe Sanitize — Crypto Erase\nPURGE tier · Key destruction"]
    NVMe_Q -- "Yes — Format UDE" --> M3["③ NVMe Format — User Data Erase\nPURGE tier · Secure format"]
    NVMe_Q -- "No NVMe / No SANITIZE" --> ATA_Q{"ATA / SATA drive?\nSecurity feature set?"}

    ATA_Q -- "Enhanced supported" --> M4["④ ATA Secure Erase Enhanced\nPURGE tier · Vendor-optimized pattern"]
    ATA_Q -- "Normal only" --> M5["⑤ ATA Secure Erase Normal\nPURGE tier · Single overwrite"]
    ATA_Q -- "No ATA security" --> BLK_Q{"Block device?\nTRIM / Unmap capable?"}

    BLK_Q -- "DRAT + RZAT confirmed" --> M6["⑥ BLKDISCARD — TRIM / Unmap\nPURGE tier · Deterministic zero"]
    BLK_Q -- "DRAT only / uncertain" --> M6B["⑥ BLKDISCARD — TRIM / Unmap\nCLEAR tier · Best-effort"]
    BLK_Q -- "Not TRIM capable" --> M7_Q{"Overwrite method?"}

    M6 --> VERIFY
    M6B --> VERIFY
    M1 & M2 & M3 & M4 & M5 --> VERIFY

    M7_Q -- "Single-pass" --> M7["⑦ Overwrite Zero — 1-Pass\nCLEAR tier · HDDs, raw images"]
    M7_Q -- "Multi-pass" --> M8["⑦ Overwrite Random — N-Pass\nCLEAR tier · User-defined pattern count"]

    M7 & M8 --> VERIFY

    VERIFY["Post-wipe Verification\n64-block sampled readback\n4096 bytes × 64 = 262,144 bytes total"] --> CERT["Ed25519-signed certificate\n→ PDF + QR export\n→ Append to audit ledger"]

    style M1 fill:#00ADB5,color:#222831
    style M2 fill:#00ADB5,color:#222831
    style M3 fill:#00ADB5,color:#222831
    style M4 fill:#00ADB5,color:#222831
    style M5 fill:#00ADB5,color:#222831
    style M6 fill:#393E46,color:#EEEEEE
    style M6B fill:#393E46,color:#EEEEEE
    style M7 fill:#393E46,color:#EEEEEE
    style M8 fill:#393E46,color:#EEEEEE
    style CERT fill:#222831,color:#EEEEEE,stroke:#00ADB5

NIST Tier Mapping

Priority Method Hardware Command NIST Tier IEEE 2883 Notes
1 NVMe Sanitize — Block Erase NVME_SANITIZE_BLOCK_ERASE Purge Purge Physically erases every cell
2 NVMe Sanitize — Crypto Erase NVME_SANITIZE_CRYPTO_ERASE Purge Purge Destroys encryption key
3 NVMe Format — User Data Erase NVME_FORMAT_UDE Purge Purge Controller-level secure format
4 ATA Secure Erase Enhanced SECURITY_ERASE_UNIT (ENHANCED) Purge Purge Vendor-optimized, hits HPA
5 ATA Secure Erase Normal SECURITY_ERASE_UNIT Purge Purge Single-pass controller erase
6 BLKDISCARD (DRAT+RZAT) BLKDISCARD ioctl Purge Purge Deterministic zero guarantee
6 BLKDISCARD (DRAT only) BLKDISCARD ioctl Clear Clear No zero guarantee after discard
7 Overwrite Zero 1-Pass write() + fsync() Clear Clear HDD, USB, raw images
7 Overwrite Random N-Pass write() + fsync() Clear Clear User-configurable pass count

DRAT and RZAT

Deterministic Read After Trim (DRAT) guarantees the controller returns a deterministic value (but not necessarily zero) after a TRIM. Read Zero After Trim (RZAT) further guarantees that value is zero. Only when both are confirmed does BLKDISCARD achieve Purge-tier assurance. s0 reads these flags from the NVMe Identify or ATA IDENTIFY DEVICE response and adjusts the certificate tier accordingly.

USB Flash Drives

USB-attached flash drives typically report neither SANITIZE support nor ATA security features. The controller firmware may silently remap sectors rather than erase them. s0 falls to BLKDISCARD and documents the uncertainty in the certificate. Physical destruction is the only way to achieve Purge-tier certainty for low-cost USB media.

Post-Wipe Verification

After every erasure, s0 performs a sampled readback to provide statistical assurance that the wipe completed:

# 64 blocks sampled uniformly across the device address space
# 4096 bytes read per block → 262,144 bytes verified total
SAMPLE_COUNT = 64
SAMPLE_BYTES = 4096

offsets = [i * (device_size // SAMPLE_COUNT) for i in range(SAMPLE_COUNT)]
for offset in offsets:
    data = read_block(device, offset, SAMPLE_BYTES)
    assert all(b == expected_byte for b in data)  # 0x00 for overwrites

The verification result, including any failed blocks, is embedded in the signed certificate payload.

Scope of Readback

Sampled verification is a probabilistic check, not a 100% full-surface scan. For NVMe Sanitize and ATA Secure Erase, the controller's own completion status is the primary integrity signal; readback is a secondary sanity check. Performing a full surface scan on a multi-terabyte drive would take hours and is outside the scope of standard NIST 800-88 compliance checking.


Module 2 — Secure File & Folder Eraser

Simple deletion (rm, del, Trash) removes the directory entry but leaves file data on disk, recoverable by any carving tool. Module 2 eliminates recovery at every layer: data clusters, filesystem metadata, alternate data streams, extended attributes, and directory entry filenames are all sanitized.

Platform Implementation

Step Mechanism Purpose
1. Strip attributes os.chmod(), os.chflags() Remove immutable/append-only flags
2. Detect CoW /proc/mounts — detect btrfs, ZFS Warn; CoW breaks overwrite guarantees
3. Extent inspection filefrag -v Map physical extents of file data
4. Overwrite POSIX open() + write() Overwrite every data byte in-place
5. Hardware flush fsync(fd) Force controller commit to media
6. Timestamp zero os.utime(path, (0, 0)) Set atime + mtime → epoch 0
7. Filename scramble os.rename() → random string Erase directory entry name
8. Unlink os.unlink() Release inode
Step Win32 API Purpose
1. Strip attributes SetFileAttributesW Remove READ_ONLY, HIDDEN, SYSTEM
2. Detect CoW GetVolumeInformationW → ReFS Warn if ReFS CoW detected
3. Open for write CreateFileW(GENERIC_WRITE\|GENERIC_READ) Exclusive overwrite handle
4. Overwrite WriteFile() In-place cluster overwrite
5. Hardware flush FlushFileBuffers() Bypass write cache, commit to media
6. ADS enumeration FindFirstStreamW / FindNextStreamW Enumerate Alternate Data Streams
7. ADS destruction DeleteFileW(path:streamname) Destroy :Zone.Identifier and all ADS
8. Timestamp zero SetFileTime(0, 0, 0) Zero creation, access, write times
9. Filename scramble MoveFileW() → random name Scrub directory entry
10. Delete DeleteFileW() Release file table entry
Step API / Tool Purpose
1. Detect CoW Mount table inspection → APFS Warn; APFS CoW breaks overwrite
2. Strip quarantine xattr -c (all extended attributes) Remove com.apple.quarantine and all xattrs
3. Open for write open() with O_WRONLY Standard POSIX write handle
4. Overwrite write() In-place data overwrite
5. Hardware flush fcntl(fd, F_FULLFSYNC, 0) Bypass HFS+ journal, hardware-level commit
6. Timestamp zero os.utime(path, (0, 0)) Set atime + mtime → epoch 0
7. Filename scramble os.rename() → random string Erase directory entry name
8. Unlink os.unlink() Release inode

Copy-on-Write Filesystem Warning

CoW Filesystems Break Overwrite Guarantees

On Btrfs, ZFS (Linux), APFS (macOS), and ReFS (Windows), write operations do not overwrite the original data blocks. Instead, the filesystem writes new data to a fresh location and atomically updates the pointer. The original data blocks remain allocated to snapshots or the extent tree until garbage-collected.

In practice: calling write() on a file on these filesystems will overwrite the logical data the OS presents, but the physical old blocks remain on disk and are recoverable with structure-aware carvers — including s0's own Module 3.

s0 detects CoW filesystems at runtime and issues a WARNING in the certificate. The only reliable file sanitization on CoW filesystems is to wipe the entire volume with Module 1.

Erasure Flow

sequenceDiagram
    participant User
    participant FileEraser
    participant OS as OS / Kernel
    participant Disk as Physical Media

    User->>FileEraser: s0 file erase /path/to/secret.doc

    FileEraser->>OS: Stat /target/volume/file.dat (check filesystem type)
    OS-->>FileEraser: ext4 (not CoW) [OK]

    FileEraser->>OS: filefrag -v secret.doc
    OS-->>FileEraser: Extent map [LBA 0x1A3F00 – 0x1A4200]

    FileEraser->>OS: open(O_WRONLY) → write(zeros) → fsync()
    OS->>Disk: Write zeros to LBA 0x1A3F00 – 0x1A4200
    Disk-->>OS: Commit confirmed

    FileEraser->>OS: utime(path, (0, 0))
    Note over OS: atime = mtime = 1970-01-01T00:00:00Z

    FileEraser->>OS: rename(secret.doc → x7k2p9q1m4)
    Note over OS: Directory entry name scrubbed

    FileEraser->>OS: unlink(x7k2p9q1m4)
    Note over OS: Inode deallocated

    FileEraser->>FileEraser: Build certificate payload
    FileEraser->>FileEraser: Ed25519 sign (Canonical JSON v1)
    FileEraser-->>User: Certificate written + audit ledger appended

Module 3 — File Carver & Recovery Engine

Module 3 is the forensic counterpart to Module 2: it recovers what sanitization tools fail to reach. It implements five distinct carving strategies, selected by the engine dispatcher based on device characteristics and user configuration.

Engine Selection Logic

flowchart TD
    INPUT(["Raw device / image / partition"]) --> PROBE["engine.py: probe target\nRead first 512 bytes"]

    PROBE --> EXT4{"Superblock at offset 1024?\nMagic: 0xEF53?"}
    EXT4 -- Yes --> E1["ext4 Structure Engine\next4_carver.py"]

    EXT4 -- No --> NTFS{"Boot sector OEM?\n'NTFS    ' (8 bytes)?"}
    NTFS -- Yes --> E2["NTFS Structure Engine\nntfs_carver.py"]

    NTFS -- No --> EXFAT{"OEM Name?\n'EXFAT   ' at offset 3?"}
    EXFAT -- Yes --> E3["exFAT Structure Engine\nexfat_carver.py"]

    EXFAT -- No --> FAT{"BPB signature?\n0x55AA at offset 510?"}
    FAT -- Yes --> E4["FAT32 Structure Engine\nfat_carver.py"]

    FAT -- No --> E5["Signature Engine\nengine.py + signatures.py\n4 MiB sliding window"]

    E1 & E2 & E3 & E4 --> FRAG{"Non-resident / fragmented\nclusters detected?"}
    FRAG -- Yes --> REASSEMBLE["Fragment Reassembly\nfragmentation.py"]
    FRAG -- No --> SCORE

    REASSEMBLE --> SCORE

    E5 --> SCORE["Confidence Scoring\nscoring.py"]
    SCORE --> OUTPUT["Recovered files with\nSHA-256 hash + confidence %"]

    style E1 fill:#00ADB5,color:#222831
    style E2 fill:#00ADB5,color:#222831
    style E3 fill:#00ADB5,color:#222831
    style E4 fill:#00ADB5,color:#222831
    style E5 fill:#393E46,color:#EEEEEE

Engine 1 — Signature Carver (engine.py + signatures.py)

Applied to unrecognized filesystems and raw block devices. Scans the device as a byte stream using a 4 MiB sliding window, searching for known magic byte sequences.

Supported signatures:

Format Header Magic Footer Max Extract Size
JPEG FF D8 FF FF D9 30 MB
PNG 89 50 4E 47 0D 0A 1A 0A 49 45 4E 44 AE 42 60 82 30 MB
PDF 25 50 44 46 2D (%PDF-) %%EOF 50 MB
ZIP 50 4B 03 04 (PK\x03\x04) 50 4B 05 06 100 MB
GIF 47 49 46 38 (GIF8) 00 3B 15 MB
GZIP 1F 8B 08 — (length-bounded) 50 MB
BMP 42 4D (BM) — (length from header) 30 MB
ELF 7F 45 4C 46 (\x7FELF) — (ELF header size) 15 MB
SQLite3 53 51 4C 69 74 65... (SQLite format 3\0) 100 MB
MP3 49 44 33 (ID3) 15 MB

Footer Matching

Where a format defines an unambiguous end-of-file marker (JPEG FF D9, PNG IEND, ZIP central directory end), s0 uses bounded extraction — it carves exactly from header to footer. For formats with no footer (ELF, BMP), extraction is bounded by the value embedded in the format's own header (e.g., ELF e_shoff + e_shentsize × e_shnum).

Engine 2 — ext4 Structure Engine (ext4_carver.py)

Directly parses ext4 filesystem internals from the raw block device, bypassing the OS mount layer entirely.

Byte 1024:  Superblock
  └── s_magic:        0xEF53         ← filesystem verification
  └── s_block_size:   1024 << s_log_block_size
  └── s_blocks_per_group, s_inodes_per_group

Block Group 0+n:  Block Group Descriptor (at block 1 or 2)
  └── bg_inode_table   ← byte offset of inode table for this group

Inode Table:
  └── For each inode: i_mode, i_size, i_links_count, i_dtime
      └── i_dtime != 0  →  DELETED inode candidate
      └── i_block[0..14]:  EXT4_EXTENT_HEADER magic (0xF30A)
          └── Extent tree: (ee_block, ee_len, ee_start_hi/lo) tuples
              └── Physical block addresses → read data clusters

What it recovers: Deleted files whose inode has i_dtime set (deletion timestamp populated by the OS at unlink time) but whose data blocks have not yet been reallocated. The extent tree gives direct physical cluster addresses, enabling content recovery without directory traversal.

Engine 3 — NTFS Structure Engine (ntfs_carver.py)

Parses the Master File Table ($MFT) directly from the NTFS volume.

Boot Sector (offset 0):
  └── OEM ID: "NTFS    " (8 bytes, padded with spaces)
  └── Bytes Per Sector, Sectors Per Cluster
  └── $MFT cluster offset  →  $MFT start byte

$MFT Records (1024 bytes each):
  └── FILE signature: 0x46 0x49 0x4C 0x45
  └── Attribute list:
      ├── $FILE_NAME (0x30)  →  filename, timestamps, parent dir ref
      ├── $DATA (0x80):
      │   ├── Resident (data embedded in record, size < ~900 bytes)
      │   └── Non-resident → RunList (VCN→LCN mapping tuples)
      └── $STANDARD_INFORMATION (0x10) → permissions, timestamps

Runlist decoding: Non-resident data is stored as a sequence of (length, offset_delta) pairs. s0 iterates the runlist, accumulating the LCN (Logical Cluster Number) to reconstruct the ordered list of physical clusters that make up the file, then concatenates them.

Engine 4 — FAT32 Structure Engine (fat_carver.py)

Parses the BPB (BIOS Parameter Block) from the boot sector and scans the root directory and FAT chains.

Boot Sector (offset 0, signature 0x55AA at offset 510):
  └── BPB_BytsPerSec, BPB_SecPerClus
  └── BPB_RsvdSecCnt, BPB_NumFATs, BPB_FATSz32
  └── BPB_RootClus  →  root directory cluster

Directory Entry (32 bytes):
  ├── Name[0] == 0xE5  →  DELETED entry
  ├── DIR_Name[1..10]  →  8.3 filename (first char was replaced by 0xE5)
  ├── DIR_FstClusHI + DIR_FstClusLO  →  starting cluster
  └── DIR_FileSize

s0 reassembles the FAT chain from the starting cluster, reading each cluster's next-cluster pointer from the FAT table, until encountering 0x0FFFFFF8 (end-of-chain). Data for deleted entries is read from the chain even though the FAT entries may have been zeroed, using the starting cluster from the directory entry.

Engine 5 — exFAT Structure Engine (exfat_carver.py)

Handles modern SD cards, large USB drives, and cameras that use exFAT (no 4 GB file limit).

VBR (Volume Boot Record, offset 3):
  └── OEM Name: "EXFAT   " (8 bytes)
  └── ClusterHeapOffset, ClusterCount, FirstClusterOfRootDirectory

Directory Entry Sets (32 bytes each):
  ├── Type 0x85 / 0x05 (File)       →  file attributes, timestamps
  ├── Type 0xC0 / 0x40 (Stream Ext) →  data length, first cluster
  └── Type 0xC1 / 0x41 (File Name)  →  Unicode filename (15 chars/entry)

Entry types with the high bit set (e.g., 0x85) are in-use; types with the high bit clear (e.g., 0x05) are deleted. s0 scans for deleted entry sets and recovers files whose cluster data is still intact.

Fragment Reassembly (fragmentation.py)

When a file's data was stored in non-contiguous clusters — common on heavily fragmented drives — s0 uses bifragment heuristic reassembly:

  1. Collect all candidate cluster runs from the structure engine (ext4 extent tree, NTFS runlist, FAT chain).
  2. For each gap between consecutive runs, probe adjacent clusters using signature continuity (does the data look like a plausible continuation of the preceding content?).
  3. Assemble the best-scoring candidate sequence into the output file.

Fragmentation Limits

Bifragment reassembly works reliably for two-fragment files (the most common case). Files split into three or more fragments across non-adjacent regions may be reassembled incorrectly or incompletely. The confidence score reflects this uncertainty.

Confidence Scoring (scoring.py)

Every carved file receives a 0–100% confidence score composed of four independently weighted factors:

\[ \text{confidence} = 0.30 \cdot H + 0.30 \cdot F + 0.20 \cdot S + 0.20 \cdot E \]
Factor Weight Signal
H — Header match 30% Magic bytes at offset 0 match the expected signature exactly
F — Footer match 30% End-of-file marker found at the predicted position
S — Size plausibility 20% File size falls within the format's known valid range
E — Shannon entropy 20% Entropy sampled at 3 points (start, middle, end); pattern matches expected entropy for this format (e.g., compressed data ≈ 7.9 bits/byte; executable code ≈ 6.0 bits/byte)

Interpreting Scores

  • ≥ 85% — High confidence. Header + footer + plausible size + entropy all consistent. Files at this threshold are typically complete and valid.
  • 60–84% — Moderate confidence. Typically missing a footer (truncated file) or minor entropy anomaly. Content is likely recoverable but should be validated by format-specific tools.
  • < 60% — Low confidence. Only the header was found. Treat as a fragment or false positive.

Module 4 — Blockchain Audit Ledger

Module 4 provides the chain of custody infrastructure that transforms s0's operations from "I ran a tool" into cryptographically provable events.

Architecture

The audit ledger lives at ~/.s0/s0_audit.db — a single SQLite file shared by both the CLI and the FastAPI web dashboard. There is one ledger per operator identity, and every operation — regardless of which interface initiated it — appends to the same chain.

block-beta
    columns 1
    B0["Genesis Block\nindex=0 · op_type=GENESIS\nprev_hash=0000…0000"]
    space
    B1["Block #1\nop_type=DRIVE_ERASE\ntarget=/dev/nvme0n1\noperator=alice\nprev_hash=SHA256(B0)"]
    space
    B2["Block #2\nop_type=FILE_ERASE\ntarget=/home/alice/secret.doc\noperator=alice\nprev_hash=SHA256(B1)"]
    space
    B3["Block #3\nop_type=FILE_CARVE\ntarget=/mnt/evidence.img\noperator=alice\nprev_hash=SHA256(B2)"]

    B0 --> B1
    B1 --> B2
    B2 --> B3

Block Hash Formula

Every block's hash is computed over the pipe (|) concatenation of all semantically significant fields from that block:

block_hash = SHA256(
    block_index  || "|" ||  # Block sequence number (integer as string)
    timestamp    || "|" ||  # ISO 8601 UTC timestamp
    op_type      || "|" ||  # DRIVE_ERASE | FILE_ERASE | FILE_CARVE | FORENSIC_IMAGING
    target_id    || "|" ||  # Device serial / file path hash
    operator_id  || "|" ||  # Operator identity string
    organization || "|" ||  # Organization / issuing authority name
    cert_uuid    || "|" ||  # Certificate UUID (links to signed cert)
    payload_hash || "|" ||  # SHA-256 of the full operation payload JSON
    signature    || "|" ||  # Ed25519 signature (base64url) of the certificate
    prev_hash               # Hash of the immediately preceding block
)

The || operator denotes byte-level concatenation of UTF-8 encoded strings separated by |. The result is a 64-character lowercase hex digest stored in the block_hash column.

Tamper Detection Example

Suppose an adversary modifies Block #1 in the SQLite database to change the target device from /dev/nvme0n1 to /dev/sdb:

Original Block #1:  target_id = "SERIAL:WD-WX12345678"
Tampered Block #1:  target_id = "SERIAL:WD-WX99999999"

Because target_id is an input to SHA256(…), the computed hash of Block #1 changes. Block #2's prev_hash now does not match the recomputed hash of Block #1. The verification engine, which iterates the chain from the genesis block forward, detects this at Block #2:

s0 audit verify

  Block #0 (GENESIS)        [OK] hash verified
  Block #1 (DRIVE_ERASE)    [FAIL] INTEGRITY FAILURE
                              stored:   9f8e7d...
                              computed: 000000... (prev_hash mismatch)
  Block #2 (FILE_ERASE)     [SKIP] skipped (upstream failure)
  Block #3 (FILE_CARVE)     [SKIP] skipped (upstream failure)

  Chain integrity: BROKEN at block 1

No Silent Tampering or Deletion

Because prev_hash is an input to every subsequent block's hash, it is not possible to surgically alter a past record without recomputing every downstream block. Furthermore, each block's block_hash is signed with the operator's Ed25519 private key (block_signature). An adversary who attempts to delete an intermediate block and recompute subsequent block hashes forward will fail verification because they cannot forge valid Ed25519 block signatures without the authority private key. For maximum security, operators can also periodically export or anchor the tip hash to external, write-once storage.

Operation Types

op_type Triggered by payload_hash covers
DRIVE_ERASE Module 1 wipe completion Method, passes, verification results, device serial
FILE_ERASE Module 2 file erasure File path hash, size, platform, CoW status
FILE_CARVE Module 3 carving session Target image, engines used, recovered file hashes, confidence scores

Cryptographic Core (core/python/s0_core/)

The cryptographic core is the sole source of truth for all signing, serialization, and certificate construction. No module signs anything independently — all cryptographic operations route through this library.

s0 Canonical JSON v1 (canonical.py)

Standard json.dumps() is not deterministic across implementations: key ordering varies, float formatting diverges, and whitespace handling differs. If two implementations produce different byte sequences for the same logical object, Ed25519 signature verification fails between them. Canonical JSON v1 solves this with seven inviolable rules:

Rule Specification
Encoding UTF-8, no BOM
Object keys Sorted by Unicode code point, recursively at every depth
Whitespace None — separators are , and : only; no newlines, no spaces
String escaping Minimal: "\", \\\, control chars U+0000–U+001F via standard escapes; non-ASCII is not \uXXXX-escaped
Numbers Integers only, base-10, no leading zeros, no fractions, no exponents
Literals true, false, null verbatim
Array order Preserved as-is (arrays are intentionally ordered)

Why no floats?

Float-to-string formatting is the single most common source of inter-implementation divergence in JSON signing schemes. ES6's JSON.stringify and Python's json.dumps format the same float differently in edge cases. s0 eliminates the problem at the schema level: every numeric field in a certificate is an integer (sizes in bytes, durations in seconds). An implementation encountering a float in a certificate payload must refuse, not guess.

# canonical.py — core sorting rule (simplified)
def canonicalize(obj: Any) -> bytes:
    if isinstance(obj, dict):
        # Keys sorted by Unicode code point, recursively
        return b"{" + b",".join(
            canonicalize(k) + b":" + canonicalize(v)
            for k, v in sorted(obj.items())
        ) + b"}"
    elif isinstance(obj, list):
        return b"[" + b",".join(canonicalize(i) for i in obj) + b"]"
    elif isinstance(obj, str):
        return json.dumps(obj, ensure_ascii=False).encode("utf-8")
    elif isinstance(obj, int) and not isinstance(obj, bool):
        return str(obj).encode("utf-8")
    # bool and None handled separately
    ...

Ed25519 Signing (crypto.py)

s0 uses pure Ed25519 (RFC 8032) — not the prehash (Ed25519ph) or context (Ed25519ctx) variants.

Signing pipeline:
  1. Construct certificate object (all fields except "signature")
  2. payload_bytes  =  canonicalize(cert_minus_signature)   ← deterministic bytes
  3. sig            =  ed25519.sign(private_key, payload_bytes)
  4. cert["signature"] = {
         "algorithm":              "Ed25519",
         "public_key_fingerprint": "sha256:<hex of DER-encoded SubjectPublicKeyInfo>",
         "signature_base64url":    base64url_unpadded(sig),
         "signed_payload_hash":    "sha256:<sha256hex(payload_bytes)>"
     }

Verification pipeline:
  1. Extract cert["signature"]["signature_base64url"] → sig_bytes
  2. Remove cert["signature"] from object → cert_minus_sig
  3. payload_bytes = canonicalize(cert_minus_sig)
  4. ed25519.verify(pinned_public_key, payload_bytes, sig_bytes)
     → True | raise InvalidSignature

signed_payload_hash is informational

The signed_payload_hash field in the signature block is a human-readable annotation for auditors who want to confirm what was signed without running full verification. Verifiers must recompute the canonical payload and check the Ed25519 signature directly — the hash field is not a shortcut.

Certificate Construction (certificate.py)

A certificate is a self-describing JSON document that bundles the operation record, the operator identity, the tool version, and the cryptographic proof into a single portable object:

{
  "schema_version": 1,
  "cert_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "issued_at": "2026-09-09T13:33:57Z",
  "tool_version": "2.0.0",
  "operator": {
    "id": "alice@example.com",
    "key_fingerprint": "sha256:a1b2c3d4..."
  },
  "operation": {
    "type": "DRIVE_ERASE",
    "target": { "serial": "WD-WX12345678", "model": "WDC WD40EZRZ" },
    "method": "NVMe Sanitize — Block Erase",
    "nist_tier": "Purge",
    "verification": { "blocks_sampled": 64, "blocks_passed": 64 }
  },
  "signature": {
    "algorithm": "Ed25519",
    "public_key_fingerprint": "sha256:a1b2c3d4...",
    "signature_base64url": "U29tZVNpZ25hdHVyZUJ5dGVz...",
    "signed_payload_hash": "sha256:deadbeef..."
  }
}

PDF + QR Generation (pdfgen.py)

For human-facing deliverables, s0 exports each certificate as a formatted PDF containing:

  1. Operation summary — method, device, timestamp, NIST tier, operator
  2. Machine-readable QR code — encodes the full certificate JSON
  3. Signature block — algorithm, public key fingerprint, base64url signature
  4. Verification instructions — URL to the verification portal + air-gap instructions

The QR code is generated with qrcode and embedded as a vector path in the ReportLab PDF canvas, making it resolution-independent for print.


Verification Portal (verification-portal/)

The verification portal is a 100% static website — no server, no API, no cloud dependency. It can be opened directly from a USB drive in any modern browser with file:///path/to/index.html.

Architecture

flowchart LR
    subgraph BROWSER["Browser (air-gapped)"]
        HTML["index.html\nDrop-zone for certificate JSON"]
        JS["verify.js\nEd25519 verification logic"]
        BUNDLE["vendor/crypto-bundle.js\nTweetNaCl — self-contained crypto"]
        KEYS["keys.json\nPinned authority public keys"]
    end

    CERT["Certificate JSON\n(from s0 output or PDF QR)"] -->|"Drag and Drop\nor file picker"| HTML
    HTML --> JS
    KEYS --> JS
    BUNDLE --> JS

    JS --> RESULT{"Verification result"}
    RESULT -- "Key pinned + signature valid" --> GREEN["ACCREDITED\nSigned by known authority"]
    RESULT -- "Signature valid, key unknown" --> AMBER["VALID\nSignature checks out,\nkey not in pinned list"]
    RESULT -- "Signature fails" --> RED["TAMPERED\nCertificate data does\nnot match signature"]

    style GREEN fill:#2e7d32,color:#fff
    style AMBER fill:#e65100,color:#fff
    style RED fill:#b71c1c,color:#fff
    style BROWSER fill:#222831,color:#EEEEEE,stroke:#00ADB5

Key Pinning (keys.json)

{
  "trusted_authorities": [
    {
      "name": "s0 Authority — Production",
      "fingerprint": "sha256:a1b2c3d4e5f6...",
      "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQ...\n-----END PUBLIC KEY-----",
      "accredited": true
    }
  ]
}

The portal checks the certificate's public_key_fingerprint against every entry in keys.json. The result mapping:

Condition Indicator Meaning
Signature valid AND key in keys.json [ACCREDITED] Certificate is authentic and issued by a recognized operator
Signature valid AND key NOT in keys.json [UNVERIFIED] Cryptographically intact but operator is unknown to this portal instance
Signature invalid (any reason) [INVALID] Certificate data was modified after signing

Adding Custom Authorities

Organizations running their own s0 deployments can fork the portal and add their operators' public key fingerprints to keys.json. The portal is intentionally static — there is no backend to compromise and no CDN to hijack.

Offline / Air-Gap Usage

The vendor/crypto-bundle.js file contains a fully self-contained build of TweetNaCl with no external fetches. Download the portal repository once, move it to a read-only USB drive, and carry it to the air-gapped machine. Verification works identically with file:// URLs and without any network connection.


Threat Model

Threat Actor Mitigation Residual Risk
Incomplete erasure leaving data residue Passive recovery, forensic competitor Module 1 waterfall selects the strongest available hardware-native erase method; post-wipe sampled readback; certificate documents tier explicitly CoW filesystems; HPA/DCO hidden areas; remapped bad sectors (SMART) — all documented in Limitations
Evidence tampering — certificate forgery Adversary with file access Ed25519 signature over Canonical JSON v1; forging requires the private key If the operator's private key is compromised, certificates can be forged — key management is the operator's responsibility
Audit ledger falsification Insider with DB access SHA-256 hash chain with Ed25519 block signing; modifying any block breaks downstream hashes; deleting or replacing blocks fails block signature verification against pinned authority keys An adversary with local root DB access who completely wipes the database file causes a loss of records; rebuilding a valid forward chain is prevented by Ed25519 block signatures; external tip anchoring provides independent verification
Verification portal compromise (supply chain) CDN hijack, MITM All crypto runs from vendored crypto-bundle.js — no CDN, no external fetch; portal is fully auditable static HTML If the portal files themselves are replaced on disk before use, integrity is broken — verify portal file hashes out-of-band
CoW filesystem bypass — file data survives Forensic examiner on the same volume Module 2 detects CoW filesystems at runtime and warns; certificate annotates the limitation Physical CoW snapshots may retain the original data; Module 2 cannot solve this without Module 1 level access
Directory entry name reconstruction Filesystem journal / log analysis Filename scrambled to random string before unlink; timestamps zeroed to epoch 0 Journal-enabled filesystems (ext4 data=journal, NTFS) may retain the original name in journal entries not yet overwritten
Partial readback verification miss Physical media defect hiding data 64-block sampled readback; any mismatching block is a certificate failure Non-sampled blocks are not verified; statistical, not exhaustive
Carving evidence chain of custody break Defense challenge to carved evidence Every carved artifact SHA-256 hashed at extraction; session manifest Ed25519-signed; block appended to audit ledger Confidence score < 100% on reassembled fragments; operator must document scoring threshold policy
Air-gap isolation failure during wipe Network exfiltration during operation s0 is fully offline; the Live ISO has no network-mounted filesystems by default; no telemetry Operator is responsible for network isolation at the hardware level
Private key on compromised system Malware key theft Key management is outside s0's scope; operators should use hardware tokens (YubiKey) for signing Stolen key = broken non-repudiation; s0 cannot defend against this without HSM integration