# 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-.zst or just 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)