class DkimVerify::Verification::Verifier
Public Class Methods
new(email_stringy_thing)
click to toggle source
# File dkimverify.rb, line 110 def initialize(email_stringy_thing) mail = Mail::Message.new(email_stringy_thing) @headers = mail.headers @body = mail.body end
Public Instance Methods
verify!()
click to toggle source
# File dkimverify.rb, line 117 def verify! return false if @headers.get("DKIM-Signature").nil? dkim_signature_str = @headers.get("DKIM-Signature").to_s @dkim_signature = Verification.parse_header_kv(dkim_signature_str) validate_signature! # just checking to make sure we have all the ingredients we need to actually verify the signature figure_out_canonicalization_methods! verify_body_hash! # 'b=' is the signed message headers' hash. # we need to decrypt the 'b=' value (with the public key) # and compare it with the computed headers_hash. # decrypted_header_hash is the "expected" value. my_headers_hash = headers_hash my_decrypted_header_hash = decrypted_header_hash raise DkimVerificationFailure.new("header hash signatures sizes don't match") if my_decrypted_header_hash.size != my_headers_hash.size # Byte-by-byte compare of signatures does_signature_match = my_decrypted_header_hash.bytes.zip(my_headers_hash.bytes).all?{|exp, got| exp == got } raise DkimVerificationFailure.new("header hash signatures don't match. expected #{my_decrypted_header_hash}, got #{my_headers_hash}") unless does_signature_match return does_signature_match # always true, but this is a good guarantee of somebody accidentally refactoring this to always return true end
Private Instance Methods
decrypted_header_hash()
click to toggle source
# File dkimverify.rb, line 256 def decrypted_header_hash begin decrypted_header_hash_bytes = OpenSSL::PKey::RSA.new(public_key).public_decrypt(Base64.decode64(@dkim_signature['b'])) rescue OpenSSL::PKey::RSAError raise DkimPermFail.new "couldn't decrypt header hash with public key" end ret = Base64.encode64(decrypted_header_hash_bytes).gsub(/\s+/, '') $debuglog.puts "decrypted_header_hash: #{ret}" unless $debuglog.nil? ret end
figure_out_canonicalization_methods!()
click to toggle source
here we're figuring out the canonicalization algorithm for the body and for the headers
# File dkimverify.rb, line 173 def figure_out_canonicalization_methods! c_match = @dkim_signature['c'].match(/(\w+)(?:\/(\w+))?$/) if not c_match $debuglog.puts "can't figure out canonicalization ('c=')" return false end @how_to_canonicalize_headers = c_match[1] if c_match[2] @how_to_canonicalize_body = c_match[2] else @how_to_canonicalize_body = "simple" end raise ArgumentError, "invalid canonicalization method for headers" unless ["relaxed", "simple"].include?(@how_to_canonicalize_headers) raise ArgumentError, "invalid canonicalization method for body" unless ["relaxed", "simple"].include?(@how_to_canonicalize_body) end
headers_digest()
click to toggle source
# File dkimverify.rb, line 224 def headers_digest hasher = if @dkim_signature['a'] == "rsa-sha1" Digest::SHA1 elsif @dkim_signature['a'] == "rsa-sha256" Digest::SHA256 else raise InvalidDkimSignature.new "couldn't figure out the right algorithm to use" end.new headers_to_sign.each do |header| hasher.update(header[0]) hasher.update(":") hasher.update(header[1]) end digest = hasher.digest $debuglog.puts "verify digest: #{ digest.each_byte.map { |b| b.to_s(16) }.join ' ' }" unless $debuglog.nil? digest end
headers_hash()
click to toggle source
# File dkimverify.rb, line 243 def headers_hash dinfo = OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Sequence.new([ @hashid, OpenSSL::ASN1::Null.new(nil), ]), OpenSSL::ASN1::OctetString.new(headers_digest), ]) headers_der = Base64.encode64(dinfo.to_der).gsub(/\s+/, '') $debuglog.puts "headers_hash: #{headers_der}" unless $debuglog.nil? headers_der end
headers_to_sign()
click to toggle source
# File dkimverify.rb, line 201 def headers_to_sign # we figure out which headers we care about, then canonicalize them header_fields_to_include = @dkim_signature['h'].split(/\s*:\s*/) $debuglog.puts "header_fields_to_include: #{header_fields_to_include}" unless $debuglog.nil? canonicalized_headers = [] header_fields_to_include_with_values = header_fields_to_include.map do |header_name| header_val = (hstr = @headers.get(header_name)).nil? ? '' : hstr #.split(":")[1..-1].join(":") [header_name, header_val ] end canonicalized_headers = Verification.canonicalize_headers(header_fields_to_include_with_values, @how_to_canonicalize_headers) canonicalized_headers += Verification.canonicalize_headers([ [ @headers.get_name("DKIM-Signature").to_s, @headers.get("DKIM-Signature").to_s.split(@dkim_signature['b']).join('') ] ], @how_to_canonicalize_headers).map{|x| [x[0], x[1].rstrip()] } $debuglog.puts "verify headers: #{canonicalized_headers}" unless $debuglog.nil? canonicalized_headers end
public_key()
click to toggle source
# File dkimverify.rb, line 189 def public_key # here we're getting the website's actual public key from the DNS system # s = dnstxt(sig['s']+"._domainkey."+sig['d']+".") # dkim_record_from_dns = DKIM::Query::Domain.query(@dkim_signature['d'], {:selectors => [@dkim_signature['s']]}).keys[@dkim_signature['s']] txt = Resolv::DNS.open{|dns| dns.getresources("#{@dkim_signature['s']}._domainkey.#{@dkim_signature['d']}", Resolv::DNS::Resource::IN::TXT).map(&:data) } raise DkimTempFail.new("couldn't get public key from DNS system for #{@dkim_signature['s']}/#{@dkim_signature['d']}") if txt.first.nil? parsed_txt = Verification.parse_header_kv(txt.first) raise DkimTempFail.new("couldn't get public key from DNS system for #{@dkim_signature['s']}/#{@dkim_signature['d']}") if !parsed_txt.keys.include?("p") publickey_asn1 = OpenSSL::ASN1.decode(Base64.decode64(parsed_txt["p"])) publickey = publickey_asn1.value[1].value end
validate_signature!()
click to toggle source
# File dkimverify.rb, line 267 def validate_signature! # version: only version 1 is defined raise InvalidDkimSignature.new("DKIM signature is missing required tag v=") unless @dkim_signature.include?('v') raise InvalidDkimSignature.new("DKIM signature v= value is invalid (got \"#{@dkim_signature['v']}\"; expected \"1\")") unless @dkim_signature['v'] == "1" # encryption algorithm raise InvalidDkimSignature.new("DKIM signature is missing required tag a=") unless @dkim_signature.include?('a') # header hash raise InvalidDkimSignature.new("DKIM signature is missing required tag b=") unless @dkim_signature.include?('b') raise InvalidDkimSignature.new("DKIM signature b= value is not valid base64") unless @dkim_signature['b'].match(/[\s0-9A-Za-z+\/]+=*$/) raise InvalidDkimSignature.new("DKIM signature is missing required tag h=") unless @dkim_signature.include?('h') # body hash (not directly encrypted) raise InvalidDkimSignature.new("DKIM signature is missing required tag bh=") unless @dkim_signature.include?('bh') raise InvalidDkimSignature.new("DKIM signature bh= value is not valid base64") unless @dkim_signature['bh'].match(/[\s0-9A-Za-z+\/]+=*$/) # domain selector raise InvalidDkimSignature.new("DKIM signature is missing required tag d=") unless @dkim_signature.include?('d') raise InvalidDkimSignature.new("DKIM signature is missing required tag s=") unless @dkim_signature.include?('s') # these are expiration dates, which are not checked above. raise InvalidDkimSignature.new("DKIM signature t= value is not a valid decimal integer") unless @dkim_signature['t'].nil? || @dkim_signature['t'].match(/\d+$/) raise InvalidDkimSignature.new("DKIM signature x= value is not a valid decimal integer") unless @dkim_signature['x'].nil? || @dkim_signature['x'].match(/\d+$/) raise InvalidDkimSignature.new("DKIM signature x= value is less than t= (and must be greater than or equal to t=). (x=#{@dkim_signature['x']}, t=#{@dkim_signature['t']}) ") unless @dkim_signature['x'].nil? || @dkim_signature['x'].to_i >= @dkim_signature['t'].to_i # other unimplemented stuff raise InvalidDkimSignature.new("DKIM signature i= domain is not a subdomain of d= (i=#{@dkim_signature[i]} d=#{@dkim_signature[d]})") if @dkim_signature['i'] && !(@dkim_signature['i'].end_with?(@dkim_signature['d']) || ["@", ".", "@."].include?(@dkim_signature['i'][-@dkim_signature['d'].size-1])) raise InvalidDkimSignature.new("DKIM signature l= value is invalid") if @dkim_signature['l'] && !@dkim_signature['l'].match(/\d{,76}$/) raise InvalidDkimSignature.new("DKIM signature q= value is invalid (got \"#{@dkim_signature['q']}\"; expected \"dns/txt\")") if @dkim_signature['q'] && @dkim_signature['q'] != "dns/txt" end
verify_body_hash!()
click to toggle source
# File dkimverify.rb, line 145 def verify_body_hash! # here we're figuring out what algorithm to use for computing the signature hasher, @hashid = if @dkim_signature['a'] == "rsa-sha1" [Digest::SHA1, HASHID_SHA1] elsif @dkim_signature['a'] == "rsa-sha256" [Digest::SHA256, HASHID_SHA256] else $debuglog.puts "couldn't figure out the right algorithm to use" exit 1 end body = Verification.canonicalize_body(@body, @how_to_canonicalize_body) bodyhash = hasher.digest(body) $debuglog.puts "bh: #{Base64.encode64(bodyhash)}" unless $debuglog.nil? if bodyhash != Base64.decode64(@dkim_signature['bh'].gsub(/\s+/, '')) error_msg = "body hash mismatch (got #{Base64.encode64(bodyhash)}, expected #{@dkim_signature['bh']})" $debuglog.puts error_msg unless $debuglog.nil? raise DkimVerificationFailure.new(error_msg) end nil end