From 8196b1c2407e96711736de338dd67ffe7d1b6d59 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Wed, 13 Jul 2016 09:37:43 +0100 Subject: [PATCH] Update GPG signature checking --- macros.gpg | 316 +++++++++++++++++++++++++++++++++++++++++++++++ openconnect.spec | 6 +- 2 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 macros.gpg diff --git a/macros.gpg b/macros.gpg new file mode 100644 index 0000000..fab8861 --- /dev/null +++ b/macros.gpg @@ -0,0 +1,316 @@ +# The gpg_verify macro is defined further down in this document. + +# gpg_verify takes one option and a list of 2- or 3-tuples. +# +# With no arguments, attempts to figure everything out. Finds one keyring and +# tries to pair each signature file with a source. If there is no source found +# which matches a signature, the build is aborted. +# +# -k gives a common keyring to verify all signatures against, except when an +# argument specifies its own keyring. +# +# Each argument must be of the form "F,S,K" or "F,S", where each of F, S and K +# is either the number or the filename of one of the source files in the +# package. A pathname including directories is not allowed. +# F is a source file to check. +# S is a signature. +# K is a keyring. +# +# When an argument specifies a keyring, that signature will be verified against +# the keys in that keyring. For arguments that don't specify a keyring, the one +# specified with -k will be used, if any. If no keyring is specified either +# way, the macro will default to the first one it finds in the source list. +# +# It is assumed that all the keys in all keyrings, whether automatically found +# or explicitly specified, are trusted to authenticate the source files. There +# must not be any untrusted keys included. + +# Some utility functions to the global namespace +# Most of these should come from the utility macros in the other repo. +%define gpg_macros_init %{lua: + function db(str) + io.stderr:write(tostring(str) .. '\\n') + end +\ + -- Simple basename clone + function basename(str) + local name = string.gsub(str, "(.*/)(.*)", "%2") + return name + end +\ + -- Get the numbered or source file. + -- The spec writer can use any numbering scheme. The sources table + -- always counts from 1 and has no gaps, so we have to go back to the + -- SOURCEN macros. + function get_numbered_source(num) + local macro = "%SOURCE" .. num + local val = rpm.expand(macro) + if val == macro then + return nil + end + return val + end + -- Get the named source file. This returns the full path to a source file, + -- or nil if no such source exists. + function get_named_source(name) + local path + for _,path in ipairs(sources) do + if name == basename(path) then + return path + end + end + return nil + end +\ + -- Determine whether the supplied filename contains a signature + -- Assumes the file will be closed when the handle goes out of scope + function is_signature(fname) + -- I don't really like this, but you can have completely binary sigs + if string.find(fname, '%.sig$') then + return true + end + local file = io.open(fname, 'r') + if file == nil then return false end +\ + local c = 1 + while true do + local line = file:read('*line') + if (line == nil or c > 10) then break end + if string.find(line, "BEGIN PGP SIGNATURE") then + return true + end + c = c+1 + end + return false + end +\ + -- Determine whether the supplied filename looks like a keyring + -- Ends in .gpg (might be binary data)? Contains "BEGIN PGP PUBLIC KEY BLOCK" + function is_keyring(fname) + -- XXX Have to hack this now to make it not find macros.gpg while we're testing. + if string.find(fname, '%.gpg$') and not string.find(fname, 'macros.gpg$') then + return true + end +\ + local file = io.open(fname, 'r') + if file == nil then return false end + io.input(file) + local c = 1 + while true do + local line = io.read('*line') + if (line == nil or c > 10) then break end + if string.find(line, "BEGIN PGP PUBLIC KEY BLOCK") then + return true + end + c = c+1 + end + return false + end +\ + -- Output code to have the current scriptlet echo something + function echo(str) + print("echo " .. str .. "\\n") + end +\ + -- Output an exit statement with nonzero return to the current scriptlet + function exit() + print("exit 1\\n") + end +\ + -- Call the RPM %error macro + function rpmerror(str) + echo("gpg_verify: " .. str) + rpm.expand("%{error:gpg_verify: " .. str .. "}") + exit(1) + end +\ + -- XXX How to we get just a flag and no option? + function getflag(flag) + return nil + end +\ + -- Extract the value of a passed option + function getoption(opt) + out = rpm.expand("%{-" .. opt .. "*}") + -- if string.len(out) == 0 then + if #out == 0 then + return nil + end + return out + end +\ + function unknownarg(a) + rpmerror("Unknown argument to %%gpg_verify: " .. a) + end +\ + function rprint(s, l, i) -- recursive Print (structure, limit, indent) + l = (l) or 100; i = i or ""; -- default item limit, indent string + if (l<1) then db("ERROR: Item limit reached."); return l-1 end; + local ts = type(s); + if (ts ~= "table") then db(i,ts,s); return l-1 end + db(i,ts); -- print "table" + for k,v in pairs(s) do -- db("[KEY] VALUE") + l = rprint(v, l, i.."\t["..tostring(k).."]"); + if (l < 0) then break end + end + return l + end +\ + -- Given a list of source file numbers or file names, validate them and + -- convert them to a list of full filenames. + function check_sources_list(arr) + local files = {} + local src,fpath + for _, src in ipairs(arr) do + if tonumber(src) then + -- We have a number; turn it to a full path to the corresponding source file + fpath = get_numbered_source(src) + else + fpath = get_named_source(src) + end + if not src then + err = 'Not a valid source: ' .. src + if src == '1' then + err = err .. '. Note that "Source:" is the 0th source file, not the 1st.' + end + rpmerror(err) + end + table.insert(files, fpath) + end + return files + end + rpm.define("gpg_macros_init %{nil}") +}# + +# The actual macro +%define gpg_verify(k:) %gpg_macros_init%{lua: + -- RPM will ignore the first thing we output unless we give it a newline. + print('\\n') +\ + local defkeyspec = getoption("k") + local args = rpm.expand("%*") + local sourcefiles = {} + local signature_table = {} + local signatures = {} + local keyrings = {} + local defkey, match, captures, s +\ + local function storematch(m, c) + match = m; captures = c + end +\ + -- Scan all of the sources and try to categorize them. + -- Move to a function + for i,s in pairs(sources) do + sourcefiles[s] = true + -- db('File: ' .. i .. ", " .. s) + if is_signature(s) then + table.insert(signatures, s) + signature_table[s] = true + db('Found signature: ' .. s) + elseif is_keyring(s) then + table.insert(keyrings, s) + db('Found keyring: ' .. s) + else + -- Must be a source + db('Found source: ' .. s) + end + end +\ + if defkeyspec then + defkey = check_sources_list({defkeyspec})[1] + if not defkey then + rpmerror('The provided keyring ' .. defkeyspec .. ' is not a valid source number or filename.') + end + end +\ + if defkey then + db('Defkey: ' .. defkey) + else + db('No common key yet') + if keyrings[1] then + defkey = keyrings[1] + db('Using first found keyring file: '..defkey) + end + end +\ + -- Check over any given args to make sure they're valid, and to see if a + -- common key is required. + local needdefkey = false + local double = rex.newPOSIX('^([^,]+),([^,]+)$') + local triple = rex.newPOSIX('^([^,]+),([^,]+),([^,]+)$') + local arglist = {} +\ + -- RPM gives us the arguments in a single string. + -- Split on spaces and iterate + for arg in args:gmatch('%S+') do + db('Checking ' .. arg) + if triple:gmatch(arg, storematch) > 0 then + db('Looks OK') + local parsed = {srcnum=captures[1], signum=captures[2], keynum=captures[3]} + s = check_sources_list({captures[1], captures[2], captures[3]}) + parsed.srcfile = s[1] + parsed.sigfile = s[2] + parsed.keyfile = s[3] + table.insert(arglist, parsed) + elseif double:gmatch(arg, storematch) > 0 then + db('Looks OK; needs common key') + needdefkey = true + local parsed = {srcnum=captures[1], signum=captures[2], keynum=defkeyspec, keyfile=defkey} + s = check_sources_list({captures[1], captures[2]}) + parsed.srcfile = s[1] + parsed.sigfile = s[2] + table.insert(arglist, parsed) + else + rpmerror('Provided argument '..arg..' is not valid.') + end + end +\ + -- So we now know if one of those args needs a common key + if needdefkey and not defkey then + rpmerror('No common key was specified or found, yet the arguments require one.') + end +\ + -- And if we have no arguments at all and no common key was found, + -- then we can't do an automatic check + if not defkey and args == '' then + rpmerror('No keyring specified and none found; cannot auto-check.') + end +\ + -- Nothing to check means automatic mode + if #arglist == 0 then + local noext + for i,_ in pairs(signature_table) do + -- Find the name without the extension + noext = string.gsub(i, '%.[^.]+$', '') + if sourcefiles[noext] then + table.insert(arglist, {srcfile=noext, sigfile=i, keyfile=defkey}) + else + rpmerror('Found signature ' .. i .. ' with no matching source file.') + end + end + end +\ + -- Now actually check things + for _,arg in ipairs(arglist) do + local gpgfile = '$GPGHOME/' .. basename(arg.keyfile) .. '.gpg' + echo('Checking signature: file ' .. arg.srcfile .. ' sig ' .. arg.sigfile .. ' key ' .. arg.keyfile) +\ + -- We need a secure temp directorry + print('GPGHOME=$(mktemp -qd)\\n') +\ + -- Call gpg2 to generate the dearmored key + print('gpg2 --homedir $GPGHOME --no-default-keyring --quiet --yes ') + print('--output '.. gpgfile .. ' --dearmor ' .. arg.keyfile .. "\\n") +\ + -- Call gpgv2 to verify the signature against the source file with the dearmored key + print('gpgv2 --homedir $GPGHOME --keyring ' .. gpgfile .. ' ' .. arg.sigfile .. ' ' .. arg.srcfile .. '\\n') +\ + print('rm -rf $GPGHOME\\n') + echo('') + end +\ + db('------------') +}# + +# vim: set filetype=spec: diff --git a/openconnect.spec b/openconnect.spec index 5a189f6..0ded4ae 100644 --- a/openconnect.spec +++ b/openconnect.spec @@ -33,7 +33,8 @@ Source0: ftp://ftp.infradead.org/pub/openconnect/openconnect-%{version}%{?gitsuf %if 0%{?gitcount} == 0 Source1: ftp://ftp.infradead.org/pub/openconnect/openconnect-%{version}%{?gitsuffix}.tar.gz.asc %endif -Source2: gpgkey-BE07D9FD54809AB2C4B0FF5F63762CDA67E2F359.gpg +Source2: gpgkey-BE07D9FD54809AB2C4B0FF5F63762CDA67E2F359.asc +Source3: macros.gpg BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) @@ -78,9 +79,10 @@ This package provides the core HTTP and authentication support from the OpenConnect VPN client, to be used by GUI authentication dialogs for NetworkManager etc. +%include %SOURCE3 %prep %if 0%{?gitcount} == 0 -gpgv2 --keyring %{SOURCE2} %{SOURCE1} %{SOURCE0} +%gpg_verify %endif %setup -q -n openconnect-%{version}%{?gitsuffix}