# Tamper-evident logging via hash chain or digital signatures

- **Pattern:** `ab-001387` (`finserv-audit-trail.retention-compliance.tamper-evidence`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001387
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001387)

## Why it matters

An append-only table that cannot be modified by the application is still vulnerable to a privileged database administrator who directly connects to the instance and runs UPDATE or DELETE without going through the application layer. Tamper evidence via hash chains or digital signatures closes that gap — any modification of a stored log entry invalidates the chain and is detectable even after the fact. NIST 800-53 AU-10 (Non-Repudiation) requires that the organization protect against individuals falsely denying having performed particular actions. NIST 800-53 AU-9 requires protection of audit information integrity. PCI-DSS v4.0 Req-10.3.4 requires file-integrity monitoring or change-detection mechanisms on audit logs so existing data cannot be changed without generating alerts — exactly the property hash chains or digital signatures provide. NIST SP 800-92 (Guide to Computer Security Log Management) recommends integrity checking of log archives. CWE-345 (Insufficient Verification of Data Authenticity) is the vulnerability class.

## Severity rationale

Medium because tamper detection is a secondary control — without it, a privileged insider can modify log records directly at the database layer and the breach may go undetected indefinitely.

## Remediation

Add `previous_hash` and `entry_hash` columns, then compute the hash in the logging service in `src/services/audit-logger.ts`:

```ts
import crypto from 'crypto';

async function logWithHashChain(entry: AuditEntry) {
  const previous = await db('transaction_logs')
    .orderBy('created_at', 'desc').first(['entry_hash']);

  const payload = JSON.stringify({
    user_id: entry.userId, operation: entry.operation,
    amount: entry.amount, timestamp: entry.timestamp,
    previous_hash: previous?.entry_hash ?? 'GENESIS'
  });

  const entryHash = crypto
    .createHmac('sha256', process.env.AUDIT_SIGNING_KEY!)
    .update(payload)
    .digest('hex');

  await db('transaction_logs').insert({
    ...entry,
    previous_hash: previous?.entry_hash ?? null,
    entry_hash: entryHash
  });
}
```

Store `AUDIT_SIGNING_KEY` in a secrets manager (AWS Secrets Manager, Vault), not in the application's environment file.

## Detection

- **ID:** `tamper-evidence`
- **Severity:** `medium`
- **What to look for:** Count all cryptographic integrity columns in the audit log table (e.g., `entry_hash`, `previous_hash`, `signature`). Quote the actual column names and hash algorithm used. Examine logging code for cryptographic signing or hashing operations — count every code path that computes a hash or signature. A table without any hash/signature columns does not count as pass — do not pass if no cryptographic integrity mechanism exists.
- **Pass criteria:** Audit log entries include at least 1 cryptographic integrity column (hash chain or digital signature). At least 1 verification function exists that can detect tampering. Quote the actual hash algorithm used (e.g., SHA-256, HMAC-SHA256). Report the count even on pass (e.g., "2 integrity columns found: entry_hash (SHA-256), previous_hash").
- **Fail criteria:** No tamper-detection mechanism found — 0 cryptographic columns in audit log. Audit logs are stored without cryptographic integrity checks.
- **Skip (N/A) when:** Never — tamper evidence is a regulatory requirement for financial systems.
- **Detail on fail:** `"Audit logs are stored as plain records. No hash chain or digital signatures are used to detect tampering."`.
- **Remediation:** Implement a hash chain:

  ```sql
  ALTER TABLE transaction_logs ADD COLUMN previous_hash VARCHAR(256);
  ALTER TABLE transaction_logs ADD COLUMN entry_hash VARCHAR(256) GENERATED ALWAYS AS (
    SHA256(CONCAT(id, timestamp, user_id, operation_type, COALESCE(previous_hash, '')))
  ) STORED;
  ```

  Or use digital signatures:

  ```ts
  import crypto from 'crypto';

  async function logTransaction(userId, operation, amount) {
    const signature = crypto
      .createHmac('sha256', process.env.AUDIT_KEY)
      .update(JSON.stringify({ userId, operation, amount, timestamp: new Date().toISOString() }))
      .digest('hex');

    await db.transactionLogs.create({
      userId,
      operationType: operation,
      amount,
      signature, // Include in record
      timestamp: new Date().toISOString()
    });
  }

  // Verify integrity
  function verifyLog(logEntry, key) {
    const expectedSig = crypto
      .createHmac('sha256', key)
      .update(JSON.stringify({
        userId: logEntry.userId,
        operation: logEntry.operationType,
        amount: logEntry.amount,
        timestamp: logEntry.timestamp
      }))
      .digest('hex');
    return logEntry.signature === expectedSig;
  }
  ```

## External references

- cwe CWE-345
- nist AU-10
- nist AU-9
- pci-dss Req-10.3.4
- nist SP-800-92

Taxons: data-integrity, regulatory-conformance

HTML version: https://auditbuffet.com/patterns/ab-001387
