## TLS/SSL Wrapper — encrypted sockets using OpenSSL (Nim stdlib) when not defined(ssl): {.error: "BaraDB requires SSL support. Compile with -d:ssl".} import std/os import std/osproc import std/strutils import std/net import std/asyncnet type TLSConfig* = object certFile*: string keyFile*: string caFile*: string verifyPeer*: bool TLSContext* = ref object sslCtx*: SslContext config*: TLSConfig proc newTLSConfig*(certFile: string, keyFile: string, caFile: string = "", verifyPeer: bool = false): TLSConfig = TLSConfig( certFile: certFile, keyFile: keyFile, caFile: caFile, verifyPeer: verifyPeer, ) proc newTLSContext*(config: TLSConfig): TLSContext = result = TLSContext(config: config) if fileExists(config.certFile) and fileExists(config.keyFile): result.sslCtx = newContext( certFile = config.certFile, keyFile = config.keyFile, verifyMode = if config.verifyPeer: CVerifyPeer else: CVerifyNone, ) else: raise newException(IOError, "TLS certificate or key file not found: " & config.certFile & ", " & config.keyFile) proc wrapClient*(tls: TLSContext, socket: AsyncSocket) {.inline.} = if tls.sslCtx != nil: tls.sslCtx.wrapSocket(socket) proc wrapServer*(tls: TLSContext, socket: AsyncSocket) {.inline.} = if tls.sslCtx != nil: tls.sslCtx.wrapConnectedSocket(socket, handshakeAsServer) proc close*(tls: TLSContext) = if tls.sslCtx != nil: tls.sslCtx.destroyContext() # TLS Certificate management type CertInfo* = object subject*: string issuer*: string notBefore*: string notAfter*: string fingerprint*: string keySize*: int isSelfSigned*: bool proc parseCertInfo*(certPath: string): CertInfo = result = CertInfo() if not fileExists(certPath): return let content = readFile(certPath) result.subject = "Unknown" result.issuer = "Unknown" result.fingerprint = "" for line in content.splitLines(): if line.startsWith("Subject:"): result.subject = line[8..^1].strip() elif line.startsWith("Issuer:"): result.issuer = line[7..^1].strip() result.isSelfSigned = result.subject == result.issuer proc generateSelfSignedCert*(outputDir: string, commonName: string = "localhost"): (string, string) = let certPath = outputDir / (commonName & ".crt") let keyPath = outputDir / (commonName & ".key") createDir(outputDir) let cmd = "openssl req -x509 -newkey rsa:2048 -keyout " & quoteShell(keyPath) & " -out " & quoteShell(certPath) & " -days 365 -nodes -subj " & quoteShell("/CN=" & commonName) & " 2>/dev/null" if execShellCmd(cmd) == 0 and fileExists(certPath): return (certPath, keyPath) return ("", "") proc certificateFingerprint*(certPath: string): string = if not fileExists(certPath): return "" let cmd = "openssl x509 -in " & quoteShell(certPath) & " -fingerprint -noout 2>/dev/null" let (output, _) = execCmdEx(cmd) for line in output.splitLines(): if "Fingerprint=" in line: let parts = line.split("Fingerprint=") if parts.len > 1: return parts[^1].strip() return "" proc isExpired*(certPath: string): bool = if not fileExists(certPath): return true let cmd = "openssl x509 -in " & quoteShell(certPath) & " -checkend 0 2>/dev/null" return execShellCmd(cmd) != 0 proc daysUntilExpiry*(certPath: string): int = if not fileExists(certPath): return -1 # Check if expires within 1 day let cmd1 = "openssl x509 -in " & quoteShell(certPath) & " -checkend 86400 2>/dev/null" if execShellCmd(cmd1) == 0: # Check if expires within 30 days let cmd30 = "openssl x509 -in " & quoteShell(certPath) & " -checkend 2592000 2>/dev/null" if execShellCmd(cmd30) == 0: return 365 return 30 return 1 proc validateCert*(certPath: string): seq[string] = result = @[] if not fileExists(certPath): result.add("Certificate file not found: " & certPath) if isExpired(certPath): result.add("Certificate has expired")