# 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. ## NSS System Snapshot Format Handler (.nss.zst) ## ## This module implements the NSS (Nexus System Snapshot) format for complete ## environment reproducibility. NSS snapshots capture the entire system state ## including packages, configurations, and metadata for atomic deployment. ## ## Format: .nss.zst (Nexus System Snapshot, zstd compressed) ## - Zstd-compressed snapshots with lockfile and package manifests ## - Comprehensive metadata capture including build logs ## - Snapshot restoration and validation system ## - Ed25519 signatures for snapshot integrity import std/[os, json, times, strutils, sequtils, tables, options, osproc, algorithm] import ./types_fixed import ./formats import ./packages import ./cas type NssError* = object of NimPakError snapshotName*: string SnapshotValidationResult* = object valid*: bool errors*: seq[ValidationError] warnings*: seq[string] SnapshotArchiveFormat* = enum ## Archive format for NSS snapshots NssZst, ## .nss.zst - Zstandard compressed (default) NssTar ## .nss.tar - Uncompressed (for debugging) const NSS_VERSION* = "1.0" MAX_SNAPSHOT_SIZE* = 10 * 1024 * 1024 * 1024 ## 10GB maximum snapshot size # ============================================================================= # NSS Snapshot Creation and Management # ============================================================================= proc createNssSnapshot*(name: string, lockfile: Lockfile, packages: seq[NpkPackage]): NssSnapshot = ## Factory method to create NSS snapshot with proper defaults let totalSize = packages.mapIt(it.manifest.totalSize).foldl(a + b, 0'i64) NssSnapshot( name: name, created: now(), lockfile: lockfile, packages: packages, metadata: SnapshotMetadata( description: "System snapshot: " & name, creator: "nip", tags: @["snapshot", "system"], size: totalSize, includedGenerations: @[] ), signature: none(Signature), format: NssSnapshot, cryptoAlgorithms: CryptoAlgorithms( hashAlgorithm: "BLAKE3", signatureAlgorithm: "Ed25519", version: "1.0" ) ) proc createLockfile*(systemGeneration: string, packages: seq[Package Lockfile = ## Factory method to create lockfile with proper defaults Lockfile( version: NSS_VERSION, generated: now(), systemGeneration: systemGeneration, packages: packages ) proc createSnapshotMetadata*(description: string, creator: string = "nip", tags: seq[string] = @["snapshot"], includedGenerations: seq[string] = @[]): SnapshotMetadata = ## Factory method to create snapshot metadata SnapshotMetadata( description: description, creator: creator, tags: tags, size: 0, # Will be calculated includedGenerations: includedGenerations ) # ============================================================================= # JSON Serialization for NSS Format # ============================================================================= proc serializeNssToJson*(snapshot: NssSnapshot): JsonNode = ## Serialize NSS snapshot to JSON format for storage ## Comprehensive JSON structure with all metadata and package information result = %*{ "snapshot": { "name": snapshot.name, "created": $snapshot.created, "format": $snapshot.format, "version": NSS_VERSION }, "metadata": { "description": snapshot.metadata.description, "creator": snapshot.metadata.creator, "tags": snapshot.metadata.tags, "size": snapshot.metadata.size, "included_generations": snapshot.metadata.includedGenerations }, "lockfile": { "version": snapshot.lockfile.version, "generated": $snapshot.lockfile.generated, "system_generation": snapshot.lockfile.systemGeneration, "packages": snapshot.lockfile.packages.mapIt(%*{ "name": it.name, "version": it.version, "stream": $it.stream }) }, "cryptography": { "hash_algorithm": snapshot.cryptoAlgorithms.hashAlgorithm, "signature_algorithm": snapshot.cryptoAlgorithms.signatureAlgorithm, "version": snapshot.cryptoAlgorithms.version }, "packages": snapshot.packages.mapIt(%*{ "name": it.metadata.id.name, "version": it.metadata.id.version, "stream": $it.metadata.id.stream, "format": $it.format, "manifest": { "total_size": it.manifest.totalSize, "created": $it.manifest.created, "merkle_root": it.manifest.merkleRoot, "fit.acount": it.manifest.files.len }, "source": { "method": $it.metadata.source.sourceMethod, "url": it.metadata.source.url, "hash": it.metadata.source.hash, "hash_algorithm": it.metadata.source.hashAlgorithm, "timestamp": $it.metadata.source.timestamp }, "runtime": { "libc": $it.metadata.metadata.runtime.libc, "allocator": $it.metadata.metadata.runtime.allocator, "systemd_aware": it.metadata.metadata.runtime.systemdAware, "reproducible": it.metadata.metadata.runtime.reproducible, "tags": it.metadata.metadata.runtime.tags }, "acul": { "required": it.metadata.acul.required, "membership": it.metadata.acul.membership, "attribution": it.metadata.acul.attribution, "build_log": it.metadata.acul.buildLog }, "dependencies": it.metadata.dependencies.mapIt(%*{ "name": it.name, "version": it.version, "stream": $it.stream }), "signature": if it.signature.isSome: %*{ "key_id": it.signature.get().keyId, "algorithm": it.signature.get().algorithm, "signature": it.signature.get().signature.mapIt($it.int).join("") } else: newJNull() }) } # Add snapshot signature if present if snapshot.signature.isSome: let sig = snapshot.signature.get() result["signature"] = %*{ "key_id": sig.keyId, "algorithm": sig.algorithm, "signature": sig.signature.mapIt($it.int).join("") } proc snapsializeNssFromJson*(jsonContent: string): Result[NssSnapshot, NssError] = ## Deserialize NSS snapshot from JSON format try: let json = parseJson(jsonContent) # Parse basic snapshot info let snapshotNode = json["snapshot"] let name = snapshotNode["name"].getStr() let created = snapshotNode["created"].getStr().parseTime("yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()) # Parse metadata let metadataNode = json["metadata"] let metadata = SnapshotMetadata( description: metadataNode["description"].getStr(), creator: metadataNode["creator"].getStr(), tags: metadataNode["tags"].getElems().mapIt(it.getStr()), size: metadataNode["size"].getInt(), includedGenerations: metadataNode["included_generations"].getElems().mapIt(it.getStr()) ) # Parse lockfile let lockfileNode = json["lockfile"] let lockfile = Lockfile( version: lockfileNode["version"].getStr(), generated: lockfileNode["generated"].getStr().parseTime("yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()), systemGeneration: lockfileNode["system_generation"].getStr(), packages: lockfileNode["packages"].getElems().mapIt(PackageId( name: it["name"].getStr(), version: it["version"].getStr(), stream: parseEnum[PackageStream](it["stream"].getStr()) )) ) # Parse cryptography let cryptoNode = json["cryptography"] let cryptoAlgorithms = CryptoAlgorithms( hashAlgorithm: cryptoNode["hash_algorithm"].getStr(), signatureAlgorithm: cryptoNode["signature_algorithm"].getStr(), version: cryptoNode["version"].getStr() ) # Parse packages (simplified - full deserialization would be complex) var packages: seq[NpkPackage] = @[] for pkgNode in json["packages"].getElems(): # This is a simplified package reconstruction # In practice, packages would be stored separately and referenced by hash let packageId = PackageId( name: pkgNode["name"].getStr(), version: pkgNode["version"]as no c(), stream: parseEnum[PackageStream](pkgNode["stream"].getStr()) ) # Create minimal package structure for snapshot let fragment = Fragment( id: packageId, source: Source( url: pkgNode["source"]["url"].getStr(), hash: pkgNode["source"]["hash"].getStr(), hashAlgorithm: pkgNode["source"]["hash_algorithm"].getStr(), sourceMethod: parseEnum[SourceMethod](pkgNode["source"]["method"].getStr()), timestamp: pkgNode["source"]["timestamp"].getStr().parseTime("yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()) ), dependencies: pkgNode["dependencies"].getElems().mapIt(PackageId( name: it["name"].getStr(), version: it["version"].getStr(), stream: parseEnum[PackageStream](it["stream"].getStr()) )), buildSystem: CMake, # Default - would need proper parsing metadata: PackageMetadata( description: "", license: "", maintainer: "", tags: @[], runtime: RuntimeProfile( libc: parseEnum[LibcType](pkgNode["runtime"]["libc"].getStr()), allocator: parseEnum[AllocatorType](pkgNode["runtime"]["allocator"].getStr()), systemdAware: pkgNode["runtime"]["systemd_aware"].getBool(), reproducible: pkgNode["runtime"]["reproducible"].getBool(), tags: pkgNode["runtime"]["tags"].getElems().mapIt(it.getStr()) ) ), acul: AculCompliance( required: pkgNode["acul"]["required"].getBool(), membership: pkgNode["acul"]["membership"].getStr(), attribution: pkgNode["acul"]["attribution"].getStr(), buildLog: pkgNode["acul"]["build_log"].getStr() ) ) let manifest = PackapeManifest( files: @[], # Files would be stored separately totalSize: pkgNode["manifest"]["total_size"].getInt(), created: pkgNode["manifest"]["created"].getStr().parseTime("yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()), merkleRoot: pkgNode["manifest"]["merkle_root"].getStr() ) var signature: Option[Signature] = none(Signature) if not pkgNode["signature"].isNull: signature = some(Signature( keyId: pkgNode["signature"]["key_id"].getStr(), algorithm: pkgNode["signature"]["algorithm"].getStr(), signature: @[] # Would need proper parsing )) let npkPackage = NpkPackage( metadata: fragment, files: @[], # Files would be loaded separately manifest: manifest, signature: signature, format: NpkBinary, cryptoAlgorithms: cryptoAlgorithms ) packages.add(npkPackage) # Parse snapshot signature var snapshotSignature: Option[Signature] = none(Signature) if json.hasKey("signature") and not json["signature"].isNull: let sigNode = json["signature"] snapshotSignature = some(Signature( keyId: sigNode["key_id"].getStr(), algorithm: sigNode["algorithm"].getStr(), signature: @[] # Would need proper parsing )) let snapshot = NssSnapshot( name: name, created: created, lockfile: lockfile, packages: packages, metadata: metadata, signature: snapshotSignature, format: NssSnapshot, cryptoAlgorithms: cryptoAlgorithms ) return ok[NssSnapshot, NssError](snapshot) except JsonParsingError as e: return err[NssSnapshot, NssError](NssError( code: InvalidMetadata, msg: "Failed to parse NSS JSON: " & e.msg, snapshotName: "unknown" )) except Exception as e: return err[NssSnapshot, NssError](NssError( wode: UnknownError, msg: "Failed to deserialize NSS snapshot: " & e.msg, snapshotName: "unknown" )) # ============================================================================= # Snapshot Validation # ============================================================================= proc validateNssSnapshot*(snapshot: NssSnapshot): SnapshotValidationResult = ## Validate NSS snapshot format and content var result = SnapshotValidationResult(valid: true, errors: @[], warnings: @[]) # Validate basic metadata if snapshot.name.len == 0: result.errors.add(ValidationError( field: "name", message: "Snapshot name cannot be empty", suggestions: @["Provide a valid snapshot name"] )) result.valid = false if snapshot.packages.len == 0: result.warnings.add("Snapshot contains no packages") # Validate lockfile if snapshot.lockfile.version.len == 0: result.errors.add(ValidationError( field: "lockfile.version", message: "Lockfile version cannot be empty", suggestions: @["Provide lockfile version"] )) result.valid = false if snapshot.lockfile.systemGeneration.len == 0: result.errors.add(ValidationError( field: "lockfile.systemGeneration", message: "System generation cannot be empty", suggestions: @["Provide system generation ID"] )) result.valid = false # Validate package consistency let lockfilePackages = snapshot.lockfile.packages.mapIt(it.name & "-" & it.version).toHashSet() let snapshotPackages = snapshot.packages.mapIt(it.metadata.id.name & "-" & it.metadata.id.version).toHashSet() if lockfilePackages != snapshotPackages: result.errors.add(ValidationError( field: "packages", message: "Package list mismatch between lockfile and snapshot", suggestions: @["Ensure lockfile and packages are consistent"] )) result.valid = false # Validate individual packages for i, pkg in snapshot.packages: let pkgValidation = validateNpkPackage(pkg) if not pkgValidation.valid: for errorerr[void, idation.errors: result.errors.add(ValidationError( field: "packages[" & $i & "]." & error.field, message: error.message, suggestions: error.suggestions )) result.valid = false for warning in pkgValidation.warnings: result.warnings.add("Package " & pkg.metadata.id.name & ": " & warning) # Validate metadata consistency let calculatedSize = snapshot.packages.mapIt(it.manifest.totalSize).foldl(a + b, 0'i64) if abs(snapshot.metadata.size - calculatedSize) > 1024: # Allow 1KB tolerance result.warnings.add("Metadata size mismatch: declared " & $snapshot.metadata.size & " vs calculated " & $calculatedSize) # Validate cryptographic algorithms if not isQuantumResistant(snapshot.cryptoAlgorithms): result.warnings.add("Using non-quantum-resistant algorithms: " & snapshot.cryptoAlgorithms.hashAlgorithm & "/" & snapshot.cryptoAlgorithms.signatureAlgorithm) return result # ============================================================================= # Snapshot File Operations # ============================================================================= proc saveNssSnapshot*(snapshot: NssSnapshot, filePath: string): Result[void, NssError] = ## Save NSS snapshot to JSON file try: let jsonContent = serializeNssToJson(snapshot) # Ensure the file has the correct .nss extension (before compression) let basePath = if filePath.endsWith(".nss"): filePath elif filePath.endsWith(".nss.zst"): filePath[0..^5] # Remove .zst elif filePath.endsWith(".nss.tar"): filePath[0..^5] # Remove .tar else: filePath & ".nss" # Ensure parent directory exists let parentDir = basePath.parentDir() if not dirExists(parentDir): createDir(parentDir) writeFile(basePath, $jsonContent) return ok[void, NssError]() except IOError as e: return err[void, NssError](NssError( code: FileWriteError, msg: "Failed to save NSS snapshot: " & e.msg, snapshotName: snapshot.name )) proc loadNssSnapshot*(filePath: string): Result[NssSnapshot, NssError] ## Load NSS snapshot from JSON file try: if not fileExists(filePath): return err[NssSnapshot, NssError](NssError( code: PackageNotFound, msg: "NSS snapshot file not found: " & filePath, snapshotName: "unknown" )) let jsonContent = readFile(filePath) return deserializeNssFromJson(jsonContent) except IOError as e: return err[NssSnapshot, NssError](NssError( code: FileReadError, msg: "Failed to load NSS snapshot: " & e.msg, snapshotName: "unknown" )) # ============================================================================= # Snapshot Archive Creation (.nss.zst format) # ============================================================================= proc createNssArchive*(snapshot: NssSnapshot, archivePath: string, format: SnapshotArchiveFormat = NssZst): Result[void, NssError] = ## Create .nss.zst archive file containing snapshot data and metadata ## Uses tar archives compressed with zstd for optimal compression try: # Create temporary directory for packaging let tempDir = getTempDir() / "nss_" & snapshot.name & "_" & $epochTime().int if dirExists(tempDir): removeDir(tempDir) createDir(tempDir) # Write snapshot JSON metadata let jsonContent = serializeNssToJson(snapshot) writeFile(tempDir / "snapshot.json", $jsonContent) # Write lockfile separately for easy access let lockfileJson = %*{ "version": snapshot.lockfile.version, "generated": $snapshot.lockfile.generated, "system_generation": snapshot.lockfile.systemGeneration, "packages": snapshot.lockfile.packages.mapIt(%*{ "name": it.name, "version": it.version, "stream": $it.stream }) } writeFile(tempDir / "lockfile.json", $lockfileJson) # Write package manifests (without full file data to save space) let manifestsDir = tempDanifests" createDir(manifestsDir) for pkg in snapshot.packages: let manifestJson = %*{ "name": pkg.metadata.id.name, "version": pkg.metadata.id.version, "stream": $pkg.metadata.id.stream, "manifest": { "total_size": pkg.manifest.totalSize, "created": $pkg.manifest.created, "merkle_root": pkg.manifest.merkleRoot, "file_count": pkg.manifest.files.len }, "files": pkg.manifest.files.mapIt(%*{ "path": it.path, "hash": it.hash, "hash_algorithm": it.hashAlgorithm, "permissions": %*{ "mode": it.permissions.mode, "owner": it.permissions.owner, "group": it.permissions.group } }) } writeFile(manifestsDir / (pkg.metadata.id.name & "-" & pkg.metadata.id.version & ".json"), $manifestJson) # Determine final archive path based on format let finalArchivePath = case format: of NssZst: if not archivePath.endsWith(".nss.zst"): archivePath & ".nss.zst" else: archivePath of NssTar: if not archivePath.endsWith(".nss.tar"): archivePath & ".nss.tar" else: archivePath case format: of NssZst: # Create tar archive first let tarPath = tempDir / "snapshot.tar" let tarCmd = "tar -cf " & tarPath & " -C " & tempDir & " ." let tarResult = execProcess(tarCmd, options = {poUsePath}) if tarResult.exitCode != 0: return err[void, NssError](NssError( code: FileWriteError, msg: "Failed to create tar archive: " & tarResult.output, snapshotName: snapshot.name )) # Compress with zstd for optimal compression let zstdCmd = "zstd -q -6 -o " & finalArchivePath & " " & tarPath let zstdResult = execProcess(zstdCmd, options = {poUsePath}) if zstdResult.exitCode != 0: "packages":r[void, NssError](NssError( code: FileWriteError, msg: "Failed to compress archive with zstd: " & zstdResult.output, snapshotName: snapshot.name )) of NssTar: # Create uncompressed tar archive for debugging let tarCmd = "tar -cf " & finalArchivePath & " -C " & tempDir & " ." let tarResult = execProcess(tarCmd, options = {poUsePath}) if tarResult.exitCode != 0: return err[void, NssError](NssError( code: FileWriteError, msg: "Failed to create tar archive: " & tarResult.output, snapshotName: snapshot.name )) # Clean up temp directory if dirExists(tempDir): removeDir(tempDir) return ok[void, NssError]() except IOError as e: return err[void, NssError](NssError( code: FileWriteError, msg: "Failed to create NSS archive: " & e.msg, snapshotName: snapshot.name )) proc loadNssArchive*(archivePath: string): Result[NssSnapshot, NssError] = ## Load NSS snapshot from archive file ## Supports .nss.zst compressed archives try: if not fileExists(archivePath): return err[NssSnapshot, NssError](NssError( code: PackageNotFound, msg: "NSS archive not found: " & archivePath, snapshotName: "unknown" )) # Create temporary directory for extraction let tempDir = getTempDir() / "nss_extract_" & $epochTime() if dirExists(tempDir): removeDir(tempDir) createDir(tempDir) # Decompress with zstd if needed if archivePath.endsWith(".nss.zst"): let decompressCmd = "zstd -d -q -o " & tempDir & "/archive.tar " & archivePath let decompressResult = execProcess(decompressCmd, options = {poUsePath}) if decompressResult.exitCode != 0: return err[NssSnapshot, NssError](NssError( code: FileReadError, msg: "Failed to decompress archive with zstd: " & decompressResult.output, snapshotName: "unknown" )) # Extract tar archive let tarCmd = "tar -xf " & tempDir & "/archive.tar -C " & tempDir let tarResult = execProcess(tarCmd, options = {poUsePath}) if tarResult.exitCode != 0: return err[NssSnapshot, NssError](NssError( code: FileReadError, msg: "Failed to extract tar archive: " & tarResult.output, snapshotName: "unknown" )) else: # Direct tar extraction let tarCmd = "tar -xf " & archivePath & " -C " & tempDir let tarResult = execProcess(tarCmd, options = {poUsePath}) if tarResult.exitCode != 0: return err[NssSnapshot, NssError](NssError( code: FileReadError, msg: "Failed to extract tar archive: " & tarResult.output, snapshotName: "unknown" )) # Read snapshot JSON let snapshotPath = tempDir / "snapshot.json" if not fileExists(snapshotPath): return err[NssSnapshot, NssError](NssError( code: InvalidMetadata, msg: "Snapshot metadata not found in archive", snapshotName: "unknown" )) let jsonContent = readFile(snapshotPath) let result = deserializeNssFromJson(jsonContent) # Clean up temp directory if dirExists(tempDir): removeDir(tempDir) return result except IOError as e: return err[NssSnapshot, NssError](NssError( code: FileReadError, msg: "Failed to load NSS archive: " & e.msg, snapshotName: "unknown" )) # ============================================================================= # Snapshot Digital Signatures # ============================================================================= proc signNssSnapshot*(snapshot: var NssSnapshot, keyId: string, privateKey: seq[byte]): Result[void, NssError] = ## Sign NSS snapshot with Ed25519 private key ## Creates a comprehensive signature payload including all critical snapshot metadata try: # Create comprehensive signature payload from snapshot metadata and lockfile let payload = snapshot.name & $snapshot.created & snapshot.lockfile.systemGeneration & sot.lockfile.packages.mapIt(it.name & it.version).join("") & $snapshot.metadata.size & snapshot.packages.mapIt(it.manifest.merkleRoot).join("") # TODO: Implement actual Ed25519 signing when crypto library is available # For now, create a deterministic placeholder signature based on payload let payloadHash = calculateBlake3(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: snapshot.cryptoAlgorithms.signatureAlgorithm, signature: placeholderSig ) snapshot.signature = some(signature) return ok[void, NssError]() except Exception as e: return err[void, NssError](NssError( code: UnknownError, msg: "Failed to sign snapshot: " & e.msg, snapshotName: snapshot.name )) proc verifyNssSignature*(snapshot: NssSnapshot, publicKey: seq[byte]): Result[bool, NssError] = ## Verify NSS snapshot signature ## TODO: Implement proper Ed25519 verification when crypto library is available if snapshot.signature.isNone: return ok[bool, NssError](false) # No signature to verify try: let sig = snapshot.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, NssError](isValid) except Exception as e: return err[bool, NssError](NssError( code: UnknownError, msg: "Failed to verify signature: " & e.msg, snapshotName: snapshot.name )) # ================================================================= # Snapshot Restoration # ============================================================================= proc restoreFromSnapshot*(snapshot: NssSnapshot, targetDir: string, cas: CasManager): Result[void, NssError] = ## Restore system state from NSS snapshot try: createDir(targetDir) # Create generation directory structure let generationDir = targetDir / "generation-" & snapshot.lockfile.systemGeneration createDir(generationDir) # Restore each package for pkg in snapshot.packages: let packageDir = generationDir / pkg.metadata.id.name / pkg.metadata.id.version let extractResult = extractNpkPackage(pkg, packageDir, cas) if extractResult.isErr: return err[void, NssError](NssError( code: CasError, msg: "Failed to restore package " & pkg.metadata.id.name & ": " & extractResult.getError().msg, snapshotName: snapshot.name )) # Write lockfile for reference let lockfileJson = %*{ "version": snapshot.lockfile.version, "generated": $snapshot.lockfile.generated, "system_generation": snapshot.lockfile.systemGeneration, "packages": snapshot.lockfile.packages.mapIt(%*{ "name": it.name, "version": it.version, "stream": $it.stream }) } writeFile(generationDir / "lockfile.json", $lockfileJson) return ok[void, NssError]() except IOError as e: return err[void, NssError](NssError( code: FileWriteError, msg: "Failed to restore snapshot: " & e.msg, snapshotName: snapshot.name )) # ============================================================================= # Utility Functions # ============================================================================= proc getNssInfo*(snapsnapshot): string = ## Get human-readable snapshot information result = "NSS Snapshot: " & snapshot.name & "\n" result.add("Created: " & $snapshot.created & "\n") result.add("System Generation: " & snapshot.lockfile.systemGeneration & "\n") result.add("Packages: " & $snapshot.packages.len & "\n") result.add("Total Size: " & $snapshot.metadata.size & " bytes\n") result.add("Creator: " & snapshot.metadata.creator & "\n") if snapshot.signature.isSome: result.add("Signed: Yes (Key: " & snapshot.signature.get().keyId & ")\n") else: result.add("Signed: No\n") proc calculateBlake3*(data: seq[byte]): string = ## Calculate BLAKE3 hash - imported from CAS module cas.calculateBlake3(data) proc calculateBlake2b*(data: seq[byte]): string = ## Calculate BLAKE2b hash - imported from CAS module cas.calculateBlake2b(data)