595 lines
22 KiB
Nim
595 lines
22 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.
|
|
|
|
## NOF Overlay Fragment Format Handler (.nof)
|
|
##
|
|
## This module implements the NOF (Nexus Overlay Fragment) format for declarative
|
|
## system modifications. NOF overlays provide immutable system configuration
|
|
## changes that can be applied atomically to system generations.
|
|
##
|
|
## Format: .nof (Nexus Overlay Fragment, plain text KDL)
|
|
## - Plain-text KDL format for immutable system overlays
|
|
## - Overlay application and validation system
|
|
## - Ed25519 signature support for overlay integrity
|
|
## - Overlay conflict detection and resolution
|
|
|
|
import std/[os, json, times, strutils, sequtils, tables, options, algorithm]
|
|
import ./types_fixed
|
|
import ./formats
|
|
import ./cas
|
|
|
|
type
|
|
NofError* = object of NimPakError
|
|
overlayName*: string
|
|
|
|
OverlayValidationResult* = object
|
|
valid*: bool
|
|
errors*: seq[ValidationError]
|
|
warnings*: seq[string]
|
|
|
|
OverlayOperation* = enum
|
|
## Types of overlay operations
|
|
AddFile, ## Add new file
|
|
ModifyFile, ## Modify existing file
|
|
RemoveFile, ## Remove file
|
|
AddSymlink, ## Add symbolic link
|
|
RemoveSymlink,## Remove symbolic link
|
|
SetPermissions,## Set file permissions
|
|
AddPackage, ## Add package to system
|
|
RemovePackage,## Remove package from system
|
|
SetConfig ## Set configuration value
|
|
|
|
OverlayModification* = object
|
|
## Individual overlay modification
|
|
operation*: OverlayOperation
|
|
target*: string ## Target path or package name
|
|
source*: Option[string] ## Source path (for copies/links)
|
|
content*: Option[string] ## File content (for inline content)
|
|
permissions*: Option[FilePermissions] ## File permissions
|
|
metadata*: JsonNode ## Additional operation-specific metadata
|
|
|
|
const
|
|
NOF_VERSION* = "1.0"
|
|
MAX_OVERLAY_SIZE* = 100 * 1024 * 1024 ## 100MB maximum overlay size
|
|
|
|
# =============================================================================
|
|
# NOF Overlay Creation and Management
|
|
# =============================================================================
|
|
|
|
proc createNofOverlay*(name: string, description: string,
|
|
overlayConfig: OverlayConfig): NofOverlay =
|
|
## Factory method to create NOF overlay with proper defaults
|
|
NofOverlay(
|
|
name: name,
|
|
description: description,
|
|
overlayConfOverlayrlayConfig,
|
|
signature: none(Signature),
|
|
format: NofOverlay,
|
|
cryptoAlgorithms: CryptoAlgorithms(
|
|
hashAlgorithm: "BLAKE2b",
|
|
signatureAlgorithm: "Ed25519",
|
|
version: "1.0"
|
|
)
|
|
)
|
|
|
|
proc createOverlayConfig*(name: string, description: string,
|
|
targetGeneration: Option[string] = none(string),
|
|
modifications: JsonNode = newJObject()): OverlayConfig =
|
|
## Factory method to create overlay configuration
|
|
OverlayConfig(
|
|
name: name,
|
|
description: description,
|
|
targetGeneration: targetGeneration,
|
|
modifications: modifications
|
|
)
|
|
|
|
proc createOverlayModification*(operation: OverlayOperation, target: string,
|
|
source: Option[string] = none(string),
|
|
content: Option[string] = none(string),
|
|
permissions: Option[FilePermissions] = none(FilePermissions),
|
|
metadata: JsonNode = newJObject()): OverlayModification =
|
|
## Factory method to create overlay modification
|
|
OverlayModification(
|
|
operation: operation,
|
|
target: target,
|
|
source: source,
|
|
content: content,
|
|
permissions: permissions,
|
|
metadata: metadata
|
|
)
|
|
|
|
# =============================================================================
|
|
# KDL Serialization for NOF Format
|
|
# =============================================================================
|
|
|
|
proc escapeKdlString(s: string): string =
|
|
## Escape special characters in KDL strings
|
|
result = "\""
|
|
for c in s:
|
|
case c:
|
|
of '"': result.add("\\\"")
|
|
of '\\': result.add("\\\\")
|
|
of '\n': result.add("\\n")
|
|
of '\r': result.add("\\r")
|
|
of '\t': result.add("\\t")
|
|
else: result.add(c)
|
|
result.add("\"")
|
|
|
|
proc formatKdlBoolean(b: bool): string =
|
|
## Format boolean for KDL
|
|
if b: "true" else: "false"
|
|
|
|
proc formatKdlArray(items: seq[string]): string =
|
|
## Format string array for KDL
|
|
if items.len == 0:
|
|
return ""
|
|
result = ""
|
|
for i, item in items:
|
|
if i > 0: result.add(" ")
|
|
result.add(escapeKdlString(item))
|
|
|
|
proc toHex(b: byte): string =
|
|
## Convert byte to hex string
|
|
const hexChars = "0123456789abcdef"
|
|
result = $hexChars[b shr 4] & $hexChars[b and 0x0F]
|
|
|
|
proc serializeModificationsToKdl(modifications: JsonNode): string =
|
|
## Serialize modifications JSON to KDL format
|
|
## This is a simplified conversion - full KDL library would be better
|
|
result = ""
|
|
|
|
if modifications.kind == JObject:
|
|
for key, value in modifications:
|
|
case value.kind:
|
|
of JString:
|
|
result.add(" " & escapeKdlString(key) & " " & escapeKdlString(value.getStr()) & "\n")
|
|
of JInt:
|
|
result.add(" " & escapeKdlString(key) & " " & $value.getInt() & "\n")
|
|
of JBool:
|
|
result.add(" " & escapeKdlString(key) & " " & formatKdlBoolean(value.getBool()) & "\n")
|
|
of JArray:
|
|
let items = value.getElems().mapIt(it.getStr())
|
|
result.add(" " & escapeKdlString(key) & " " & formatKdlArray(items) & "\n")
|
|
of JObject:
|
|
result.add(" " & escapeKdlString(key) & " {\n")
|
|
for subKey, subValue in value:
|
|
result.add(" " & escapeKdlString(subKey) & " " & escapeKdlString(subValue.getStr()) & "\n")
|
|
result.add(" }\n")
|
|
else:
|
|
result.add(" " & escapeKdlString(key) & " " & escapeKdlString($value) & "\n")
|
|
|
|
proc serializeNofToKdl*(overlay: NofOverlay): string =
|
|
## Serialize NOF overlay to KDL format with comprehensive metadata
|
|
## Plain-text format optimized for immutable system overlays
|
|
|
|
result = "overlay " & escapeKdlString(overlay.name) & " {\n"
|
|
result.add(" version " & escapeKdlString(NOF_VERSION) & "\n")
|
|
result.add(" format " & escapeKdlString($overlay.format) & "\n")
|
|
result.add(" description " & escapeKdlString(overlay.description) & "\n")
|
|
result.add("\n")
|
|
|
|
# Overlay configuration
|
|
result.add(" config {\n")
|
|
result.add(" name " & escapeKdlString(overlay.overlayConfig.name) & "\n")
|
|
result.add(" description " & escapeKdlString(overlay.overlayConfig.description) & "\n")
|
|
|
|
if overlay.overlayConfig.targetGeneration.isSome:
|
|
result.add(" target-generation " & escapeKdlString(overlay.overlayConfig.targetGeneration.get()) & "\n")
|
|
|
|
result.add(" }\n\n")
|
|
|
|
# Modifications section
|
|
result.add(" modifications {\n")
|
|
result.add(serializeModificationsToKdl(overlay.overlayConfig.modifications))
|
|
result.add(" }\n\n")
|
|
|
|
# Cryptographic integrity and signature
|
|
result.add(" integrity {\n")
|
|
result.add(" algorithm " & escapeKdlString(overlay.cryptoAlgorithms.hashAlgorithm) & "\n")
|
|
result.add(" signature-algorithm " & escapeKdlString(overlay.cryptoAlgorithms.signatureAlgorithm) & "\n")
|
|
result.add(" version " & escapeKdlString(overlay.cryptoAlgorithms.version) & "\n")
|
|
if overlay.signature.isSome:
|
|
let sig = overlay.signature.get()
|
|
result.add(" signature " & escapeKdlString(sig.signature.mapIt(it.toHex()).join("")) & "\n")
|
|
result.add(" key-id " & escapeKdlString(sig.keyId) & "\n")
|
|
result.add(" }\n")
|
|
|
|
result.add("}\n")
|
|
|
|
proc deserializeNofFromKdl*(kdlContent: string): Result[NofOverlay, NofError] =
|
|
## Deserialize NOF overlay from KDL format
|
|
## TODO: Implement proper KDL parsing when kdl library is available
|
|
## For now, return an error indicating this is not yet implemented
|
|
return err[NofOverlay, NofError](NofError(
|
|
code: InvalidMetadata,
|
|
msg: "KDL deserialization not yet implemented - waiting for kdl library",
|
|
overlayName: "unknown"
|
|
))
|
|
|
|
# =============================================================================
|
|
# Overlay Validation
|
|
# =============================================================================
|
|
|
|
proc validateNofOverlay*(overlay: NofOverlay): OverlayValidationResult =
|
|
## Validate NOF overlay format and content
|
|
var result = OverlayValidationResult(valid: true, errors: @[], warnings: @[])
|
|
|
|
# Validate basic metadata
|
|
if overlay.name.len == 0:
|
|
result.errors.add(ValidationError(
|
|
field: "name",
|
|
message: "Overlay name cannot be empty",
|
|
suggestions: @["Provide a valid overlay name"]
|
|
))
|
|
result.valid = false
|
|
|
|
if overlay.description.len == 0:
|
|
result.warnings.add("Overlay has no description")
|
|
|
|
# Validate overlay configuration
|
|
if overlay.overlayConfig.name.len == 0:
|
|
result.errors.add(ValidationError(
|
|
field: "overlayConfig.name",
|
|
message: "Overlay config name cannot be empty",
|
|
suggestions: @["Provide a valid config name"]
|
|
))
|
|
result.valid = false
|
|
|
|
if overlay.overlayConfig.description.len == 0:
|
|
result.warnings.add("Overlay config has no description")
|
|
|
|
# Validate modifications structure
|
|
if overlay.overlayConfig.modifications.kind != JObject:
|
|
result.errors.add(ValidationError(
|
|
field: "overlayConfig.modifications",
|
|
message: "Modifications must be a JSON object",
|
|
suggestions: @["Provide valid modifications object"]
|
|
))
|
|
result.valid = false
|
|
|
|
# Validate target generation if specified
|
|
if overlay.overlayConfig.targetGeneration.isSome:
|
|
let targetGen = overlay.overlayConfig.targetGeneration.get()
|
|
if targetGen.len == 0:
|
|
result.errors.add(ValidationError(
|
|
field: "overlayConfig.targetGeneration",
|
|
message: "Target generation cannot be empty if specified",
|
|
suggestions: @["Provide valid generation ID or remove target"]
|
|
))
|
|
result.valid = false
|
|
|
|
# Validate cryptographic algorithms
|
|
if not isQuantumResistant(overlay.cryptoAlgorithms):
|
|
result.warnings.add("Using non-quantum-resistant algorithms: " &
|
|
overlay.cryptoAlgorithms.hashAlgorithm & "/" &
|
|
overlay.cryptoAlgorithms.signatureAlgorithm)
|
|
|
|
return result
|
|
|
|
proc validateOverlayModification*(modification: OverlayModification): seq[string] =
|
|
## Validate individual overlay modification and return warnings
|
|
var warnings: seq[string] = @[]
|
|
|
|
case modification.operation:
|
|
of AddFile, ModifyFile:
|
|
if modification.content.isNone and modification.source.isNone:
|
|
warnings.add("File operation without content or source")
|
|
if modification.target.len == 0:
|
|
warnings.add("File operation without target path")
|
|
|
|
of RemoveFile, RemoveSymlink:
|
|
if modification.target.len == 0:
|
|
warnings.add("Remove operation without target path")
|
|
|
|
of AddSymlink:
|
|
if modification.source.isNone:
|
|
warnings.add("Symlink operation without source")
|
|
if modification.target.len == 0:
|
|
warnings.add("Symlink operation without target")
|
|
|
|
of SetPermissions:
|
|
if modification.permissions.isNone:
|
|
warnings.add("Permission operation without permissions")
|
|
if modification.target.len == 0:
|
|
warnings.add("Permission operation without target")
|
|
|
|
of AddPackage, RemovePackage:
|
|
if modification.target.len == 0:
|
|
warnings.add("Package operation without package name")
|
|
|
|
of SetConfig:
|
|
if modification.target.len == 0:
|
|
warnings.add("Config operation without config key")
|
|
if modification.content.isNone:
|
|
warnings.add("Config operation without value")
|
|
|
|
return warnings
|
|
|
|
# =============================================================================
|
|
# Overlay File Operations
|
|
# =============================================================================
|
|
|
|
proc saveNofOverlay*(overlay: NofOverlay, filePath: string): Result[void, NofError] =
|
|
## Save NOF overlay to file in KDL format
|
|
try:
|
|
let kdlContent = serializeNofToKdl(overlay)
|
|
|
|
# Ensure the file has the correct .nof extension
|
|
let finalPath = if filePath.endsWith(".nof"): filePath else: filePath & ".nof"
|
|
|
|
# Ensure parent directory exists
|
|
let parentDir = finalPath.parentDir()
|
|
if not dirExists(parentDir):
|
|
createDir(parentDir)
|
|
|
|
writeFile(finalPath, kdlContent)
|
|
return ok[void, NofError]()
|
|
|
|
except IOError as e:
|
|
return err[void, NofError](NofError(
|
|
code: FileWriteError,
|
|
msg: "Failed to save NOF overlay: " & e.msg,
|
|
overlayName: overlay.name
|
|
))
|
|
|
|
proc loadNofOverlay*(filePath: string): Result[NofOverlay, NofError] =
|
|
## Load NOF overlay from file
|
|
try:
|
|
if not fileExists(filePath):
|
|
return err[NofOverlay, NofError](NofError(
|
|
code: PackageNotFound,
|
|
msg: "NOF overlay file not found: " & filePath,
|
|
overlayName: "unknown"
|
|
))
|
|
|
|
let kdlContent = readFile(filePath)
|
|
return deserializeNofFromKdl(kdlContent)
|
|
|
|
except IOError as e:
|
|
return err[NofOverlay, NofError](NofError(
|
|
code: FileReadError,
|
|
msg: "Failed to load NOF overlay: " & e.msg,
|
|
overlayName: "unknown"
|
|
))
|
|
|
|
# =============================================================================
|
|
# Overlay Digital Signatures
|
|
# =============================================================================
|
|
|
|
proc signNofOverlay*(overlay: var NofOverlay, keyId: string, privateKey: seq[byte]): Result[void, NofError] =
|
|
## Sign NOF overlay with Ed25519 private key
|
|
## Creates a comprehensive signature payload including all critical overlay metadata
|
|
try:
|
|
# Create comprehensive signature payload from overlay metadata and modifications
|
|
let payload = overlay.name &
|
|
overlay.description &
|
|
overlay.overlayConfig.name &
|
|
overlay.overlayConfig.description &
|
|
(if overlay.overlayConfig.targetGeneration.isSome: overlay.overlayConfig.targetGeneration.get() else: "") &
|
|
$overlay.overlayConfig.modifications
|
|
|
|
# TODO: Implement actual Ed25519 signing when crypto library is available
|
|
# For now, create a deterministic placeholder signature based on payload
|
|
let payloadHash = calculateBlake2b(payload.toOpenArrayByte(0, payload.len - 1).toSeq())
|
|
let placeholderSig = payloadHash[0..63].toOpenArrayByte(0, 63).toSeq() # 64 bytes like Ed25519
|
|
|
|
let signature = Signature(
|
|
keyId: keyId,
|
|
algorithm: overlay.cryptoAlgorithms.signatureAlgorithm,
|
|
signature: placeholderSig
|
|
)
|
|
|
|
overlay.signature = some(signature)
|
|
return ok[void, NofError]()
|
|
|
|
except Exception as e:
|
|
return err[void, NofError](NofError(
|
|
code: UnknownError,
|
|
msg: "Failed to sign overlay: " & e.msg,
|
|
overlayName: overlay.name
|
|
))
|
|
|
|
proc verifyNofSignature*(overlay: NofOverlay, publicKey: seq[byte]): Result[bool, NofError] =
|
|
## Verify NOF overlay signature
|
|
## TODO: Implement proper Ed25519 verification when crypto library is available
|
|
if overlay.signature.isNone:
|
|
return ok[bool, NofError](false) # No signature to verify
|
|
|
|
try:
|
|
let sig = overlay.signature.get()
|
|
|
|
# TODO: Implement actual Ed25519 verification
|
|
# For now, just check if signature exists and has correct length
|
|
let isValid = sig.signature.len == 64 and sig.keyId.len > 0
|
|
|
|
return ok[bool, NofError](isValid)
|
|
|
|
except Exception as e:
|
|
return err[bool, NofError](NofError(
|
|
code: UnknownError,
|
|
msg: "Failed to verify signature: " & e.msg,
|
|
overlayName: overlay.name
|
|
))
|
|
|
|
# =============================================================================
|
|
# Overlay Application System
|
|
# =============================================================================
|
|
|
|
proc applyOverlay*(overlay: NofOverlay, targetDir: string, dryRun: bool = false): Result[seq[string], NofError] =
|
|
## Apply overlay modifications to target directory
|
|
## Returns list of operations performed
|
|
try:
|
|
var operations: seq[string] = @[]
|
|
|
|
# Parse modifications from JSON
|
|
if overlay.overlayConfig.modifications.kind != JObject:
|
|
return err[seq[string], NofError](NofError(
|
|
code: InvalidMetadata,
|
|
msg: "Invalid modifications format",
|
|
overlayName: overlay.name
|
|
))
|
|
|
|
# Process each modification
|
|
for key, value in overlay.overlayConfig.modifications:
|
|
let operation = "Apply " & key & ": " & $value
|
|
operations.add(operation)
|
|
|
|
if not dryRun:
|
|
# TODO: Implement actual overlay application logic
|
|
# This would involve:
|
|
# - Parsing the modification type and parameters
|
|
# - Applying file operations (create, modify, delete)
|
|
# - Managing symlinks and permissions
|
|
# - Handling package operations
|
|
# - Setting configuration values
|
|
discard
|
|
|
|
return ok[seq[string], NofError](operations)
|
|
|
|
except Exception as e:
|
|
return err[seq[string], NofError](NofError(
|
|
code: UnknownError,
|
|
msg: "Failed to apply overlay: " & e.msg,
|
|
overlayName: overlay.name
|
|
))
|
|
|
|
proc detectOverlayConflicts*(overlays: seq[NofOverlay]): seq[string] =
|
|
## Detect conflicts between multiple overlays
|
|
var conflicts: seq[string] = @[]
|
|
var targetPaths: Table[string, string] = initTable[string, string]()
|
|
|
|
for overlay in overlays:
|
|
if overlay.overlayConfig.modifications.kind == JObject:
|
|
for key, value in overlay.overlayConfig.modifications:
|
|
if key in targetPaths:
|
|
conflicts.add("Conflict: " & key & " modified by both " &
|
|
targetPaths[key] & " and " & overlay.name)
|
|
else:
|
|
targetPaths[key] = overlay.name
|
|
|
|
return conflicts
|
|
|
|
proc resolveOverlayConflicts*(overlays: seq[NofOverlay],
|
|
resolution: Table[string, string]): seq[NofOverlay] =
|
|
## Resolve overlay conflicts using provided resolution strategy
|
|
## Resolution table maps conflict keys to preferred overlay names
|
|
var resolved: seq[NofOverlay] = @[]
|
|
|
|
for overlay in overlays:
|
|
var resolvedOverlay = overlay
|
|
|
|
if overlay.overlayConfig.modifications.kind == JObject:
|
|
var newModifications = newJObject()
|
|
|
|
for key, value in overlay.overlayConfig.modifications:
|
|
if key in resolution:
|
|
if resolution[key] == overlay.name:
|
|
newModifications[key] = value
|
|
else:
|
|
newModifications[key] = value
|
|
|
|
resolvedOverlay.overlayConfig.modifications = newModifications
|
|
|
|
resolved.add(resolvedOverlay)
|
|
|
|
return resolved
|
|
|
|
# =============================================================================
|
|
# Overlay Templates and Presets
|
|
# =============================================================================
|
|
|
|
proc createFileOverlay*(name: string, description: string,
|
|
filePath: string, content: string,
|
|
permissions: Option[FilePermissions] = none(FilePermissions)): NofOverlay =
|
|
## Create overlay for adding/modifying a file
|
|
let modifications = %*{
|
|
"files": {
|
|
filePath: {
|
|
"operation": "add_file",
|
|
"content": content,
|
|
"permissions": if permissions.isSome: %*{
|
|
"mode": permissions.get().mode,
|
|
"owner": permissions.get().owner,
|
|
"group": permissions.get().group
|
|
} else: newJNull()
|
|
}
|
|
}
|
|
}
|
|
|
|
let config = createOverlayConfig(name, description, modifications = modifications)
|
|
return createNofOverlay(name, description, config)
|
|
|
|
proc createPackageOverlay*(name: string, description: string,
|
|
packageName: string, packageVersion: string,
|
|
operation: string = "add"): NofOverlay =
|
|
## Create overlay for adding/removing a package
|
|
let modifications = %*{
|
|
"packages": {
|
|
packageName: {
|
|
"operation": operation,
|
|
"version": packageVersion
|
|
}
|
|
}
|
|
}
|
|
|
|
let config = createOverlayConfig(name, description, modifications = modifications)
|
|
return createNofOverlay(name, description, config)
|
|
|
|
proc createConfigOverlay*(name: string, description: string,
|
|
configKey: string, configValue: string): NofOverlay =
|
|
## Create overlay for setting configuration values
|
|
let modifications = %*{
|
|
"config": {
|
|
configKey: {
|
|
"operation": "set_config",
|
|
"value": configValue
|
|
}
|
|
}
|
|
}
|
|
|
|
let config = createOverlayConfig(name, description, modifications = modifications)
|
|
return createNofOverlay(name, description, config)
|
|
|
|
proc createSymlinkOverlay*(name: string, description: string,
|
|
linkPath: string, targetPath: string): NofOverlay =
|
|
## Create overlay for adding symbolic links
|
|
let modifications = %*{
|
|
"symlinks": {
|
|
linkPath: {
|
|
"operation": "add_symlink",
|
|
"target": targetPath
|
|
}
|
|
}
|
|
}
|
|
|
|
let config = createOverlayConfig(name, description, modifications = modifications)
|
|
return createNofOverlay(name, description, config)
|
|
|
|
# =============================================================================
|
|
# Utility Functions
|
|
# =============================================================================
|
|
|
|
proc getNofInfo*(overlay: NofOverlay): string =
|
|
## Get human-readable overlay information
|
|
result = "NOF Overlay: " & overlay.name & "\n"
|
|
result.add("Description: " & overlay.description & "\n")
|
|
result.add("Config: " & overlay.overlayConfig.name & "\n")
|
|
if overlay.overlayConfig.targetGeneration.isSome:
|
|
result.add("Target Generation: " & overlay.overlayConfig.targetGeneration.get() & "\n")
|
|
result.add("Modifications: " & $overlay.overlayConfig.modifications.len & " items\n")
|
|
if overlay.signature.isSome:
|
|
result.add("Signed: Yes (Key: " & overlay.signature.get().keyId & ")\n")
|
|
else:
|
|
result.add("Signed: No\n")
|
|
|
|
proc calculateBlake2b*(data: seq[byte]): string =
|
|
## Calculate BLAKE2b hash - imported from CAS module
|
|
cas.calculateBlake2b(data)
|
|
|
|
proc calculateBlake3*(data: seq[byte]): string =
|
|
## Calculate BLAKE3 hash - imported from CAS module
|
|
cas.calculateBlake3(data) |