a_08_uri.lua 13.5 KB
Newer Older
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
--[[
Copyright 2016, CZ.NIC z.s.p.o. (http://www.nic.cz/)

This file is part of the turris updater.

Updater is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Updater is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Updater.  If not, see <http://www.gnu.org/licenses/>.
]]--

--[[
This module prepares and manipulates contexts and environments for
the configuration scripts to be run in.
]]

local error = error
local ipairs = ipairs
local pairs = pairs
28
local tonumber = tonumber
29
local tostring = tostring
30
local pcall = pcall
31
local type = type
32
local require = require
33 34 35
local unpack = unpack
local setmetatable = setmetatable
local table = table
36 37
local os = os
local io = io
38 39
local string = string
local events_wait = events_wait
40
local download = download
41
local run_command = run_command
42
local run_util = run_util
43
local utils = require "utils"
44
local DBG = DBG
45
local uri_internal_get = uri_internal_get
46
local sha256 = sha256
47 48 49

module "uri"

50 51 52 53 54
-- luacheck: globals wait signature_check parse new  system_cas ignore_crl

-- Constants used for inheritance breakage
system_cas = "uri_system_cas"
ignore_crl = "uri_ignore_crl"
55

56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
local function percent_decode(text)
	return text:gsub('%%(..)', function (encoded)
		local cnum = tonumber(encoded, 16)
		if not cnum then
			error(utils.exception("bad value", encoded .. " is not a hex number"))
		end
		return string.char(cnum)
	end)
end

--[[
The following function is borrowed from http://lua-users.org/wiki/BaseSixtyFour
-- Lua 5.1+ base64 v3.0 (c) 2009 by Alex Kloss <alexthkloss@web.de>
-- licensed under the terms of the LGPL2
]]
-- character table string
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

-- decoding
75
local function base64_decode(data)
76 77 78 79 80 81 82 83 84 85 86 87 88 89
    data = string.gsub(data, '[^'..b..'=]', '')
    return (data:gsub('.', function(x)
        if (x == '=') then return '' end
        local r,f='',(b:find(x)-1)
        for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
        return r;
    end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
        if (#x ~= 8) then return '' end
        local c=0
        for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
        return string.char(c)
    end))
end
-- End of borrowed function.
90

91
local function handler_data(uri, err_cback, done_cback)
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
	local params, data = uri:match('^data:([^,]*),(.*)')
	if not data then
		return err_cback(utils.exception("malformed URI", "It doesn't look like data URI"))
	end
	local ok, result = pcall(percent_decode, data)
	if ok then
		data = result
	else
		return err_cback(utils.exception("malformed URI", "Bad URL encoding"))
	end
	params = utils.lines2set(params, ';')
	if params['base64'] then
		local ok, result = pcall(base64_decode, data)
		if ok then
			data = result
		else
			return err_cback(utils.exception("malformed URI", "Bad base64 data"))
		end
	end
	-- Once decoded, this is complete ‒ nothing asynchronous about this URI
	done_cback(data)
end

115
local function handler_file(uri, err_cback, done_cback)
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
	local fname = uri:match('^file://(.*)')
	if not fname then
		return err_cback(utils.exception("malformed URI", "Not a file:// URI"))
	end
	local ok
	ok, fname = pcall(percent_decode, fname)
	if not ok then
		return err_cback(utils.exception("malformed URI", "Bad URL encoding"))
	end
	local ok, content, err = pcall(utils.slurp, fname)
	if (not ok) or (not content) then
		return err_cback(utils.exception("unreachable", tostring(content or err)))
	end
	done_cback(content)
end

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
local function handler_internal(uri, err_cback, done_cback)
	local iname = uri:match('^internal:(.*)')
	if not iname then
		return err_cback(utils.exception("malformed URI", "Not a internal:// URI"))
	end
	local ok
	ok, iname = pcall(percent_decode, iname)
	if not ok then
		return err_cback(utils.exception("malformed URI", "Bad URL encoding"))
	end
	local content
	ok, content = pcall(uri_internal_get, iname)
	if not ok then
		return err_cback(utils.exception("unreachable", tostring(content)))
	end
	done_cback(content)
end

150
-- Actually, both for http and https
151
local function handler_http(uri, err_cback, done_cback, ca, crl, use_ssl)
152 153 154 155
	return download(function (status, answer)
		if status == 200 then
			done_cback(answer)
		else
156
			err_cback(utils.exception("unreachable", uri .. ": " .. tostring(answer)))
157
		end
158
	end, uri, ca, crl, use_ssl)
159 160
end

161 162 163 164 165 166 167 168
local function match_check(uri, context)
	if context:level_check("Remote") then
		-- No restriction in this context. Don't check.
		return true
	end
	return uri:match('^' .. context.restrict .. '$')
end

169
local handlers = {
170 171 172 173 174 175 176 177 178 179 180 181
	data = {
		handler = handler_data,
		immediate = true,
		def_verif = 'none',
		sec_level = 'Restricted'
	},
	file = {
		handler = handler_file,
		immediate = true,
		def_verif = 'none',
		sec_level = 'Local'
	},
182 183 184 185 186 187
	internal = {
		handler = handler_internal,
		immediate = true,
		def_verif = 'none',
		sec_level = 'Local'
	},
188 189 190
	http = {
		handler = handler_http,
		def_verif = 'sig',
191 192
		sec_level = 'Restricted',
		match = match_check
193 194 195 196 197
	},
	https = {
		handler = handler_http,
		can_check_cert = true,
		def_verif = 'both',
198 199
		sec_level = 'Restricted',
		match = match_check
200
	}
201 202 203 204 205 206 207 208 209 210 211 212 213 214
}

function wait(...)
	local events = {}
	local offset = 0
	for _, u in pairs({...}) do
		for i, e in pairs(u.events) do
			events[i + offset] = e
		end
		offset = offset + #u.events
	end
	events_wait(unpack(events))
end

215
local function tempfile()
216
	local fname = os.tmpname()
217 218 219 220 221 222 223
	local f, err = io.open(fname, "w")
	if not f then DIE(err) end
	return fname, f
end

local function tmpstore(content)
	local fname, f = tempfile()
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
	f:write(content)
	f:close()
	return fname
end

function signature_check(content, key, signature)
	local ok
	local fcontent = tmpstore(content)
	local fkey = tmpstore(key)
	local fsig = tmpstore(signature)
	events_wait(run_command(function (ecode)
		ok = (ecode == 0)
	end, nil, nil, -1, -1, '/usr/bin/usign', '-V', '-p', fkey, '-x', fsig, '-m', fcontent))
	os.remove(fcontent)
	os.remove(fkey)
	os.remove(fsig)
	return ok
end

243 244
local allowed_verifications = utils.arr2set({'none', 'cert', 'sig', 'both'})

245 246
-- Parse the uri and throw an error or return the handler for it
function parse(context, uri)
247 248 249 250 251 252 253 254
	local schema = uri:match('^(%a+):')
	if not schema then
		error(utils.exception("bad value", "Malformed URI " .. uri))
	end
	local handler = handlers[schema]
	if not handler then
		error(utils.exception("bad value", "Unknown URI schema " .. schema))
	end
255 256 257
	if not context:level_check(handler.sec_level) then
		error(utils.exception("access violation", "At least " .. handler.sec_level .. " level required for " .. schema .. " URI"))
	end
258 259 260
	if handler.match and not handler.match(uri, context) then
		error(utils.exception("access violation", "The uri " .. uri .. " does not pass restrictions"))
	end
261 262 263
	return handler
end

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
--[[
This function stores the content in a file with name corresponding to the hash of the content.
That ensures that we create only one temporary file of each content. This doesn't solve
the problem of leftover files completely, but makes it non-fatal (there's no chance
this would eat all the available space in /tmp).
]]
local function hashed_file(content)
	local hash = sha256(content)
	local name, f = tempfile()
	f:write(content)
	f:close()
	local renamed = name:gsub("[^/]*$", hash)
	os.rename(name, renamed)
	return renamed
end

280
function new(context, uri, verification)
281
	DBG("Creating new URI: ", uri)
282
	local handler = parse(context, uri)
283
	-- Prepare verification
284
	verification = verification or {}
285 286 287 288 289 290 291 292 293 294
	local context_nocheck_cache
	local function context_nocheck(want_context)
		if want_context and not context_nocheck_cache then
			local sandbox = require "sandbox"
			context_nocheck_cache = sandbox.new('Full', context)
		end
		return context_nocheck_cache
	end
	-- Try to find out verification parameter, in the verification argument, in the context or use a default. Provide a context used to check the required URIs inside the result (eg. if it is inherited, don't check them).
	local function ver_lookup(field, default, want_context)
295
		if verification[field] ~= nil then
296
			return verification[field], context
297
		elseif context[field] ~= nil then
298
			return context[field], context_nocheck(want_context)
299
		else
300
			return default, context
301 302
		end
	end
303 304 305
	local result = {
		tp = "uri",
		done = false,
306
		done_primary = false,
307
		uri = uri,
308 309 310
		callbacks = {},
		events = {}
	}
311 312 313 314 315 316 317 318 319 320 321 322
	-- Prepare the result and callbacks into the handler
	function result:ok()
		if self.done then
			return self.err == nil
		else
			return nil
		end
	end
	function result:get()
		wait(self)
		return self:ok(), self.content or self.err
	end
323 324 325 326
	-- Soft failure during the preparation
	local function give_up(err)
		result.done = true
		result.err = err
327
		result.events = {}
328 329 330
		return result
	end
	local vermode = ver_lookup('verification', handler.def_verif)
331 332 333
	if not allowed_verifications[vermode] then
		error(utils.exception('bad value', "Unknown verification mode " .. vermode))
	end
334
	local do_cert = handler.can_check_cert and (vermode == 'both' or vermode == 'cert')
335
	local explicit_ca, use_crl
336 337 338 339 340 341
	if do_cert then
		local function pem_get(uris, context)
			if type(uris) == 'string' then
				uris = {uris}
			end
			if type(uris) == 'table' then
342
				local all = ''
343 344 345
				for _, curi in ipairs(uris) do
					local u = new(context, curi, {verification = 'none'})
					local ok, content = u:get()
346 347 348 349
					if not ok then
						give_up(content)
						return nil
					end
350
					all = all .. content
351
				end
352
				return hashed_file(all)
353 354 355 356
			else
				error(utils.exception('bad value', "The ca and crl must be either string or table, not " .. type(uris)))
			end
		end
357 358 359 360 361 362
		local ca, ca_context = ver_lookup('ca', system_cas, true)
		if ca ~= system_cas then
			explicit_ca = pem_get(ca, ca_context)
			if not explicit_ca then
				return result
			end
363
		end
364 365
		local crl, crl_context = ver_lookup('crl', ignore_crl, true)
		if crl ~= ignore_crl then
366
			use_crl = pem_get(crl, crl_context)
367 368 369 370 371 372 373 374 375 376 377 378
			if not use_crl then
				return result
			end
		end
	end
	local do_sig = vermode == 'both' or vermode == 'sig'
	local sig_data
	local sig_pubkeys = {}
	local sub_uris = {}
	if do_sig then
		-- As we check the signature after we download everything, just schedule it now
		local sig_uri = verification.sig or uri .. ".sig"
379 380 381 382
		local sig_veri = utils.table_overlay(verification)
		sig_veri.verification = 'none'
		if do_cert then sig_veri.verification = 'cert' end -- If the main resource checks cert, the .sig should too
		sig_data = new(context, sig_uri, sig_veri)
383
		table.insert(sub_uris, sig_data)
384
		local pubkeys, pk_context = ver_lookup('pubkey', {}, true)
385 386 387 388 389
		if type(pubkeys) == 'string' then
			pubkeys = {pubkeys}
		end
		if type(pubkeys) == 'table' then
			for _, uri in ipairs(pubkeys) do
390
				local u = new(pk_context, uri, {verification = 'none'})
391 392 393 394
				table.insert(sub_uris, u)
				table.insert(sig_pubkeys, u)
			end
		else
395
			error(utils.exception('bad value', "The pubkey must be either string or table, not " .. type(pubkeys)))
396 397
		end
	end
398
	local wait_sub_uris = #sub_uris
399
	local function dispatch()
400
		if result.done_primary and wait_sub_uris == 0 and not result.done then
401
			result.done = true
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
			if do_sig and result.err == nil then
				local ok, signature = sig_data:get()
				if ok then
					local found = false
					local key_broken = false
					local function sigval(content)
						for _, key in ipairs(sig_pubkeys) do
							local ok, k = key:get()
							if ok then
								if signature_check(content, k, signature) then
									found = true
									break
								end
							else
								key_broken = true
							end
						end
					end
					sigval(result.content)
					if not found and result.content:sub(1, 2) == string.char(0x1F, 0x8B) then
						-- Try once more with gzip decompressed
423
						local function gzip_done(ecode, _, stdout)
424 425 426 427
							if ecode == 0 then
								sigval(stdout)
							end
						end
428
						events_wait(run_util(gzip_done, nil, result.content, -1, -1, 'gzip', '-c', '-d'))
429 430 431 432 433 434 435 436 437 438 439 440 441 442
					end
					if not found then
						local msg = "Signature validation failed"
						if key_broken then
							msg = msg .. " (some keys are missing)"
						end
						result.err = utils.exception("corruption", msg)
						result.content = nil
					end
				else
					result.err = signature
					result.content = nil
				end
			end
443
		end
444
		if result.done then
445
			result.events = {}
446 447 448 449 450 451 452 453 454 455 456
			for _, cback in ipairs(result.callbacks) do
				cback(result:get())
			end
			result.callbacks = {}
		end
	end
	function result:cback(cback)
		table.insert(self.callbacks, cback)
		dispatch()
	end
	local function err_cback(err)
457
		result.done_primary = true
458 459 460 461
		result.err = err
		dispatch()
	end
	local function done_cback(content)
462
		result.done_primary = true
463 464 465
		result.content = content
		dispatch()
	end
466
	result.events = {handler.handler(uri, err_cback, done_cback, explicit_ca, use_crl, do_cert)}
467 468 469 470 471 472 473 474 475 476 477
	-- Wait for the sub uris and include them in our events
	local function sub_cback()
		wait_sub_uris = wait_sub_uris - 1
		dispatch()
	end
	for _, subu in ipairs(sub_uris) do
		subu:cback(sub_cback)
		for _, e in ipairs(subu.events) do
			table.insert(result.events, e)
		end
	end
478 479 480 481 482 483 484 485 486
	return result
end

--[[
Magic allowing to just call the module and get the corresponding object.
Instead of calling uri.new("file:///stuff"), uri("file://stuff") can be
used (the first version works too).
]]
local meta = {
487
	__call = function (_, context, uri, verification)
488 489 490 491 492
		return new(context, uri, verification)
	end
}

return setmetatable(_M, meta)