From dd8772c131f3b0c2dc40fd78df0f177285edb89f Mon Sep 17 00:00:00 2001 From: Vsevolod Stakhov Date: Tue, 5 Jun 2018 17:03:10 +0100 Subject: [PATCH] [Feature] Implement signatures verification using rspamadm keypair --- lualib/rspamadm/keypair.lua | 211 +++++++++++++++++++++++++++--------- 1 file changed, 159 insertions(+), 52 deletions(-) diff --git a/lualib/rspamadm/keypair.lua b/lualib/rspamadm/keypair.lua index 69b50b921..518c9e65f 100644 --- a/lualib/rspamadm/keypair.lua +++ b/lualib/rspamadm/keypair.lua @@ -16,8 +16,11 @@ limitations under the License. local argparse = require "argparse" local rspamd_keypair = require "rspamd_cryptobox_keypair" +local rspamd_pubkey = require "rspamd_cryptobox_pubkey" +local rspamd_signature = require "rspamd_cryptobox_signature" local rspamd_crypto = require "rspamd_cryptobox" local ucl = require "ucl" +local logger = require "rspamd_logger" -- Define command line options local parser = argparse() @@ -58,11 +61,35 @@ sign:argument "file" :argname "" :args "*" +local verify = parser:command "verify ver v" + :description "Verifies a file using keypair or a public key" +verify:mutex( + verify:option "-p --pubkey" + :description "Load pubkey from the specified file" + :argname "", + verify:option "-P --pubstring" + :description "Load pubkey from the base32 encoded string" + :argname "", + verify:option "-k --keypair" + :description "Get pubkey from the keypair file" + :argname "" +) +verify:argument "file" + :description "File to verify" + :argname "" + :args "*" +verify:flag "-n --nist" + :description "Uses nistp curves (P256)" +verify:option "-s --suffix" + :description "Suffix for signature" + :argname "" + :default("sig") + -- Default command is generate, so duplicate options parser:flag "-s --sign" :description "Generates a sign keypair instead of the encryption one" parser:flag "-n --nist" - :description "Uses nist encryption algorithm" + :description "Uses nistp curves (P256)" parser:mutex( parser:flag "-j --json" :description "Output JSON instead of UCL", @@ -74,81 +101,161 @@ parser:option "-o --output" :description "Write keypair to file" :argname "" -local function handler(args) - local opts = parser:parse(args) +local function fatal(...) + logger.errx(...) + os.exit(1) +end - local command = opts.command or "generate" +local function generate_handler(opts) + local mode = 'encryption' + if opts.sign then + mode = 'sign' + end + local alg = 'curve25519' + if opts.nist then + alg = 'nist' + end + -- TODO: probably, do it in a more safe way + local kp = rspamd_keypair.create(mode, alg):totable() - if command == 'generate' then - local mode = 'encryption' - if opts.sign then - mode = 'sign' + local format = 'ucl' + + if opts.json then + format = 'json' + end + + if opts.output then + local out = io.open(opts.output, 'w') + if not out then + fatal('cannot open output to write: ' .. opts.output) end - local alg = 'curve25519' - if opts.nist then - alg = 'nist' + out:write(ucl.to_format(kp, format)) + out:close() + else + io.write(ucl.to_format(kp, format)) + end +end + +local function sign_handler(opts) + if opts.file then + if type(opts.file) == 'string' then + opts.file = {opts.file} end - -- TODO: probably, do it in a more safe way - local kp = rspamd_keypair.create(mode, alg):totable() + else + parser:error('no files to sign') + end + if not opts.keypair then + parser:error("no keypair specified") + end - local format = 'ucl' + local ucl_parser = ucl.parser() + local res,err = ucl_parser:parse_file(opts.keypair) - if opts.json then - format = 'json' - end + if not res then + fatal(string.format('cannot load %s: %s', opts.keypair, err)) + end - if opts.output then - local out = io.open(opts.output, 'w') - if not out then - parser:error('cannot open output to write: ' .. opts.output) - end - out:write(ucl.to_format(kp, format)) - out:close() - else - io.write(ucl.to_format(kp, format)) + local kp = rspamd_keypair.load(ucl_parser:get_object()) + + if not kp then + fatal("cannot load keypair: " .. opts.keypair) + end + + for _,fname in ipairs(opts.file) do + local sig = rspamd_crypto.sign_file(kp, fname) + + if not sig then + fatal(string.format("cannot sign %s\n", fname)) end - elseif command == 'sign' then - if opts.file then - if type(opts.file) == 'string' then - opts.file = {opts.file} - end - else - parser:error('no files to sign') + local out = string.format('%s.%s', fname, opts.suffix or 'sig') + local of = io.open(out, 'w') + if not of then + fatal('cannot open output to write: ' .. out) end - if not opts.keypair then - parser:error("no keypair specified") + of:write(sig:bin()) + of:close() + io.write(string.format('signed %s -> %s (%s)\n', fname, out, sig:hex())) + end +end + +local function verify_handler(opts) + if opts.file then + if type(opts.file) == 'string' then + opts.file = {opts.file} end + else + parser:error('no files to verify') + end + local pk + local alg = 'curve25519' + + if opts.keypair then local ucl_parser = ucl.parser() local res,err = ucl_parser:parse_file(opts.keypair) if not res then - parser:error(string.format('cannot load %s: %s', opts.keypair, err)) + fatal(string.format('cannot load %s: %s', opts.keypair, err)) end local kp = rspamd_keypair.load(ucl_parser:get_object()) if not kp then - parser:error("cannot load keypair: " .. opts.keypair) + fatal("cannot load keypair: " .. opts.keypair) end - for _,fname in ipairs(opts.file) do - local sig = rspamd_crypto.sign_file(kp, fname) - - if not sig then - parser:error(string.format("cannot sign %s\n", fname)) - end - - local out = string.format('%s.%s', fname, opts.suffix or 'sig') - local of = io.open(out, 'w') - if not of then - parser:error('cannot open output to write: ' .. out) - end - of:write(sig:bin()) - of:close() - io.write(string.format('signed %s -> %s (%s)\n', fname, out, sig:hex())) + pk = kp:pk() + alg = kp:alg() + elseif opts.pubkey then + if opts.nist then alg = 'nist' end + pk = rspamd_pubkey.load(opts.pubkey, 'sign', alg) + elseif opts.pubstr then + if opts.nist then alg = 'nist' end + pk = rspamd_pubkey.create(opts.pubstr, 'sign', alg) + end + + if not pk then + fatal("cannot create pubkey") + end + + local valid = true + + for _,fname in ipairs(opts.file) do + + local sig_fname = string.format('%s.%s', fname, opts.suffix or 'sig') + local sig = rspamd_signature.load(sig_fname, alg) + + if not sig then + fatal(string.format("cannot load signature for %s -> %s", + fname, sig_fname)) end + + if rspamd_crypto.verify_file(pk, sig, fname, alg) then + io.write(string.format('verified %s -> %s (%s)\n', fname, sig_fname, sig:hex())) + else + valid = false + io.write(string.format('FAILED to verify %s -> %s (%s)\n', fname, + sig_fname, sig:hex())) + end + end + + if not valid then + os.exit(1) + end +end + +local function handler(args) + local opts = parser:parse(args) + + local command = opts.command or "generate" + + if command == 'generate' then + generate_handler(opts) + elseif command == 'sign' then + sign_handler(opts) + elseif command == 'verify' then + verify_handler(opts) else parser:error('command %s is not yet implemented', command) end -- 2.39.5