[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [sup-devel] [PATCH] Converted crypto to use the gpgme gem
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 <dmishd@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 <dmishd@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 <dmishd@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 <dmishd@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