OK, here is a set of 4 patches that implement the change over to the gpgme library. There's quite a bit of work in there so I thought I'd leave it as a few patches, but I have done some tidying.
I have some more ideas for improvements, but I'm happy that this reproduces the behaviour of using the gpg binary, and I find sup usage much smoother with this change. I'll leave it for others to decide whether to stick this in sup 0.12 - I guess it could be risky to stick it in without it being tested, but maybe it could be applied to the next tree, and then moved to main after 0.12 has been released. Hamish Downer > > >> I am also working on having extra information generated when the key > >> is not trusted, but this is not done yet. And I am also working on a > >> hook where you can generate as much information as you want from the > >> signature for the CryptoNotice. Hopefully be ready to submit before > >> the weekend. > > > > This sounds nice. Thanks for your work! > > And thank you for your testing and patience :) > > The github version also has the sig-output hook set up. From the hook text: > > START > Runs when the signature output is being generated, allowing you to > add extra information to your signatures if you want. > > Variables: > signature: the signature object (class is GPGME::Signature) > from_key: the key that generated the signature (class is GPGME::Key) > > Return value: an array of lines of output > END > > I've attached a sample hook file if you want to have a play with it. > I'll document this all on the wiki if it gets accepted. > > I'll give you a few days to find some more problems, but if you fail > to find any then I'll package this up as a single patch and resubmit > it all. > > Hamish
From 09dc4c17600742572c61605f977450d328296964 Mon Sep 17 00:00:00 2001 From: Hamish Downer <dmi...@gmail.com> Date: Fri, 5 Nov 2010 22:30:55 +0000 Subject: [PATCH 1/4] Converted crypto to use the gpgme gem --- bin/sup | 11 +++ lib/sup/crypto.rb | 231 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 141 insertions(+), 101 deletions(-) diff --git a/bin/sup b/bin/sup index 10be161..ad7a0d1 100755 --- a/bin/sup +++ b/bin/sup @@ -10,6 +10,13 @@ rescue LoadError no_ncursesw = true end +no_gpgme = false +begin + require 'gpgme' +rescue LoadError + no_gpgme = true +end + require 'fileutils' require 'trollop' require "sup"; Redwood::check_library_version_against "git" @@ -23,6 +30,10 @@ if no_ncursesw info "No 'ncursesw' gem detected. Install it for wide character support." end +if no_gpgme + info "No 'gpgme' gem detected. Install it for email encryption, decryption and signatures." +end + $opts = Trollop::options do version "sup v#{Redwood::VERSION}" banner <<EOS diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb index c7b57c1..9d21ea0 100644 --- a/lib/sup/crypto.rb +++ b/lib/sup/crypto.rb @@ -1,3 +1,8 @@ +begin + require 'gpgme' +rescue LoadError +end + module Redwood class CryptoManager @@ -11,76 +16,79 @@ class CryptoManager [:encrypt, "Encrypt only"] ) - HookManager.register "gpg-args", <<EOS -Runs before gpg is executed, allowing you to modify the arguments (most + HookManager.register "gpg-options", <<EOS +Runs before gpg is called, allowing you to modify the options (most likely you would want to add something to certain commands, like ---trust-model always to signing/encrypting a message, but who knows). +{:always_trust => true} to encrypting a message, but who knows). Variables: -args: arguments for running GPG +operation: what operation will be done ("sign", "encrypt", "decrypt" or "verify") +options: a dictionary of values to be passed to GPGME -Return value: the arguments for running GPG +Return value: a dictionary to be passed to GPGME EOS def initialize @mutex = Mutex.new - bin = `which gpg`.chomp - @cmd = case bin - when /\S/ - debug "crypto: detected gpg binary in #{bin}" - "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent" - else - debug "crypto: no gpg binary detected" - nil + # test if the gpgme gem is available + @gpgme_present = true + begin + GPGME.check_version({:protocol => GPGME::PROTOCOL_OpenPGP}) + rescue NameError, GPGME::Error + @gpgme_present = false end end - def have_crypto?; !...@cmd.nil? end + def have_crypto?; @gpgme_present end def sign from, to, payload - payload_fn = Tempfile.new "redwood.payload" - payload_fn.write format_payload(payload) - payload_fn.close + return unknown_status(cant_find_gpgme) unless @gpgme_present - sig_fn = Tempfile.new "redwood.signature"; sig_fn.close + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true} + gpg_opts.merge(gen_sign_user_opts(from)) + gpg_opts = HookManager.run("gpg-options", + {:operation => "sign", :options => gpg_opts}) || gpg_opts - sign_user_opts = gen_sign_user_opts from - message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --digest-algo sha256 #{sign_user_opts} #{payload_fn.path}", :interactive => true - unless $?.success? - info "Error while running gpg: #{message}" + begin + sig = GPGME.detach_sign(format_payload(payload), gpg_opts) + rescue GPGME::Error => exc + info "Error while running gpg: #{exc.message}" raise Error, "GPG command failed. See log for details." end envelope = RMail::Message.new - envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha256' + envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature' envelope.add_part payload - signature = RMail::Message.make_attachment IO.read(sig_fn.path), "application/pgp-signature", nil, "signature.asc" + signature = RMail::Message.make_attachment sig, "application/pgp-signature", nil, "signature.asc" envelope.add_part signature envelope end def encrypt from, to, payload, sign=false - payload_fn = Tempfile.new "redwood.payload" - payload_fn.write format_payload(payload) - payload_fn.close - - encrypted_fn = Tempfile.new "redwood.encrypted"; encrypted_fn.close - - recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ") - sign_opts = "" - sign_opts = "--sign --digest-algo sha256 " + gen_sign_user_opts(from) if sign - message = run_gpg "--output #{encrypted_fn.path} --yes --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}", :interactive => true - unless $?.success? - info "Error while running gpg: #{message}" + return unknown_status(cant_find_gpgme) unless @gpgme_present + + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true} + if sign + gpg_opts.merge(gen_sign_user_opts(from)) + gpg_opts.merge({:sign => true}) + end + gpg_opts = HookManager.run("gpg-options", + {:operation => "encrypt", :options => gpg_opts}) || gpg_opts + recipients = to + [from] + + begin + cipher = GPGME.encrypt(recipients, format_payload(payload), gpg_opts) + rescue GPGME::Error => exc + info "Error while running gpg: #{exc.message}" raise Error, "GPG command failed. See log for details." end encrypted_payload = RMail::Message.new encrypted_payload.header["Content-Type"] = "application/octet-stream" encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"' - encrypted_payload.body = IO.read(encrypted_fn.path) + encrypted_payload.body = cipher control = RMail::Message.new control.header["Content-Type"] = "application/pgp-encrypted" @@ -99,70 +107,85 @@ EOS encrypt from, to, payload, true end - def verified_ok? output, rc - output_lines = output.split(/\n/) - - if output =~ /^gpg: (.* signature from .*$)/ - if rc == 0 - Chunk::CryptoNotice.new :valid, $1, output_lines - else - Chunk::CryptoNotice.new :invalid, $1, output_lines + def verified_ok? verify_result + valid = true + unknown = false + output_lines = [] + + verify_result.signatures.each do |signature| + output_lines.push(sig_output_lines(signature)) + output_lines.flatten! + err_code = GPGME::gpgme_err_code(signature.status) + if err_code == GPGME::GPG_ERR_BAD_SIGNATURE + valid = false + elsif err_code != GPGME::GPG_ERR_NO_ERROR + valid = false + unknown = true end - elsif output_lines.length == 0 && rc == 0 - # the message wasn't signed + end + + if output_lines.length == 0 Chunk::CryptoNotice.new :valid, "Encrypted message wasn't signed", output_lines + elsif valid + Chunk::CryptoNotice.new(:valid, simplify_sig_line(verify_result.signatures[0].to_s), output_lines) + elsif !unknown + Chunk::CryptoNotice.new(:invalid, simplify_sig_line(verify_result.signatures[0].to_s), output_lines) else unknown_status output_lines end end def verify payload, signature, detached=true # both RubyMail::Message objects - return unknown_status(cant_find_binary) unless @cmd + return unknown_status(cant_find_gpgme) unless @gpgme_present + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP} + gpg_opts = HookManager.run("gpg-options", + {:operation => "verify", :options => gpg_opts}) || gpg_opts + ctx = GPGME::Ctx.new(gpg_opts) + sig_data = GPGME::Data.from_str signature.decode if detached - payload_fn = Tempfile.new "redwood.payload" - payload_fn.write format_payload(payload) - payload_fn.close - end - - signature_fn = Tempfile.new "redwood.signature" - signature_fn.write signature.decode - signature_fn.close - - if detached - output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}" + signed_text_data = GPGME::Data.from_str(format_payload(payload)) + plain_data = nil else - output = run_gpg "--verify #{signature_fn.path}" + signed_text_data = nil + plain_data = GPGME::Data.empty end - - self.verified_ok? output, $? + begin + ctx.verify(sig_data, signed_text_data, plain_data) + rescue GPGME::Error => exc + return unknown_status exc.message + end + self.verified_ok? ctx.verify_result end ## returns decrypted_message, status, desc, lines def decrypt payload, armor=false # a RubyMail::Message object - return unknown_status(cant_find_binary) unless @cmd - - payload_fn = Tempfile.new(["redwood.payload", ".asc"]) - payload_fn.write payload.to_s - payload_fn.close - - output_fn = Tempfile.new "redwood.output" - output_fn.close - - message = run_gpg "--output #{output_fn.path} --skip-verify --yes --decrypt #{payload_fn.path}", :interactive => true - - unless $?.success? - info "Error while running gpg: #{message}" - return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", message.split("\n")) + return unknown_status(cant_find_gpgme) unless @gpgme_present + + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP} + gpg_opts = HookManager.run("gpg-options", + {:operation => "decrypt", :options => gpg_opts}) || gpg_opts + ctx = GPGME::Ctx.new(gpg_opts) + cipher_data = GPGME::Data.from_str(format_payload(payload)) + plain_data = GPGME::Data.empty + begin + ctx.decrypt_verify(cipher_data, plain_data) + rescue GPGME::Error => exc + info "Error while running gpg: #{exc.message}" + return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", exc.message) end - - output = IO.read output_fn.path + sig = self.verified_ok? ctx.verify_result + plain_data.seek(0, IO::SEEK_SET) + output = plain_data.read output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding + ## TODO: test to see if it is still necessary to do a 2nd run if verify + ## fails. + # ## check for a valid signature in an extra run because gpg aborts if the ## signature cannot be verified (but it is still able to decrypt) - sigoutput = run_gpg "#{payload_fn.path}" - sig = self.verified_ok? sigoutput, $? + #sigoutput = run_gpg "#{payload_fn.path}" + #sig = self.old_verified_ok? sigoutput, $? if armor msg = RMail::Message.new @@ -207,8 +230,8 @@ private Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines end - def cant_find_binary - ["Can't find gpg binary in path."] + def cant_find_gpgme + ["Can't find gpgme gem."] end ## here's where we munge rmail output into the format that signed/encrypted @@ -217,6 +240,28 @@ private payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n") end + # remove the hex key_id and info in () + def simplify_sig_line sig_line + sig_line = sig_line.sub(/from [0-9A-F]{16} /, "from ") + sig_line.sub(/\(.+\) </, "<") + end + + def sig_output_lines signature + time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") + + " using key ID " + signature.fingerprint[-8..-1] + first_sig = signature.to_s.sub(/from [0-9A-F]{16} /, 'from "') + '"' + output_lines = [time_line, first_sig] + + ctx = GPGME::Ctx.new + if from_key = ctx.get_key(signature.fingerprint) + if from_key.uids.length > 1 + aka_list = from_key.uids[1..-1] + aka_list.each { |aka| output_lines << ' aka "' + aka.uid + '"' } + end + end + output_lines + end + # logic is: # if gpgkey set for this account, then use that # elsif only one account, then leave blank so gpg default will be user @@ -224,30 +269,14 @@ private def gen_sign_user_opts from account = AccountManager.account_for from if !account.gpgkey.nil? - opts = "--local-user '#{account.gpgkey}'" + opts = {:signers => account.gpgkey} elsif AccountManager.user_emails.length == 1 # only one account - opts = "" + opts = {} else - opts = "--local-user '#{from}'" + opts = {:signers => from} end opts end - - def run_gpg args, opts={} - args = HookManager.run("gpg-args", { :args => args }) || args - cmd = "LC_MESSAGES=C #...@cmd} #{args}" - if opts[:interactive] && BufferManager.instantiated? - output_fn = Tempfile.new "redwood.output" - output_fn.close - cmd += " > #{output_fn.path} 2> /dev/null" - debug "crypto: running: #{cmd}" - BufferManager.shell_out cmd - IO.read(output_fn.path) rescue "can't read output" - else - debug "crypto: running: #{cmd}" - `#{cmd} 2> /dev/null` - end - end end end -- 1.7.1
From 6615a325d8640b53e4f5bda3161d8ad56f9c1ed6 Mon Sep 17 00:00:00 2001 From: Hamish Downer <dmi...@gmail.com> Date: Mon, 8 Nov 2010 22:31:01 +0000 Subject: [PATCH 2/4] catch exception when no public key present --- lib/sup/crypto.rb | 16 ++++++++++++---- 1 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb index 9d21ea0..83176d9 100644 --- a/lib/sup/crypto.rb +++ b/lib/sup/crypto.rb @@ -247,19 +247,27 @@ private end def sig_output_lines signature + # It appears that the signature.to_s call can lead to a EOFError if + # the key is not found. So start by looking for the key. + ctx = GPGME::Ctx.new + begin + from_key = ctx.get_key(signature.fingerprint) + first_sig = signature.to_s.sub(/from [0-9A-F]{16} /, 'from "') + '"' + rescue EOFError => error + first_sig = "No public key available for #{signature.fingerprint}" + end + time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") + " using key ID " + signature.fingerprint[-8..-1] - first_sig = signature.to_s.sub(/from [0-9A-F]{16} /, 'from "') + '"' output_lines = [time_line, first_sig] - ctx = GPGME::Ctx.new - if from_key = ctx.get_key(signature.fingerprint) + if from_key if from_key.uids.length > 1 aka_list = from_key.uids[1..-1] aka_list.each { |aka| output_lines << ' aka "' + aka.uid + '"' } end end - output_lines + output_lines.flatten! end # logic is: -- 1.7.1
From 07f12934700cd8d85132d75b307019625cd17076 Mon Sep 17 00:00:00 2001 From: Hamish Downer <dmi...@gmail.com> Date: Tue, 16 Nov 2010 20:58:01 +0000 Subject: [PATCH 3/4] improved signature messages --- lib/sup/crypto.rb | 31 ++++++++++++++++++++++++++----- 1 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb index 83176d9..88228ff 100644 --- a/lib/sup/crypto.rb +++ b/lib/sup/crypto.rb @@ -242,8 +242,7 @@ private # remove the hex key_id and info in () def simplify_sig_line sig_line - sig_line = sig_line.sub(/from [0-9A-F]{16} /, "from ") - sig_line.sub(/\(.+\) </, "<") + sig_line.sub(/from [0-9A-F]{16} /, "from ") end def sig_output_lines signature @@ -253,21 +252,43 @@ private begin from_key = ctx.get_key(signature.fingerprint) first_sig = signature.to_s.sub(/from [0-9A-F]{16} /, 'from "') + '"' - rescue EOFError => error + rescue EOFError + from_key = nil first_sig = "No public key available for #{signature.fingerprint}" end time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") + - " using key ID " + signature.fingerprint[-8..-1] + " using " + key_type(from_key, signature.fingerprint) + + "key ID " + signature.fingerprint[-8..-1] output_lines = [time_line, first_sig] if from_key + # first list all the uids if from_key.uids.length > 1 aka_list = from_key.uids[1..-1] aka_list.each { |aka| output_lines << ' aka "' + aka.uid + '"' } end + + # now we want to look at the trust of that key + if signature.validity != GPGME::GPGME_VALIDITY_FULL && signature.validity != GPGME::GPGME_VALIDITY_MARGINAL + output_lines << "WARNING: This key is not certified with a trusted signature!" + output_lines << "There is no indication that the signature belongs to the owner" + end + end + output_lines + end + + def key_type key, fpr + return "" if key.nil? + subkey = key.subkeys.find {|subkey| subkey.fpr == fpr || subkey.keyid == fpr } + return "" if subkey.nil? + + case subkey.pubkey_algo + when GPGME::PK_RSA then "RSA " + when GPGME::PK_DSA then "DSA " + when GPGME::PK_ELG then "ElGamel " + when GPGME::PK_ELG_E then "ElGamel " end - output_lines.flatten! end # logic is: -- 1.7.1
From 71bae56903351adf43f6a7ebdd9766764f302f16 Mon Sep 17 00:00:00 2001 From: Hamish Downer <dmi...@gmail.com> Date: Tue, 16 Nov 2010 22:54:06 +0000 Subject: [PATCH 4/4] added signature output hook --- lib/sup/crypto.rb | 15 +++++++++++++++ 1 files changed, 15 insertions(+), 0 deletions(-) diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb index 88228ff..b9ffb17 100644 --- a/lib/sup/crypto.rb +++ b/lib/sup/crypto.rb @@ -28,6 +28,17 @@ options: a dictionary of values to be passed to GPGME Return value: a dictionary to be passed to GPGME EOS + HookManager.register "sig-output", <<EOS +Runs when the signature output is being generated, allowing you to +add extra information to your signatures if you want. + +Variables: +signature: the signature object (class is GPGME::Signature) +from_key: the key that generated the signature (class is GPGME::Key) + +Return value: an array of lines of output +EOS + def initialize @mutex = Mutex.new @@ -274,6 +285,10 @@ private output_lines << "WARNING: This key is not certified with a trusted signature!" output_lines << "There is no indication that the signature belongs to the owner" end + + # finally, run the hook + output_lines << HookManager.run("sig-output", + {:signature => signature, :from_key => from_key}) end output_lines end -- 1.7.1
_______________________________________________ Sup-devel mailing list Sup-devel@rubyforge.org http://rubyforge.org/mailman/listinfo/sup-devel