Files
Baradb/src/barabadb/protocol/ssl.nim
T

134 lines
4.4 KiB
Nim

## 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):
# caFile is only honored by newContext when verifyPeer is true
# (verifyMode != CVerifyNone); a missing CA file then raises IOError,
# which is the desired fail-closed behavior.
result.sslCtx = newContext(
certFile = config.certFile,
keyFile = config.keyFile,
verifyMode = if config.verifyPeer: CVerifyPeer else: CVerifyNone,
caFile = config.caFile,
)
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:
# wrapConnectedSocket (asyncnet overload) sets connect state; the
# handshake itself is driven lazily by the first send/recv. Plain
# wrapSocket leaves the SSL handle in SSL_ST_BEFORE and the first
# SSL_write fails with "uninitialized".
tls.sslCtx.wrapConnectedSocket(socket, handshakeAsClient)
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")