Forensic block capture: write-block-gated acquisition, capacity preflight, dual-digest signed manifest, and the CaptureReceipt/VerifiedCapture pair that is the only way to authorize what runs after a capture
| Tier | operations |
| Role | capability |
| Path | crates/operations/block-imaging |
| Edition | 2021 |
| Targets | operations_block_imaging, adapters, capture_order, token, verify_roundtrip, write_block_adapter |
| Public items | 34 across 0 modules |
| Tests | 24 |
What it is for
operations-block-imaging — forensic block capture, and the proof that authorizes everything downstream of it.
Two things live here, and the second is why the first is worth having.
Acquisition (capture) reads a suspect device in one fixed order: the write-block is confirmed, the destination is proven large enough, and only then is a byte read. Both gates refuse before the reader is touched — on a disk you are not allowed to write to, "it returned an error afterwards" is not the same guarantee. A capture that fails mid-stream keeps what it read and never publishes the final artifact name, because deleting a partial acquisition can destroy the only readable data a failing disk will ever give up.
Proof (CaptureReceipt and VerifiedCapture) is the ordering invariant expressed as a type rather than as a file on disk. verify, triage and reinstall are separate process invocations, so the thing that crosses between them is the signed, serializable CaptureReceipt; the VerifiedCapture token is reconstructed inside the consuming process by VerifiedCapture::from_receipt, which is its only constructor and which re-hashes the artifact for itself. A receipt assembled by hand, signed by an untrusted key, replayed from another case or another device, or covering an artifact that has since changed, yields no token — so a caller cannot hold one without the capture having actually happened, for this case, on this device.
What this does NOT claim: against code already running as root on the same host, no userspace mechanism here holds. The guarantee is detection through the signed digest, not prevention.
Testability
The device, the destination and the write-blocker are traits (SourceReader, ArtifactSink, WriteBlockCheck), so every refusal above is proven against fixtures without touching hardware — the same seam idiom operations-device-safety uses for operations_device_safety::DeviceResolver.
Capabilities
crate root
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
fn capacity_sufficient(source_bytes : u64, free_bytes : u64, reserve_bytes : u64) -> bool |
fn digest_hex(bytes : & u8) -> String |
fn capture <W, S, K>(spec : & CaptureSpec, blocker : & mut W, reader : & mut S, sink : & mut K,) -> Result <CaptureManifest, BlockImagingError> where W : WriteBlockCheck + ? Sized, S : SourceReader + ? Sized, K : ArtifactSink + ? Sized, |
fn verify <R : ArtifactReader + ? Sized>(manifest : & CaptureManifest, artifact : & mut R, key : & SigningKey,) -> Result <CaptureReceipt, BlockImagingError> |
ArtifactReader
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub trait ArtifactReader |
ArtifactSink
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub trait ArtifactSink |
BlockImagingError
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub enum BlockImagingError |
BlockdevWriteBlock
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub struct BlockdevWriteBlock |
BlockdevWriteBlock :: fn new(blockdev_bin : impl Into <PathBuf>, device_path : impl Into <String>) -> Self |
BlockdevWriteBlock :: fn confirm_read_only(& mut self, _device : & DeviceId) -> Result <bool, BlockImagingError> |
CaptureManifest
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub struct CaptureManifest |
CaptureReceipt
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub struct CaptureReceipt |
CaptureReceipt :: fn sign(manifest : CaptureManifest, key : & SigningKey) -> Self |
CaptureReceipt :: fn manifest(& self) -> & CaptureManifest |
CaptureSpec
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub struct CaptureSpec |
DeviceReader
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub struct DeviceReader |
DeviceReader :: fn open(path : impl AsRef <Path>) -> Result <Self, BlockImagingError> |
DeviceReader :: fn size_bytes(& self) -> u64 |
DeviceReader :: fn read_chunk(& mut self, buf : & mut u8) -> Result <usize, BlockImagingError> |
DeviceReader :: fn read_chunk(& mut self, buf : & mut u8) -> Result <usize, BlockImagingError> |
FileArtifactSink
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub struct FileArtifactSink |
FileArtifactSink :: fn create(final_path : impl AsRef <Path>, free_bytes : u64,) -> Result <Self, BlockImagingError> |
FileArtifactSink :: fn partial_path(& self) -> & Path |
FileArtifactSink :: fn free_bytes(& self) -> u64 |
FileArtifactSink :: fn write(& mut self, bytes : & u8) -> Result <(), BlockImagingError> |
FileArtifactSink :: fn promote(& mut self) -> Result <(), BlockImagingError> |
SourceReader
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub trait SourceReader |
VerifiedCapture
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub struct VerifiedCapture |
VerifiedCapture :: fn from_receipt <R : ArtifactReader + ? Sized>(receipt : & CaptureReceipt, trusted : & VerifyingKey, expected_case : & str, expected_device : & DeviceId, artifact : & mut R,) -> Result <Self, BlockImagingError> |
VerifiedCapture :: fn case_id(& self) -> & str |
VerifiedCapture :: fn source_device(& self) -> & DeviceId |
VerifiedCapture :: fn artifact_sha256(& self) -> & str |
VerifiedCapture :: fn raw_sha256(& self) -> & str |
WriteBlockCheck
operations-block-imaging — forensic block capture, and the proof that authorizes
| Item |
|---|
pub trait WriteBlockCheck |
How to use it
No examples/ target and no doctest in this crate's rustdoc. The tests listed under Verification are the closest executable usage.
Module structure
No public modules: the crate root is its whole surface.
Public surface
`crate root`
| Item | What it is |
|---|---|
pub enum BlockImagingError | Everything that can go wrong acquiring or proving a capture. |
pub trait SourceReader | Reads the suspect device |
pub trait ArtifactSink | Receives the artifact |
pub trait ArtifactReader | Reads an already-written artifact back, so its digest can be recomputed independently. |
pub trait WriteBlockCheck | Reports whether the source is confirmed read-only |
pub struct CaptureSpec | What a capture is asked to do. |
pub struct CaptureManifest | What a capture attests |
pub struct CaptureReceipt | A signed, serializable attestation that a capture happened and verified |
CaptureReceipt :: fn sign(manifest : CaptureManifest, key : & SigningKey) -> Self | Sign a manifest with the case key. |
CaptureReceipt :: fn manifest(& self) -> & CaptureManifest | What this receipt attests, unverified |
pub struct VerifiedCapture | Proof that a capture happened, verified, and belongs to this case and this device |
VerifiedCapture :: fn from_receipt <R : ArtifactReader + ? Sized>(receipt : & CaptureReceipt, trusted : & VerifyingKey, expected_case : & str, expected_device : & DeviceId, artifact : & mut R,) -> Result <Self, BlockImagingError> | Rebuild the token from a receipt, inside the process that intends to act on it |
VerifiedCapture :: fn case_id(& self) -> & str | The engagement this proof belongs to. |
VerifiedCapture :: fn source_device(& self) -> & DeviceId | The device this proof covers. |
VerifiedCapture :: fn artifact_sha256(& self) -> & str | Digest of the persisted artifact. |
VerifiedCapture :: fn raw_sha256(& self) -> & str | Digest of the bytes read from the device. |
fn capacity_sufficient(source_bytes : u64, free_bytes : u64, reserve_bytes : u64) -> bool | Whether a destination can hold source_bytes plus reserve_bytes |
fn digest_hex(bytes : & u8) -> String | SHA-256 of bytes, lowercase hex |
fn capture <W, S, K>(spec : & CaptureSpec, blocker : & mut W, reader : & mut S, sink : & mut K,) -> Result <CaptureManifest, BlockImagingError> where W : WriteBlockCheck + ? Sized, S : SourceReader + ? Sized, K : ArtifactSink + ? Sized, | Acquire spec.source_device into sink, in the one order that is safe |
fn verify <R : ArtifactReader + ? Sized>(manifest : & CaptureManifest, artifact : & mut R, key : & SigningKey,) -> Result <CaptureReceipt, BlockImagingError> | Independently re-read artifact and, only if it still hashes to what manifest attests, sign a CaptureReceipt for it |
pub struct DeviceReader | Reads a block device — or, for tests and for imaging a file-backed source, any file |
DeviceReader :: fn open(path : impl AsRef <Path>) -> Result <Self, BlockImagingError> | Open path read-only and learn its size |
DeviceReader :: fn size_bytes(& self) -> u64 | — |
DeviceReader :: fn read_chunk(& mut self, buf : & mut u8) -> Result <usize, BlockImagingError> | — |
DeviceReader :: fn read_chunk(& mut self, buf : & mut u8) -> Result <usize, BlockImagingError> | — |
pub struct FileArtifactSink | Writes an artifact to a .partial name and publishes the final one only on promote |
FileArtifactSink :: fn create(final_path : impl AsRef <Path>, free_bytes : u64,) -> Result <Self, BlockImagingError> | Create the .partial companion of final_path |
FileArtifactSink :: fn partial_path(& self) -> & Path | Where the in-progress bytes are landing. |
FileArtifactSink :: fn free_bytes(& self) -> u64 | — |
FileArtifactSink :: fn write(& mut self, bytes : & u8) -> Result <(), BlockImagingError> | — |
FileArtifactSink :: fn promote(& mut self) -> Result <(), BlockImagingError> | — |
pub struct BlockdevWriteBlock | Puts a real blockdev behind capture's first refusal, by driving infrastructure_write_blocker::enable_and_verify — which sets read-only and then checks that it took effect, rather than trusting that the request succeeded |
BlockdevWriteBlock :: fn new(blockdev_bin : impl Into <PathBuf>, device_path : impl Into <String>) -> Self | Bind to a blockdev binary and the /dev path it should act on |
BlockdevWriteBlock :: fn confirm_read_only(& mut self, _device : & DeviceId) -> Result <bool, BlockImagingError> | — |
No pub use re-exports: every item above is declared in this crate.
Boundary
Reaches into foundation, infrastructure.
Shares tier operations with 40 other crates: operations-approval-workflow, operations-assessments, operations-boot-media, operations-browser-agent-worker, operations-camera-discovery, operations-camera-liveview, operations-camera-registry, operations-compliance, … (40 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | operations |
| Architectural role (taxonomy) | capability |
| Location | crates/operations/block-imaging |
| Vocabulary in force (lexicon) | current |
Tier flow. Which tiers this crate's own edges cross.
flowchart LR n_operations["operations"] --> n_foundation["foundation"] n_operations["operations"] --> n_infrastructure["infrastructure"]
Dependencies
Runtime, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `foundation-audit-log` | foundation | no | always |
| `foundation-bounded-io` | foundation | no | always |
| `foundation-crypto-sign` | foundation | no | always |
| `infrastructure-write-blocker` | infrastructure | no | always |
| `operations-device-safety` | operations | no | always |
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
serde | ^1 | derive | no | always |
serde_json | ^1 | — | no | always |
thiserror | ^2 | — | no | always |
tokio | ^1 | full, rt | no | always |
Development, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `foundation-crypto-sign` | foundation | no | always |
Development, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
rand_core | ^0.6 | getrandom | no | always |
Build. None.
Depended on by. 1 workspace crate.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR n_tools_ir_triage["tools-ir-triage"] -->|uses| SELF SELF["operations-block-imaging"] SELF -->|development| n_foundation_crypto_sign["foundation-crypto-sign"] SELF -->|runtime| n_foundation_audit_log["foundation-audit-log"] SELF -->|runtime| n_foundation_bounded_io["foundation-bounded-io"] SELF -->|runtime| n_foundation_crypto_sign["foundation-crypto-sign"] SELF -->|runtime| n_infrastructure_write_blocker["infrastructure-write-blocker"] SELF -->|runtime| n_operations_device_safety["operations-device-safety"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
No Cargo features are defined: every capability is unconditional, so no consumer can receive a half-wired crate.
Targets
| Kind | Name | Source |
|---|---|---|
| lib | operations_block_imaging | `src/lib.rs` |
| test | adapters | `tests/adapters.rs` |
| test | capture_order | `tests/capture_order.rs` |
| test | token | `tests/token.rs` |
| test | verify_roundtrip | `tests/verify_roundtrip.rs` |
| test | write_block_adapter | `tests/write_block_adapter.rs` |
Error model
| Error type | Named by |
|---|---|
BlockImagingError | capture, verify |
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | none detected |
| async runtime | yes |
| database access | none detected |
| network I/O | none detected |
| unsafe code | none detected |
| environment variables | none detected |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
No environment variable is read with a literal name anywhere in this crate. A variable whose key is computed at run time cannot be listed here, and is not claimed to be absent.
Related capabilities
1 workspace crate depends on this one: tools-ir-triage.
Verification
| Kind | Count |
|---|---|
| Unit tests | 2 |
| Integration tests | 22 |
| Examples | 0 |
| Doctests | 0 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
crate root | 16 | 0 | 10 |
What the tests establish, by name:
a_device_reader_returns_exactly_the_bytes_and_the_size—tests/adapters.rsa_real_block_device_reports_its_true_size_not_zero—tests/adapters.rsa_sink_that_is_never_promoted_leaves_the_partial_and_no_final—tests/adapters.rsa_sink_writes_to_partial_and_the_final_name_does_not_exist_until_promoted—tests/adapters.rsfree_bytes_comes_from_the_destination_filesystem_not_a_guess—tests/adapters.rsopening_a_source_that_does_not_exist_fails_rather_than_yielding_an_empty_capture—tests/adapters.rsa_capture_that_fails_mid_stream_never_publishes_the_final_artifact—tests/capture_order.rsa_clean_capture_promotes_and_reports_both_digests—tests/capture_order.rsan_unconfirmed_write_block_refuses_before_the_device_is_read—tests/capture_order.rsinsufficient_capacity_refuses_before_the_device_is_read—tests/capture_order.rsa_receipt_assembled_by_hand_is_refused—tests/token.rsa_receipt_for_another_device_is_refused—tests/token.rsa_receipt_from_another_case_is_refused—tests/token.rsa_receipt_survives_serialization_because_that_is_what_it_is_for—tests/token.rsa_receipt_whose_artifact_was_altered_after_signing_is_refused—tests/token.rsan_honest_receipt_yields_a_token—tests/token.rsa_receipt_is_bound_to_the_key_that_signed_it—tests/verify_roundtrip.rsan_artifact_that_does_not_match_its_manifest_never_produces_a_receipt—tests/verify_roundtrip.rscapture_then_verify_yields_a_receipt_that_opens_the_token—tests/verify_roundtrip.rsa_blockdev_that_cannot_be_run_errors_rather_than_confirming—tests/write_block_adapter.rsa_verified_write_block_confirms—tests/write_block_adapter.rsa_write_block_that_did_not_take_effect_is_never_reported_as_confirmed—tests/write_block_adapter.rsa_source_plus_reserve_that_overflows_does_not_wrap_into_sufficiency—src/lib.rscapacity_must_cover_the_source_and_the_reserve_both—src/lib.rs
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 27 | 34 |
Public modules with a //! block | 0 | 0 |
pie showData
title Public items with rustdoc
"Documented" : 27
"No rustdoc detected" : 7
Metrics
| Metric | Value |
|---|---|
| Rust source files | 1 |
| Source lines | 654 |
| Code lines | 391 |
| Public API items | 34 |
| Public modules | 0 |
| Tests | 24 |
| Examples | 0 |
| Cargo features | 0 |
| Direct runtime dependencies | 9 |
| Workspace reverse dependencies | 1 |
pie showData
title Public API by kind
"enum" : 1
"function" : 4
"method" : 18
"struct" : 7
"trait" : 4
pie showData
title Rust source composition
"Code" : 391
"Blank or comment" : 263
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.