Blob Blame History Raw
# 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: