Skip to content

Commit

Permalink
refactor signature verification
Browse files Browse the repository at this point in the history
* add Mail::Message#verify that works like decrypt by returning a new
  Message instance with verification results but without the raw signature data
* add tests for inline signed / mime signed, multipart / not mp messages
  • Loading branch information
jkraemer committed Jul 16, 2014
1 parent ff2adc3 commit 0e0bdae
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 53 deletions.
24 changes: 19 additions & 5 deletions README.md
Expand Up @@ -119,14 +119,28 @@ Receive the mail as usual. Check if it is signed using the `signed?` method. Che
```ruby
mail = Mail.first
if !mail.encrypted? && mail.signed?
# do not call signed on encrypted mails. The signature on encrypted mails
# must be checked by setting the :verify option when decrypting
puts "signature(s) valid: #{mail.signature_valid?}"
verified = mail.verify
puts "signature(s) valid: #{verified.signature_valid?}"
puts "message signed by: #{verified.signatures.map{|sig|sig.from}.join("\n")}"
end
```

Note that for encrypted mails the signatures can not be checked using these methods. For encrypted mails
the `:verify` option for the `decrypt` operation must be used instead.
Note that for encrypted mails the signatures can not be checked using these
methods. For encrypted mails the `:verify` option for the `decrypt` operation
must be used instead:

```ruby
if mail.encrypted?
decrypted = mail.decrypt(verify: true, password: 's3cr3t')
puts "signature(s) valid: #{decrypted.signature_valid?}"
puts "message signed by: #{decrypted.signatures.map{|sig|sig.from}.join("\n")}"
end
```

It's important to actually use the information contained in the `signatures`
array to check if the message really has been signed by the person that you (or
your users) think is the sender - usually by comparing the key id of the
signature with the key id of the expected sender.

### Key import from public key servers

Expand Down
20 changes: 12 additions & 8 deletions lib/mail/gpg.rb
Expand Up @@ -12,6 +12,8 @@
require 'mail/gpg/message_patch'
require 'mail/gpg/rails'
require 'mail/gpg/signed_part'
require 'mail/gpg/mime_signed_message'
require 'mail/gpg/inline_signed_message'

module Mail
module Gpg
Expand Down Expand Up @@ -149,6 +151,16 @@ def self.decrypt_pgp_inline(encrypted_mail, options)
InlineDecryptedMessage.new(encrypted_mail, options)
end

def self.verify(signed_mail, options = {})
if signed_mime?(signed_mail)
Mail::Gpg::MimeSignedMessage.new signed_mail, options
elsif signed_inline?(signed_mail)
Mail::Gpg::InlineSignedMessage.new signed_mail, options
else
signed_mail
end
end

# check signature for PGP/MIME (RFC 3156, section 5) signed mail
def self.signature_valid_pgp_mime?(signed_mail, options)
# MUST contain exactly two body parts
Expand All @@ -164,7 +176,6 @@ def self.signature_valid_pgp_mime?(signed_mail, options)
def self.signature_valid_inline?(signed_mail, options)
result = nil
if signed_mail.multipart?

signed_mail.parts.each do |part|
if signed_inline?(part)
if result.nil?
Expand All @@ -182,13 +193,6 @@ def self.signature_valid_inline?(signed_mail, options)
return result
end

INLINE_SIGNED_MARKER_RE = Regexp.new('^-----(BEGIN|END) PGP SIGNED MESSAGE-----$(\s*Hash: \w+$)?', Regexp::MULTILINE)
INLINE_SIG_RE = Regexp.new('^-----BEGIN PGP SIGNATURE-----\s*$.*^-----END PGP SIGNATURE-----\s*$', Regexp::MULTILINE)
# utility method to remove inline signature and related pgp markers
def self.strip_inline_signature(signed_text)
signed_text.gsub(INLINE_SIGNED_MARKER_RE, '').gsub(INLINE_SIG_RE, '').strip
end


# check if PGP/MIME encrypted (RFC 3156)
def self.encrypted_mime?(mail)
Expand Down
72 changes: 72 additions & 0 deletions lib/mail/gpg/inline_signed_message.rb
@@ -0,0 +1,72 @@
require 'mail/gpg/verified_part'

module Mail
module Gpg
class InlineSignedMessage < Mail::Message

def initialize(signed_mail, options = {})
if signed_mail.multipart?
super() do
global_verify_result = []
signed_mail.header.fields.each do |field|
header[field.name] = field.value
end
signed_mail.parts.each do |part|
if Mail::Gpg.signed_inline?(part)
signed_text = part.body.to_s
success, vr = GpgmeHelper.inline_verify(signed_text, options)
p = VerifiedPart.new(part)
if success
p.body self.class.strip_inline_signature signed_text
end
p.verify_result vr
global_verify_result << vr
add_part p
else
add_part part
end
end
verify_result global_verify_result
end # of multipart
else
super() do
signed_mail.header.fields.each do |field|
header[field.name] = field.value
end
signed_text = signed_mail.body.to_s
success, vr = GpgmeHelper.inline_verify(signed_text, options)
if success
body self.class.strip_inline_signature signed_text
else
body signed_text
end
verify_result vr
end
end
end

END_SIGNED_TEXT = '-----END PGP SIGNED MESSAGE-----'
END_SIGNED_TEXT_RE = /^#{END_SIGNED_TEXT}\s*$/
INLINE_SIG_RE = Regexp.new('^-----BEGIN PGP SIGNATURE-----\s*$.*^-----END PGP SIGNATURE-----\s*$', Regexp::MULTILINE)
BEGIN_SIG_RE = /^(-----BEGIN PGP SIGNATURE-----)\s*$/


# utility method to remove inline signature and related pgp markers
def self.strip_inline_signature(signed_text)
if signed_text =~ INLINE_SIG_RE
signed_text = signed_text.dup
if signed_text !~ END_SIGNED_TEXT_RE
# insert the 'end of signed text' marker in case it is missing
signed_text = signed_text.gsub BEGIN_SIG_RE, "-----END PGP SIGNED MESSAGE-----\n\\1"
end
signed_text.gsub! INLINE_SIG_RE, ''
signed_text.strip!
end
signed_text
end

end
end
end


16 changes: 11 additions & 5 deletions lib/mail/gpg/message_patch.rb
Expand Up @@ -53,6 +53,7 @@ def gpg(options = nil)
end
end

# true if this mail is encrypted
def encrypted?
Mail::Gpg.encrypted?(self)
end
Expand All @@ -65,17 +66,22 @@ def decrypt(options = {})
Mail::Gpg.decrypt(self, options)
end

# true if this mail is signed (but not encrypted)
def signed?
Mail::Gpg.signed?(self)
end

# checks validity of signatures (true / false)
# verify signatures. returns a new mail object with signatures removed and
# populated verify_result.
#
# after calling this, you can gain access the gpgme verification result
# via the verify_result method.
def signature_valid?(options = {})
Mail::Gpg.signature_valid?(self, options)
# verified = signed_mail.verify()
# verified.signature_valid?
# signers = mail.signatures.map{|sig| sig.from}
def verify(options = {})
Mail::Gpg.verify(self, options)
end


end
end
end
Expand Down
30 changes: 30 additions & 0 deletions lib/mail/gpg/mime_signed_message.rb
@@ -0,0 +1,30 @@
require 'mail/gpg/verified_part'

module Mail
module Gpg
class MimeSignedMessage < Mail::Message

def initialize(signed_mail, options = {})
content_part, signature = signed_mail.parts
success, vr = SignPart.verify_signature(content_part, signature, options)
super() do
verify_result vr
signed_mail.header.fields.each do |field|
header[field.name] = field.value
end
content_part.header.fields.each do |field|
header[field.name] = field.value
end
if content_part.multipart?
content_part.parts.each{|part| add_part part}
else
body content_part.body.to_s
end
end
end
end
end
end



14 changes: 14 additions & 0 deletions lib/mail/gpg/verify_result_attribute.rb
@@ -1,6 +1,7 @@
module Mail
module Gpg
module VerifyResultAttribute

# the result of signature verification, as provided by GPGME
def verify_result(result = nil)
if result
Expand All @@ -12,6 +13,19 @@ def verify_result(result = nil)
def verify_result=(result)
@verify_result = result
end

# checks validity of signatures (true / false)
def signature_valid?
sigs = self.signatures
sigs.any? && sigs.detect{|s|!s.valid?}.blank?

This comment has been minimized.

Copy link
@mashedcode

mashedcode Apr 12, 2017

Shouldn't this be .nil? instead of .blank??

This comment has been minimized.

Copy link
@jkraemer

jkraemer Apr 13, 2017

Author Owner

indeed. Guess this never surfaced because when Rails is loaded, nil.blank? is a thing.

This comment has been minimized.

Copy link
@mashedcode

mashedcode Apr 16, 2017

#47 I need this to be fixed for some application I'm working on.

end

# list of all signatures from verify_result
def signatures
[verify_result].flatten.compact.map do |vr|
vr.signatures
end.flatten.compact
end
end
end
end
21 changes: 13 additions & 8 deletions test/gpg_test.rb
Expand Up @@ -36,8 +36,11 @@ def check_signature(mail = @mail, signed = @signed)
assert sig.valid?
end
assert Mail::Gpg.signature_valid?(signed)
assert signed.verify_result.present?
assert signed.verify_result.signatures.any?
assert verified = signed.verify
assert verified.verify_result.present?
assert verified.verify_result.signatures.any?
assert verified.signatures.any?
assert verified.signature_valid?
end

def check_mime_structure_signed(mail = @mail, signed = @signed)
Expand Down Expand Up @@ -252,9 +255,9 @@ def check_headers_signed(mail = @mail, signed = @signed)
should 'decrypt and verify' do
assert mail = Mail::Gpg.decrypt(@encrypted, { :verify => true, :password => 'abc' })
assert mail == @mail
assert vr = mail.verify_result
assert sig = vr.signatures.first
assert sig.to_s=~ /Joe/
assert mail.verify_result
assert sig = mail.signatures.first
assert sig.to_s =~ /Joe/
assert sig.valid?
end
end
Expand Down Expand Up @@ -365,9 +368,11 @@ def check_headers_signed(mail = @mail, signed = @signed)
assert mail = Mail::Gpg.decrypt(@encrypted, { :verify => true, :password => 'abc' })
assert mail == @mail
assert mail.parts[1] == @mail.parts[1]
assert vr = mail.verify_result
assert sig = vr.signatures.first
assert sig.to_s=~ /Joe/
assert mail.verify_result
assert signatures = mail.signatures
assert_equal 1, signatures.size
assert sig = signatures[0]
assert sig.to_s =~ /Joe/
assert sig.valid?
end
end
Expand Down
45 changes: 24 additions & 21 deletions test/inline_signed_message_test.rb
Expand Up @@ -18,13 +18,13 @@ class InlineSignedMessageTest < Test::Unit::TestCase
context 'strip_inline_signature' do
should 'strip signature from signed text' do
body = self.class.inline_sign(@mail, 'i am signed')
assert stripped_body = Mail::Gpg.strip_inline_signature(body)
assert_equal 'i am signed', stripped_body
assert stripped_body = Mail::Gpg::InlineSignedMessage.strip_inline_signature(body)
assert_equal "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA1\n\ni am signed\n-----END PGP SIGNED MESSAGE-----", stripped_body
end

should 'not change unsigned text' do
assert stripped_body = Mail::Gpg.strip_inline_signature("foo\nbar\n")
assert_equal "foo\nbar", stripped_body
assert stripped_body = Mail::Gpg::InlineSignedMessage.strip_inline_signature("foo\nbar\n")
assert_equal "foo\nbar\n", stripped_body
end
end

Expand All @@ -34,9 +34,9 @@ class InlineSignedMessageTest < Test::Unit::TestCase
mail.body = self.class.inline_sign(mail, mail.body.to_s)
assert !mail.multipart?
assert mail.signed?
assert mail.signature_valid?
assert vr = mail.verify_result
assert sig = vr.signatures.first
assert verified = mail.verify
assert verified.signature_valid?
assert sig = verified.signatures.first
assert sig.to_s=~ /Joe/
assert sig.valid?
end
Expand All @@ -46,9 +46,10 @@ class InlineSignedMessageTest < Test::Unit::TestCase
mail.body = self.class.inline_sign(mail, mail.body.to_s).gsub /i am/, 'i was'
assert !mail.multipart?
assert mail.signed?
assert !mail.signature_valid?
assert vr = mail.verify_result
assert sig = vr.signatures.first
assert verified = mail.verify
assert !verified.signature_valid?
assert vr = verified.verify_result
assert sig = verified.signatures.first
assert sig.to_s=~ /Joe/
assert !sig.valid?
end
Expand All @@ -64,13 +65,14 @@ class InlineSignedMessageTest < Test::Unit::TestCase
end
assert mail.multipart?
assert mail.signed?
assert mail.signature_valid?
assert vr = mail.parts.last.verify_result
assert !mail.parts.first.signed?
assert mail.parts.last.signed?
assert Mail::Gpg.signed_inline?(mail.parts.last)
assert_equal [vr], mail.verify_result
assert sig = vr.signatures.first
assert verified = mail.verify
assert verified.signature_valid?
assert vr = verified.parts.last.verify_result
assert !verified.parts.first.signed?
assert verified.parts.last.signed?
assert Mail::Gpg.signed_inline?(verified.parts.last)
assert_equal [vr], verified.verify_result
assert sig = verified.signatures.first
assert sig.to_s=~ /Joe/
assert sig.valid?
end
Expand All @@ -87,17 +89,18 @@ class InlineSignedMessageTest < Test::Unit::TestCase

assert mail.multipart?
assert mail.signed?
assert !mail.signature_valid?
assert vr = mail.verify_result
assert verified = mail.verify
assert !verified.signature_valid?
assert vr = verified.verify_result
assert_equal 2, vr.size

invalid = mail.parts[1]
invalid = verified.parts[1]
assert !invalid.signature_valid?
assert sig = invalid.verify_result.signatures.first
assert sig.to_s=~ /Joe/
assert !sig.valid?

valid = mail.parts[2]
valid = verified.parts[2]
assert valid.signature_valid?
assert sig = valid.verify_result.signatures.first
assert sig.to_s=~ /Joe/
Expand Down

0 comments on commit 0e0bdae

Please sign in to comment.