186 lines
5.8 KiB
Lua
186 lines
5.8 KiB
Lua
-- SPDX-License-Identifier: GPL-3.0-or-later
|
|
--[[
|
|
Conventions:
|
|
- key = private+public key-pair in openssl.pkey format
|
|
- certs = lua list of certificates (at least one), each in openssl.x509 format,
|
|
ordered from leaf to almost-root
|
|
- panic('...') is used on bad problems instead of returning nils or such
|
|
--]]
|
|
local tls_cert = {}
|
|
|
|
local ffi = require('ffi')
|
|
local x509, pkey = require('openssl.x509'), require('openssl.pkey')
|
|
|
|
-- @function Create self-signed certificate; return certs, key
|
|
local function new_ephemeral(host)
|
|
-- Import luaossl directly
|
|
local name = require('openssl.x509.name')
|
|
local altname = require('openssl.x509.altname')
|
|
local openssl_bignum = require('openssl.bignum')
|
|
local openssl_rand = require('openssl.rand')
|
|
-- Create self-signed certificate
|
|
host = host or hostname()
|
|
local crt = x509.new()
|
|
local now = os.time()
|
|
crt:setVersion(3)
|
|
-- serial needs to be unique or browsers will show uninformative error messages
|
|
crt:setSerial(openssl_bignum.fromBinary(openssl_rand.bytes(16)))
|
|
-- use the host we're listening on as canonical name
|
|
local dn = name.new()
|
|
dn:add("CN", host)
|
|
crt:setSubject(dn)
|
|
crt:setIssuer(dn) -- should match subject for a self-signed
|
|
local alt = altname.new()
|
|
alt:add("DNS", host)
|
|
crt:setSubjectAlt(alt)
|
|
-- Valid for 90 days
|
|
crt:setLifetime(now, now + 90*60*60*24)
|
|
-- Can't be used as a CA
|
|
crt:setBasicConstraints{CA=false}
|
|
crt:setBasicConstraintsCritical(true)
|
|
-- Create and set key (default: EC/P-256 as a most "interoperable")
|
|
local key = pkey.new {type = 'EC', curve = 'prime256v1'}
|
|
crt:setPublicKey(key)
|
|
crt:sign(key)
|
|
return { crt }, key
|
|
end
|
|
|
|
-- @function Write certs and key to files
|
|
local function write_cert_files(certs, key, certfile, keyfile)
|
|
-- Write certs
|
|
local f = assert(io.open(certfile, 'w'), string.format('cannot open "%s" for writing', certfile))
|
|
for _, cert in ipairs(certs) do
|
|
f:write(tostring(cert))
|
|
end
|
|
f:close()
|
|
-- Write key as a pair
|
|
f = assert(io.open(keyfile, 'w'), string.format('cannot open "%s" for writing', keyfile))
|
|
local pub, priv = key:toPEM('public', 'private')
|
|
assert(f:write(pub .. priv))
|
|
f:close()
|
|
end
|
|
|
|
-- @function Start maintenance of a self-signed TLS context (at ephem_state.ctx).
|
|
-- Keep updating the ephem_state.servers table. Stop updating by calling _destroy().
|
|
-- TODO: each process maintains its own ephemeral cert ATM, and the files aren't ever read from.
|
|
function tls_cert.ephemeral_state_maintain(ephem_state, certfile, keyfile)
|
|
local certs, key = new_ephemeral()
|
|
write_cert_files(certs, key, certfile, keyfile)
|
|
ephem_state.ctx = tls_cert.new_tls_context(certs, key)
|
|
-- Each server needs to have its ctx updated.
|
|
for _, s in pairs(ephem_state.servers) do
|
|
s.server.ctx = ephem_state.ctx
|
|
s.config.ctx = ephem_state.ctx -- not required, but let's keep it synchronized
|
|
end
|
|
log_info(ffi.C.LOG_GRP_HTTP, 'created new ephemeral TLS certificate')
|
|
local _, expiry_stamp = certs[1]:getLifetime()
|
|
local wait_msec = 1000 * math.max(1, expiry_stamp - os.time() - 3 * 24 * 3600)
|
|
if not ephem_state.timer_id then
|
|
ephem_state.timer_id = event.after(wait_msec, function ()
|
|
tls_cert.ephemeral_state_maintain(ephem_state, certfile, keyfile)
|
|
end)
|
|
else
|
|
event.reschedule(ephem_state.timer_id, wait_msec)
|
|
end
|
|
end
|
|
function tls_cert.ephemeral_state_destroy(ephem_state)
|
|
if ephem_state and ephem_state.timer_id then
|
|
event.cancel(ephem_state.timer_id)
|
|
end
|
|
end
|
|
|
|
-- @function Read a certificate chain and a key from files; return certs, key
|
|
function tls_cert.load(certfile, keyfile)
|
|
-- get key
|
|
local f, err = io.open(keyfile, 'r')
|
|
if not f then
|
|
panic('[http] unable to open TLS key file: %s', err)
|
|
end
|
|
local key = pkey.new(f:read('*all'))
|
|
f:close()
|
|
if not key then
|
|
panic('[http] unable to parse TLS key file %s', keyfile)
|
|
end
|
|
|
|
-- get certs list
|
|
local certs = {}
|
|
local f, err = io.open(certfile, 'r')
|
|
if not f then
|
|
panic('[http] unable to read TLS certificate file: %s', err)
|
|
end
|
|
while true do
|
|
-- Get the next "block" = single certificate as PEM string.
|
|
local block = nil
|
|
local line
|
|
repeat
|
|
line = f:read()
|
|
if not line then break end
|
|
if block then
|
|
block = block .. '\n' .. line
|
|
else
|
|
block = line
|
|
end
|
|
-- separator: "posteb" in https://tools.ietf.org/html/rfc7468#section-3
|
|
until string.sub(line, 1, 9) == '-----END '
|
|
-- Empty block means clean EOF.
|
|
if not block then break end
|
|
if not line then
|
|
panic('[http] unable to parse TLS certificate file %s, certificate number %d', certfile, 1 + #certs)
|
|
end
|
|
|
|
-- Parse the cert and append to the list.
|
|
local cert = x509.new(block, 'PEM')
|
|
if not cert then
|
|
panic('[http] unable to parse TLS certificate file %s, certificate number %d', certfile, 1 + #certs)
|
|
end
|
|
table.insert(certs, cert)
|
|
end
|
|
f:close()
|
|
|
|
return certs, key
|
|
end
|
|
|
|
|
|
-- @function Prefer HTTP/2 or HTTP/1.1
|
|
local function alpnselect(_, protos)
|
|
for _, proto in ipairs(protos) do
|
|
if proto == 'h2' or proto == 'http/1.1' then
|
|
return proto
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local warned_old_luaossl = false
|
|
|
|
-- @function Return a new TLS context for a server.
|
|
function tls_cert.new_tls_context(certs, key)
|
|
local ctx = require('http.tls').new_server_context()
|
|
if ctx.setAlpnSelect then
|
|
ctx:setAlpnSelect(alpnselect)
|
|
end
|
|
assert(ctx:setPrivateKey(key))
|
|
assert(ctx:setCertificate(certs[1]))
|
|
|
|
-- Set up certificate chain to be sent, if required and possible.
|
|
if #certs == 1 then return ctx end
|
|
if ctx.setCertificateChain then
|
|
local chain = require('openssl.x509.chain').new()
|
|
assert(chain)
|
|
for i = 2, #certs do
|
|
chain:add(certs[i])
|
|
assert(chain)
|
|
end
|
|
assert(ctx:setCertificateChain(chain))
|
|
elseif not warned_old_luaossl then
|
|
-- old luaossl version -> only final cert sent to clients
|
|
log_warn(ffi.C.LOG_GRP_HTTP,
|
|
'need luaossl >= 20181207 to support sending intermediary certificate to clients')
|
|
warned_old_luaossl = true
|
|
end
|
|
return ctx
|
|
end
|
|
|
|
|
|
return tls_cert
|
|
|