mirror of
https://github.com/overcuriousity/trace.git
synced 2025-12-20 13:02:21 +00:00
Refactor note signing: sign hash only + comprehensive documentation
Changed the cryptographic signing approach to be more efficient and standard:
**Signing Logic Changes:**
1. **Note-level signing** (CLI & TUI):
- Old: Sign "Hash: {hash}\nContent: {content}"
- New: Sign only the SHA256 hash
- Rationale: Hash already proves integrity (timestamp+content),
signature proves authenticity. More efficient, standard approach.
2. **Export-level signing** (unchanged):
- Entire markdown export is GPG-signed
- Provides document-level integrity verification
**Implementation:**
- trace/cli.py: Updated quick_add_note() to sign hash only
- trace/tui_app.py: Updated note creation dialog to sign hash only
- Updated export format labels to clarify what's being signed:
"SHA256 Hash (timestamp:content)" and "GPG Signature of Hash"
**Documentation (NEW):**
Added comprehensive "Cryptographic Integrity & Chain of Custody" section
to README.md explaining:
- Layer 1: Note-level integrity (hash + signature)
- Layer 2: Export-level integrity (document signature)
- First-run GPG setup wizard
- Internal verification workflow (TUI symbols: ✓/✗/?)
- External verification workflow (court/auditor use case)
- Step-by-step verification instructions
- Cryptographic trust model diagram
- Security considerations and limitations
Added "CRYPTOGRAPHIC INTEGRITY" section to in-app help (press ?):
- Explains dual-layer signing approach
- Shows verification symbol meanings
- Documents 'v' key for verification details
- External verification command
**Verification Workflow:**
1. Investigator: trace --export + gpg --armor --export
2. Recipient: gpg --import pubkey.asc
3. Document: gpg --verify export.md
4. Individual notes: Extract signature blocks and verify
Files modified:
- README.md: +175 lines of documentation
- trace/cli.py: Sign hash only, update labels
- trace/tui_app.py: Sign hash only, add help section
This commit is contained in:
175
README.md
175
README.md
@@ -131,6 +131,181 @@ After this, you can log with just: `t "Your note here"`
|
|||||||
| **Tag System** | Supports `#hashtags` for classification and filtering. | **Efficient triage** of large log sets. |
|
| **Tag System** | Supports `#hashtags` for classification and filtering. | **Efficient triage** of large log sets. |
|
||||||
| **Minimal Footprint** | Built solely on Python standard library modules. | **Maximum portability** on restricted forensic environments. |
|
| **Minimal Footprint** | Built solely on Python standard library modules. | **Maximum portability** on restricted forensic environments. |
|
||||||
|
|
||||||
|
## Cryptographic Integrity & Chain of Custody
|
||||||
|
|
||||||
|
`trace` implements a dual-layer cryptographic system designed for legal admissibility and forensic integrity:
|
||||||
|
|
||||||
|
### Layer 1: Note-Level Integrity (Always Active)
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. **Timestamp Generation** - Precise Unix timestamp captured at note creation
|
||||||
|
2. **Content Hashing** - SHA256 hash computed from `timestamp:content`
|
||||||
|
3. **Optional Signature** - Hash is signed with investigator's GPG private key
|
||||||
|
|
||||||
|
**Mathematical Representation:**
|
||||||
|
```
|
||||||
|
hash = SHA256(timestamp + ":" + content)
|
||||||
|
signature = GPG_Sign(hash, private_key)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security Properties:**
|
||||||
|
- **Temporal Integrity**: Timestamp is cryptographically bound to content (cannot backdate notes)
|
||||||
|
- **Tamper Detection**: Any modification to content or timestamp invalidates the hash
|
||||||
|
- **Non-Repudiation**: GPG signature proves who created the note (if signing enabled)
|
||||||
|
- **Efficient Storage**: Signing only the hash (64 hex chars) instead of full content
|
||||||
|
|
||||||
|
### Layer 2: Export-Level Integrity (On Demand)
|
||||||
|
|
||||||
|
When exporting to markdown (`--export`), the **entire export document** is GPG-signed if signing is enabled.
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Generate complete markdown export with all cases, evidence, and notes
|
||||||
|
2. Individual note signatures are preserved within the export
|
||||||
|
3. Entire document is clearsigned with GPG
|
||||||
|
|
||||||
|
**Security Properties:**
|
||||||
|
- **Document Integrity**: Proves export hasn't been modified after generation
|
||||||
|
- **Dual Verification**: Both individual notes AND complete document can be verified
|
||||||
|
- **Chain of Custody**: Establishes provenance from evidence collection through report generation
|
||||||
|
|
||||||
|
### First-Run GPG Setup
|
||||||
|
|
||||||
|
On first launch, `trace` runs an interactive wizard to configure GPG signing:
|
||||||
|
|
||||||
|
1. **GPG Detection** - Checks if GPG is installed (gracefully continues without if missing)
|
||||||
|
2. **Key Selection** - Lists available secret keys from your GPG keyring
|
||||||
|
3. **Configuration** - Saves selected key ID to `~/.trace/settings.json`
|
||||||
|
|
||||||
|
**If GPG is not available:**
|
||||||
|
- Application continues to function normally
|
||||||
|
- Notes are hashed (SHA256) but not signed
|
||||||
|
- You can enable GPG later by editing `~/.trace/settings.json`
|
||||||
|
|
||||||
|
### Verification Workflows
|
||||||
|
|
||||||
|
#### Internal Verification (Within trace TUI)
|
||||||
|
|
||||||
|
The TUI automatically verifies signatures and displays status symbols:
|
||||||
|
- `✓` - Signature verified with public key in keyring
|
||||||
|
- `✗` - Signature verification failed (tampered or missing key)
|
||||||
|
- `?` - Note is unsigned
|
||||||
|
|
||||||
|
**To verify a specific note:**
|
||||||
|
1. Navigate to the note in TUI
|
||||||
|
2. Press `Enter` to view note details
|
||||||
|
3. Press `v` to see detailed verification information
|
||||||
|
|
||||||
|
#### External Verification (Manual/Court)
|
||||||
|
|
||||||
|
**Scenario**: Forensic investigator sends evidence to court/auditor
|
||||||
|
|
||||||
|
**Step 1 - Investigator exports evidence:**
|
||||||
|
```bash
|
||||||
|
# Export all notes with signatures
|
||||||
|
trace --export --output investigation-2024-001.md
|
||||||
|
|
||||||
|
# Export public key for verification
|
||||||
|
gpg --armor --export investigator@agency.gov > investigator-pubkey.asc
|
||||||
|
|
||||||
|
# Send both files to recipient
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2 - Recipient verifies document:**
|
||||||
|
```bash
|
||||||
|
# Import investigator's public key
|
||||||
|
gpg --import investigator-pubkey.asc
|
||||||
|
|
||||||
|
# Verify entire export document
|
||||||
|
gpg --verify investigation-2024-001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected output if valid:**
|
||||||
|
```
|
||||||
|
gpg: Signature made Mon Dec 13 14:23:45 2024
|
||||||
|
gpg: using RSA key ABC123DEF456
|
||||||
|
gpg: Good signature from "John Investigator <investigator@agency.gov>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3 - Verify individual notes (optional):**
|
||||||
|
|
||||||
|
Individual note signatures are embedded in the markdown export. To verify a specific note:
|
||||||
|
|
||||||
|
1. Open `investigation-2024-001.md` in a text editor
|
||||||
|
2. Locate the note's signature block:
|
||||||
|
```
|
||||||
|
- **GPG Signature of Hash:**
|
||||||
|
```
|
||||||
|
-----BEGIN PGP SIGNED MESSAGE-----
|
||||||
|
Hash: SHA256
|
||||||
|
|
||||||
|
a3f5b2c8d9e1f4a7b6c3d8e2f5a9b4c7d1e6f3a8b5c2d9e4f7a1b8c6d3e0f5a2
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
...
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
|
```
|
||||||
|
3. Extract the signature block (from `-----BEGIN PGP SIGNED MESSAGE-----` to `-----END PGP SIGNATURE-----`)
|
||||||
|
4. Save to a file and verify:
|
||||||
|
```bash
|
||||||
|
cat > note-signature.txt
|
||||||
|
<paste signature block>
|
||||||
|
Ctrl+D
|
||||||
|
|
||||||
|
gpg --verify note-signature.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**What gets verified:**
|
||||||
|
- The SHA256 hash proves the note content and timestamp haven't changed
|
||||||
|
- The GPG signature proves who created that hash
|
||||||
|
- Together: Proves this specific content was created by this investigator at this time
|
||||||
|
|
||||||
|
### Cryptographic Trust Model
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Note Creation (Investigator) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. Content: "Malware detected on host-192.168.1.50" │
|
||||||
|
│ 2. Timestamp: 1702483425.123456 │
|
||||||
|
│ 3. Hash: SHA256(timestamp:content) │
|
||||||
|
│ → a3f5b2c8d9e1f4a7b6c3d8e2f5a9b4c7... │
|
||||||
|
│ 4. Signature: GPG_Sign(hash, private_key) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Export Generation │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. Build markdown with all notes + individual sigs │
|
||||||
|
│ 2. Sign entire document: GPG_Sign(document) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Verification (Court/Auditor) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. Import investigator's public key │
|
||||||
|
│ 2. Verify document signature → Proves export integrity │
|
||||||
|
│ 3. Verify individual notes → Proves note authenticity │
|
||||||
|
│ 4. Recompute hashes → Proves content hasn't changed │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
**What is protected:**
|
||||||
|
- ✓ Content integrity (hash detects any modification)
|
||||||
|
- ✓ Temporal integrity (timestamp cryptographically bound)
|
||||||
|
- ✓ Attribution (signature proves who created it)
|
||||||
|
- ✓ Export completeness (document signature proves no additions/removals)
|
||||||
|
|
||||||
|
**What is NOT protected:**
|
||||||
|
- ✗ Note deletion (signatures can't prevent removal from database)
|
||||||
|
- ✗ Selective disclosure (investigator can choose which notes to export)
|
||||||
|
- ✗ Sequential ordering (signatures are per-note, not chained)
|
||||||
|
|
||||||
|
**Trust Dependencies:**
|
||||||
|
- You must trust the investigator's GPG key (verify fingerprint out-of-band)
|
||||||
|
- You must trust the investigator's system clock was accurate
|
||||||
|
- You must trust the investigator didn't destroy contradictory evidence
|
||||||
|
|
||||||
## TUI Reference (Management Console)
|
## TUI Reference (Management Console)
|
||||||
|
|
||||||
Execute `trace` (no arguments) to enter the Text User Interface. This environment is used for setup, review, and reporting.
|
Execute `trace` (no arguments) to enter the Text User Interface. This environment is used for setup, review, and reporting.
|
||||||
|
|||||||
@@ -49,12 +49,13 @@ def quick_add_note(content: str):
|
|||||||
note.extract_tags() # Extract hashtags from content
|
note.extract_tags() # Extract hashtags from content
|
||||||
note.extract_iocs() # Extract IOCs from content
|
note.extract_iocs() # Extract IOCs from content
|
||||||
|
|
||||||
# Try signing if enabled
|
# Try signing the hash if enabled
|
||||||
signature = None
|
signature = None
|
||||||
if settings.get("pgp_enabled", True):
|
if settings.get("pgp_enabled", True):
|
||||||
gpg_key_id = settings.get("gpg_key_id", None)
|
gpg_key_id = settings.get("gpg_key_id", None)
|
||||||
if gpg_key_id:
|
if gpg_key_id:
|
||||||
signature = Crypto.sign_content(f"Hash: {note.content_hash}\nContent: {note.content}", key_id=gpg_key_id)
|
# Sign only the hash (hash already includes timestamp:content for integrity)
|
||||||
|
signature = Crypto.sign_content(note.content_hash, key_id=gpg_key_id)
|
||||||
if signature:
|
if signature:
|
||||||
note.signature = signature
|
note.signature = signature
|
||||||
else:
|
else:
|
||||||
@@ -169,9 +170,9 @@ def format_note_for_export(note: Note) -> str:
|
|||||||
# Properly indent multi-line content
|
# Properly indent multi-line content
|
||||||
for line in note.content.splitlines():
|
for line in note.content.splitlines():
|
||||||
lines.append(f" {line}\n")
|
lines.append(f" {line}\n")
|
||||||
lines.append(f" - Hash: `{note.content_hash}`\n")
|
lines.append(f" - SHA256 Hash (timestamp:content): `{note.content_hash}`\n")
|
||||||
if note.signature:
|
if note.signature:
|
||||||
lines.append(" - **Individual Note Signature:**\n")
|
lines.append(" - **GPG Signature of Hash:**\n")
|
||||||
lines.append(" ```\n")
|
lines.append(" ```\n")
|
||||||
# Indent signature for markdown block
|
# Indent signature for markdown block
|
||||||
for line in note.signature.splitlines():
|
for line in note.signature.splitlines():
|
||||||
|
|||||||
@@ -1346,12 +1346,22 @@ class TUI:
|
|||||||
help_lines.append((" Highlighted in red in full note views", curses.A_DIM))
|
help_lines.append((" Highlighted in red in full note views", curses.A_DIM))
|
||||||
help_lines.append((" Note Navigation Press Enter on any note to view with highlighting", curses.A_NORMAL))
|
help_lines.append((" Note Navigation Press Enter on any note to view with highlighting", curses.A_NORMAL))
|
||||||
help_lines.append((" Selected note auto-centered and highlighted", curses.A_DIM))
|
help_lines.append((" Selected note auto-centered and highlighted", curses.A_DIM))
|
||||||
help_lines.append((" Integrity All notes SHA256 hashed + optional GPG signing", curses.A_NORMAL))
|
|
||||||
help_lines.append((" GPG Settings Press 's' to toggle signing & select GPG key", curses.A_NORMAL))
|
|
||||||
help_lines.append((" Source Hash Store evidence file hashes for chain of custody", curses.A_NORMAL))
|
help_lines.append((" Source Hash Store evidence file hashes for chain of custody", curses.A_NORMAL))
|
||||||
help_lines.append((" Export Run: trace --export --output report.md", curses.A_DIM))
|
help_lines.append((" Export Run: trace --export --output report.md", curses.A_DIM))
|
||||||
help_lines.append(("", curses.A_NORMAL))
|
help_lines.append(("", curses.A_NORMAL))
|
||||||
|
|
||||||
|
# Cryptographic Integrity
|
||||||
|
help_lines.append(("CRYPTOGRAPHIC INTEGRITY", curses.A_BOLD | curses.color_pair(2)))
|
||||||
|
help_lines.append((" Layer 1: Notes SHA256(timestamp:content) proves integrity", curses.A_NORMAL))
|
||||||
|
help_lines.append((" GPG signature of hash proves authenticity", curses.A_DIM))
|
||||||
|
help_lines.append((" Layer 2: Export Entire export document GPG-signed", curses.A_NORMAL))
|
||||||
|
help_lines.append((" Dual verification: individual + document level", curses.A_DIM))
|
||||||
|
help_lines.append((" Verification ✓=verified ✗=failed ?=unsigned", curses.A_NORMAL))
|
||||||
|
help_lines.append((" Press 'v' on note detail for verification info", curses.A_DIM))
|
||||||
|
help_lines.append((" GPG Settings Press 's' to toggle signing & select GPG key", curses.A_NORMAL))
|
||||||
|
help_lines.append((" External Verify gpg --verify exported-file.md", curses.A_DIM))
|
||||||
|
help_lines.append(("", curses.A_NORMAL))
|
||||||
|
|
||||||
# Data Location
|
# Data Location
|
||||||
help_lines.append(("DATA STORAGE", curses.A_BOLD | curses.color_pair(2)))
|
help_lines.append(("DATA STORAGE", curses.A_BOLD | curses.color_pair(2)))
|
||||||
help_lines.append((" All data: ~/.trace/data.json", curses.A_NORMAL))
|
help_lines.append((" All data: ~/.trace/data.json", curses.A_NORMAL))
|
||||||
@@ -2622,7 +2632,8 @@ class TUI:
|
|||||||
|
|
||||||
signed = False
|
signed = False
|
||||||
if pgp_enabled:
|
if pgp_enabled:
|
||||||
sig = Crypto.sign_content(f"Hash: {note.content_hash}\nContent: {note.content}", key_id=gpg_key_id or "")
|
# Sign only the hash (hash already includes timestamp:content for integrity)
|
||||||
|
sig = Crypto.sign_content(note.content_hash, key_id=gpg_key_id or "")
|
||||||
if sig:
|
if sig:
|
||||||
note.signature = sig
|
note.signature = sig
|
||||||
signed = True
|
signed = True
|
||||||
|
|||||||
Reference in New Issue
Block a user