nip/src/nimpak/protection.nim

259 lines
8.9 KiB
Nim

# SPDX-License-Identifier: LSL-1.0
# Copyright (c) 2026 Markus Maiwald
# Stewardship: Self Sovereign Society Foundation
#
# This file is part of the Nexus Sovereign Core.
# See legal/LICENSE_SOVEREIGN.md for license terms.
# Read-Only Protection Manager
#
# This module implements the read-only protection system for CAS storage,
# ensuring immutability by default with controlled write access elevation.
#
# SECURITY NOTE: chmod-based protection is a UX feature, NOT a security feature!
# In user-mode (~/.local/share/nexus/cas/), chmod 555 only prevents ACCIDENTAL
# deletion/modification. A user who owns the files can bypass this trivially.
#
# Real security comes from:
# 1. Merkle tree verification (cryptographic integrity)
# 2. User namespaces (kernel-enforced read-only mounts during execution)
# 3. Root ownership (system-mode only: /var/lib/nexus/cas/)
#
# See docs/cas-security-architecture.md for full security model.
import std/[os, times, sequtils, strutils]
import xxhash
import ./types
type
ProtectionManager* = object
casPath*: string # Path to CAS root directory
auditLog*: string # Path to audit log file
SecurityEvent* = object
timestamp*: DateTime
eventType*: string
hash*: string
details*: string
severity*: string # "info", "warning", "critical"
proc newProtectionManager*(casPath: string): ProtectionManager =
## Create a new protection manager for the given CAS path
result = ProtectionManager(
casPath: casPath,
auditLog: casPath / "audit.log"
)
proc logOperation*(pm: ProtectionManager, op: string, path: string, hash: string = "") =
## Log a write operation to the audit log
try:
let timestamp = now().format("yyyy-MM-dd'T'HH:mm:ss'Z'")
var logEntry = "[" & timestamp & "] " & op & " path=" & path
if hash.len > 0:
logEntry.add(" hash=" & hash)
logEntry.add("\n")
let logFile = open(pm.auditLog, fmAppend)
logFile.write(logEntry)
logFile.close()
except IOError:
# If we can't write to audit log, continue anyway
# (better to allow operation than to fail)
discard
proc setReadOnly*(pm: ProtectionManager): VoidResult[NimPakError] =
## Set CAS directory to read-only (chmod 555)
try:
setFilePermissions(pm.casPath, {fpUserRead, fpUserExec,
fpGroupRead, fpGroupExec,
fpOthersRead, fpOthersExec})
pm.logOperation("SET_READONLY", pm.casPath)
return ok(NimPakError)
except OSError as e:
return VoidResult[NimPakError](isOk: false, errValue: NimPakError(
code: FileWriteError,
msg: "Failed to set read-only permissions: " & e.msg
))
proc setWritable*(pm: ProtectionManager): VoidResult[NimPakError] =
## Set CAS directory to writable (chmod 755)
try:
setFilePermissions(pm.casPath, {fpUserRead, fpUserWrite, fpUserExec,
fpGroupRead, fpGroupExec,
fpOthersRead, fpOthersExec})
pm.logOperation("SET_WRITABLE", pm.casPath)
return ok(NimPakError)
except OSError as e:
return VoidResult[NimPakError](isOk: false, errValue: NimPakError(
code: FileWriteError,
msg: "Failed to set writable permissions: " & e.msg
))
proc withWriteAccess*(pm: ProtectionManager, operation: proc()): VoidResult[NimPakError] =
## Execute operation with temporary write access, then restore read-only
## This ensures atomic permission elevation and restoration
var oldPerms: set[FilePermission]
try:
# Save current permissions
oldPerms = getFilePermissions(pm.casPath)
# Enable write (755)
let setWritableResult = pm.setWritable()
if not setWritableResult.isOk:
return setWritableResult
# Perform operation
operation()
# Restore read-only (555)
let setReadOnlyResult = pm.setReadOnly()
if not setReadOnlyResult.isOk:
return setReadOnlyResult
return ok(NimPakError)
except Exception as e:
# Ensure permissions restored even on error
try:
setFilePermissions(pm.casPath, oldPerms)
pm.logOperation("RESTORE_PERMS_AFTER_ERROR", pm.casPath)
except:
discard # Best effort to restore
return VoidResult[NimPakError](isOk: false, errValue: NimPakError(
code: UnknownError,
msg: "Write operation failed: " & e.msg
))
proc ensureReadOnly*(pm: ProtectionManager): VoidResult[NimPakError] =
## Ensure CAS directory is in read-only state
## This should be called during initialization
return pm.setReadOnly()
proc verifyReadOnly*(pm: ProtectionManager): bool =
## Verify that CAS directory is in read-only state
try:
let perms = getFilePermissions(pm.casPath)
# Check that write permission is not set for user
return fpUserWrite notin perms
except:
return false
# Merkle Integrity Verification
# This is the PRIMARY security mechanism (not chmod)
proc logSecurityEvent*(pm: ProtectionManager, event: SecurityEvent) =
## Log security events (integrity violations, tampering attempts, etc.)
try:
let timestamp = event.timestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'")
let logEntry = "[" & timestamp & "] SECURITY_EVENT type=" & event.eventType &
" severity=" & event.severity & " hash=" & event.hash &
" details=" & event.details & "\n"
let logFile = open(pm.auditLog, fmAppend)
logFile.write(logEntry)
logFile.close()
except IOError:
# If we can't write to audit log, at least try stderr
stderr.writeLine("SECURITY EVENT: " & event.eventType & " - " & event.details)
proc verifyChunkIntegrity*(pm: ProtectionManager, data: seq[byte], expectedHash: string): VoidResult[NimPakError] =
## Verify chunk integrity by recalculating hash
## This is the PRIMARY security mechanism - always verify before use
try:
let calculatedHash = "xxh3-" & $XXH3_128bits(cast[string](data))
if calculatedHash != expectedHash:
# CRITICAL: Hash mismatch detected!
let event = SecurityEvent(
timestamp: now(),
eventType: "INTEGRITY_VIOLATION",
hash: expectedHash,
details: "Hash mismatch: expected=" & expectedHash & " calculated=" & calculatedHash,
severity: "critical"
)
pm.logSecurityEvent(event)
return VoidResult[NimPakError](isOk: false, errValue: NimPakError(
code: UnknownError,
context: "Object Hash: " & expectedHash,
msg: "Chunk integrity violation detected! Expected: " & expectedHash &
", Got: " & calculatedHash & ". This chunk may be corrupted or tampered with."
))
# Hash matches - integrity verified
let event = SecurityEvent(
timestamp: now(),
eventType: "INTEGRITY_VERIFIED",
hash: expectedHash,
details: "Chunk integrity verified successfully",
severity: "info"
)
pm.logSecurityEvent(event)
return ok(NimPakError)
except Exception as e:
return VoidResult[NimPakError](isOk: false, errValue: NimPakError(
code: UnknownError,
msg: "Failed to verify chunk integrity: " & e.msg,
context: "Object Hash: " & expectedHash
))
proc verifyChunkIntegrityFromFile*(pm: ProtectionManager, filePath: string, expectedHash: string): VoidResult[NimPakError] =
## Verify chunk integrity by reading file and checking hash
try:
let data = readFile(filePath)
let byteData = data.toOpenArrayByte(0, data.len - 1).toSeq()
return pm.verifyChunkIntegrity(byteData, expectedHash)
except IOError as e:
return VoidResult[NimPakError](isOk: false, errValue: NimPakError(
code: FileReadError,
msg: "Failed to read chunk file for verification: " & e.msg,
context: "Object Hash: " & expectedHash
))
proc scanCASIntegrity*(pm: ProtectionManager, casPath: string): tuple[verified: int, corrupted: seq[string]] =
## Scan entire CAS directory and verify integrity of all chunks
## Returns count of verified chunks and list of corrupted chunk hashes
result.verified = 0
result.corrupted = @[]
try:
let chunksDir = casPath / "chunks"
if not dirExists(chunksDir):
return
for entry in walkDirRec(chunksDir):
if fileExists(entry):
# Extract hash from filename
let filename = extractFilename(entry)
# Assume format: xxh3-<hash>.zst or just <hash>
var hash = filename
if not hash.startsWith("xxh3-"):
hash = "xxh3-" & hash.replace(".zst", "")
# Verify integrity
let verifyResult = pm.verifyChunkIntegrityFromFile(entry, hash)
if verifyResult.isOk:
result.verified.inc
else:
result.corrupted.add(hash)
# Log corruption
let event = SecurityEvent(
timestamp: now(),
eventType: "CORRUPTION_DETECTED",
hash: hash,
details: "Chunk failed integrity check during scan",
severity: "critical"
)
pm.logSecurityEvent(event)
except Exception as e:
stderr.writeLine("Error during CAS integrity scan: " & e.msg)