evtree

package module
v0.0.0-...-09637af Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 1, 2026 License: AGPL-3.0 Imports: 21 Imported by: 0

README

Evtree

Evtree (Evidence Tree) is a Go library for forensic evidence integrity, providing Merkle tree based integrity validator, chain of custody documentation, RFC 3161 trusted timestamping, structured audit trails, and tamper-evident sealing.

At its core, the library computes a deterministic, directory-aware Merkle tree hash over a set of files. Given a list of file entries, each described by a relative path, byte size, SHA-256 digest, and modification time, the library reconstructs the directory hierarchy from the file paths and recursively hashes each directory node. Leaf nodes are constructed by prepending a domain separation byte (0x00) to a canonical string representation of the file metadata, then computing the SHA-256 digest. Internal directory nodes are computed by sorting their children lexicographically by name, concatenating the child hashes with a distinct domain separation byte (0x01), and hashing the result. This structure ensures that the final root hash is sensitive to both the content and the hierarchical organisation of the file tree, and that it is fully deterministic regardless of the order in which file entries are provided.

During acquisition, the library collects file entries into a signed evidence acquisition alongside case metadata — including case number, exhibit reference, examiner identity, device identifiers, and organisational details — providing a structured record of the circumstances under which the evidence was obtained. Files that cannot be read during acquisition, whether due to access restrictions or device errors, are recorded as evidence errors with a timestamp and the reason for failure rather than causing the acquisition to abort. This ensures that partial acquisitions are documented rather than silently discarded, which is critical when dealing with locked or protected files on seized devices.

Once an evidence acquisition has been produced, the library supports RFC 3161 trusted timestamping. The root hash is submitted to a trusted timestamping authority (TSA), which returns a cryptographically signed token binding the hash to a specific point in time. This token is stored within the acquisition and can be verified at any stage by recomputing the root hash and comparing it against the hash embedded in the token. Because the token is signed by an independent third party, it directly addresses the weakness of relying on system clocks — which can be manipulated — by anchoring the acquisition to an externally verifiable time source.

This is particularly important for maintaining chain of custody in digital forensic investigations. When evidence is acquired from a device, any subsequent handling, transfer, or storage introduces the possibility of accidental or deliberate modification. A single root hash computed at the time of acquisition serves as a cryptographic seal over the entire evidence set. At any later stage, whether during analysis, peer review, or courtroom presentation, the same hash can be recomputed from the files on hand and compared against the original. If even a single byte in any file has changed, or if a file has been added, removed, or moved to a different directory, the root hash will differ, immediately revealing that the evidence has been altered. Because the tree mirrors the directory structure, it is also possible to isolate which branch of the hierarchy was affected without rehashing the entire collection, identifying precisely which files were added, deleted, or modified between any two acquisitions. This provides both a tamper detection mechanism and an efficient means of auditing evidence integrity across custodial transfers.

The library is primarily used for MESH, where it provides tamper-evident integrity verification of acquired forensic artifacts via androidqf.

This work is inspired by ECo-Bag: An elastic container based on merkle tree as a universal digital evidence acquisition. Acknowledgements to the authors and Korea Univ.

Installation

go get github.com/BARGHEST-ngo/Evtree

Reference

Acquisition
Function Description
Acquire(root string, meta CaseMetadata) (Acquisition, []EvidenceError, error) Walk a directory and produce an evidence acquisition
AcquireDir(root string) ([]FileEntry, []EvidenceError, error) Walk a directory and return raw file entries
MerkleFromDir(root string) (Hash32, error) Compute the root Merkle hash of a directory
Integrity
Function Description
Compare(comp1, comp2 Acquisition) ([]Added, []Deleted, []Modified, error) Compare two acquisitions and return changes
Verify(root string, meta CaseMetadata, comp1 string) (Result, []EvidenceError, error) Re-acquire a live directory and compare against a saved acquisition in one call
Timestamp(acquisition *Acquisition, tsaURL string) error Request an RFC 3161 timestamp from a TSA and store it in the acquisition
VerifyTimestamp(acquisition Acquisition) error Verify the stored RFC 3161 timestamp token against the acquisition root hash
Storage
Function Description
(Acquisition) Save(filename string) error Serialise an acquisition to JSON
LoadAcquisition(path string) (Acquisition, error) Load an acquisition from JSON
Seal(acquisition Acquisition, evidenceDir string, recipient age.Recipient, outPath string) error Encrypt the evidence directory into a tamper-evident age-encrypted ZIP archive, with the acquisition manifest written inside and as a detached JSON file
Unseal(sealedPath string, identity age.Identity, outDir string) (Acquisition, error) Decrypt a sealed archive and extract its contents, returning the acquisition manifest
Audit Trail

A hash-chained, append-only log of events (acquisition, transfer, sealing, access, etc.). Each entry records the action, outcome, examiner and case metadata, host, and PID, and is stamped with a sequence number and timestamp. Entries are linked by SHA-256: every entry stores the hash of the previous entry (prev_hash) and its own hash (entry_hash), so any modification, deletion, or reordering of an earlier entry breaks the chain. Entries are written to the provided io.Writer as newline-delimited JSON.

Function Description
AuditLog(w io.Writer) *Logger Create a logger that appends entries to w
(*Logger) Log(action Action, outcome Outcome, input TrailEntry) (TrailEntry, error) Append one hash-chained entry, returning the completed entry

TODO

  • Audit trail verification — re-read a persisted trail, recompute each hash, and confirm the chain is intact
  • Digital signatures — sign the root hash with the examiner's private key for non-repudiation

Usage

meta := evtree.CaseMetadata{
    CaseNumber:   "2024-001",
    ExhibitRef:   "EX-01",
    Examiner:     "J. Smith",
    Organisation: "Digital Forensics Lab",
    DeviceModel:  "Pixel 7",
}

// Acquire evidence at time of seizure
acquisition, errs, err := evtree.Acquire("/path/to/evidence", meta)
if err != nil {
    log.Fatal(err)
}
if len(errs) > 0 {
    // handle files that could not be read
}

// Anchor to a trusted timestamp
if err := evtree.Timestamp(&acquisition, evtree.DefaultTSA); err != nil {
    log.Fatal(err)
}

// Save to disk
if err := acquisition.Save("evidence.json"); err != nil {
    log.Fatal(err)
}

// Later: reload, verify timestamp, and compare against a re-acquired directory
acquisition1, err := evtree.LoadAcquisition("evidence.json")
if err != nil {
    log.Fatal(err)
}
if err := evtree.VerifyTimestamp(acquisition1); err != nil {
    log.Fatal(err)
}

acquisition2, _, err := evtree.Acquire("/path/to/evidence", meta)
if err != nil {
    log.Fatal(err)
}

added, deleted, modified, err := evtree.Compare(acquisition1, acquisition2)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Added: %d  Deleted: %d  Modified: %d\n", len(added), len(deleted), len(modified))
Audit Trail
// Open an append-only log file for the trail
f, err := os.OpenFile("audit.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
    log.Fatal(err)
}
defer f.Close()

logger := evtree.AuditLog(f)

// Record a successful acquisition
_, err = logger.Log(evtree.ActionAcquire, evtree.OutcomeSuccess, evtree.TrailEntry{
    Examiner:   "J. Smith",
    Org:        "Digital Forensics Lab",
    CaseNumber: "2024-001",
    ExhibitRef: "EX-01",
    Subject:    "Acquired disk image",
    Details:    map[string]string{"tool": "dc3dd"},
})
if err != nil {
    log.Fatal(err)
}

// Each call appends a hash-chained line to audit.log

Documentation

Index

Constants

View Source
const DefaultTSA = "https://freetsa.org/tsr"

Variables

This section is empty.

Functions

func Acquire

func Acquire(root string, meta CaseMetadata) (Acquisition, []EvidenceError, error)

func AcquireDir

func AcquireDir(root string) ([]FileEntry, []EvidenceError, error)

func Compare

func Compare(comp1 Acquisition, comp2 Acquisition) ([]Added, []Deleted, []Modified, error)

func Seal

func Seal(acquisition Acquisition, evidenceDir string, recipient age.Recipient, outPath string) error

func Timestamp

func Timestamp(bag *Acquisition, tsaURL string) error

func Verify

func Verify(root string, meta CaseMetadata, comp1 string) (Result, []EvidenceError, error)

func VerifyTimestamp

func VerifyTimestamp(bag Acquisition) error

Types

type Acquisition

type Acquisition struct {
	Timestamp      time.Time    `json:"acquired_at"`
	Case           CaseMetadata `json:"case"`
	Entries        []FileEntry  `json:"entries"`
	Root           *TreeNode    `json:"root"`
	TimestampToken []byte       `json:"timestamp_token,omitempty"`
	TimestampedAt  time.Time    `json:"timestamped_at,omitempty"`
}

func LoadAcquisition

func LoadAcquisition(path string) (Acquisition, error)

func Unseal

func Unseal(sealedPath string, identity age.Identity, outZipPath string) (Acquisition, error)

Unseal decrypts an age-encrypted archive produced by Seal, writing the decrypted ZIP to outZipPath and returning the acquisition manifest stored inside.

func (Acquisition) Save

func (b Acquisition) Save(filename string) error

type Action

type Action string

to keep consistency

const (
	ActionOpen      Action = "open"
	ActionAcquire   Action = "acquire"
	ActionSeal      Action = "seal"
	ActionUnseal    Action = "unseal"
	ActionVerify    Action = "verify"
	ActionTimestamp Action = "timestamp"
	ActionTransfer  Action = "transfer"
	ActionAccess    Action = "access"
	ActionClose     Action = "close"
)

type Added

type Added struct {
	Event
}

type CaseMetadata

type CaseMetadata struct {
	CaseNumber   string `json:"case_number"`
	ExhibitRef   string `json:"exhibit_ref"`
	Examiner     string `json:"examiner"`
	Organisation string `json:"organisation"`
	DeviceIMEI   string `json:"device_imei,omitempty"`
	DeviceSerial string `json:"device_serial,omitempty"`
	DeviceModel  string `json:"device_model,omitempty"`
	Notes        string `json:"notes,omitempty"`
}

func (CaseMetadata) Validate

func (m CaseMetadata) Validate() error

type Deleted

type Deleted struct {
	Event
}

type Event

type Event struct {
	Mtime  time.Time
	Path   string
	Sha256 Hash32
}

type EvidenceError

type EvidenceError struct {
	Timestamp time.Time
	Everror   error
	File      string
}

type FileEntry

type FileEntry struct {
	Path   string    `json:"path"`
	Size   int64     `json:"size"`
	Sha256 Hash32    `json:"sha256"`
	Mtime  time.Time `json:"modificationtime"`
}

type Hash32

type Hash32 [32]byte

func Hash32FromHex

func Hash32FromHex(s string) (Hash32, error)

func MerkleFromDir

func MerkleFromDir(root string) (Hash32, error)

func (Hash32) MarshalJSON

func (h Hash32) MarshalJSON() ([]byte, error)

func (Hash32) String

func (h Hash32) String() string

func (*Hash32) UnmarshalJSON

func (h *Hash32) UnmarshalJSON(data []byte) error

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

func AuditLog

func AuditLog(w io.Writer) *Logger

func (*Logger) Log

func (l *Logger) Log(action Action, outcome Outcome, input TrailEntry) (TrailEntry, error)

type Modified

type Modified struct {
	Path      string
	MtimeOld  time.Time
	MtimeNew  time.Time
	Sha256Old Hash32
	Sha256New Hash32
}

type Outcome

type Outcome string

we want to log even failures as evidence

const (
	OutcomeSuccess Outcome = "success"
	OutcomeFailure Outcome = "failure"
)

type Result

type Result struct {
	Verified bool
	Event    []Event
	Modified []Modified
}

type TrailEntry

type TrailEntry struct {
	Seq        uint64            `json:"seq"`
	Timestamp  time.Time         `json:"timestamp"`
	Action     Action            `json:"action"`
	Outcome    Outcome           `json:"outcome"`
	Examiner   string            `json:"examiner"`
	Org        string            `json:"organisation"`
	Host       string            `json:"host"`
	PID        int               `json:"pid"`
	CaseNumber string            `json:"case_number"`
	ExhibitRef string            `json:"exhibit_ref"`
	Subject    string            `json:"subject"`
	Details    map[string]string `json:"details,omitempty"`
	ErrorMsg   string            `json:"error,omitempty"`
	PrevHash   string            `json:"prev_hash"`
	EntryHash  string            `json:"entry_hash"`
}

type TreeNode

type TreeNode struct {
	Hash     Hash32               `json:"merkle_hash"`
	Size     int64                `json:"size,omitempty"`
	Sha256   *Hash32              `json:"sha256,omitempty"`
	Children map[string]*TreeNode `json:"children,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL