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