1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
-- 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
|