summaryrefslogtreecommitdiffstats
path: root/modules/http/http_tls_cert.lua
blob: 7e557c452b91a36a2c49ac65c2293877533a7d00 (plain)
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