Skip to content

Instantly share code, notes, and snippets.

@alloy
Created August 17, 2011 12:42
Show Gist options
  • Save alloy/1151454 to your computer and use it in GitHub Desktop.
Save alloy/1151454 to your computer and use it in GitHub Desktop.

Hi,

Based on the code, docs, and an email on this list (http://bit.ly/oSLiox) I’ve started implementing my own certificate verification code, but my experience with OpenSSL is very limited and I’ve run into a few snags so I’d like some feedback on this code.

The code (and a prettier version of this email) is at: https://gist.github.com/1151454

The snags are:

  • Adding a certificate to a OpenSSL::X509::Store which is already in the chain will raise an exception. This is something that can happen quite easily, because:

    • some servers send the same certificate multiple times (cough gmail cough).
    • servers may also send the root certificate, which is already in the store because I initialize it with a list of the root CA certificates.
    • it's impossible to ask the store if a certificate is already in the chain.

    So am I doing this completely wrong? Right now I assume I have the root certificate and ignore duplicates by keeping a reference to the last seen certificate, but without these assumptions/checks the only way I see to do it is by rescueing the exception :'(

  • Is there an existing way to get the hostname that the Connection might have been initialized with? As far as I could tell you can only get the actual address (ip/port)

Cheers, Eloy

def ssl_verify_peer(cert_string)
@last_seen_cert = OpenSSL::X509::Certificate.new(cert_string)
if ca_store.verify(@last_seen_cert)
begin
ca_store.add_cert(@last_seen_cert)
rescue OpenSSL::X509::StoreError => e
# This is so lame, but there appears to be no way to check this in a simple way.
#
# TODO It _might_ be possible to do this with the C api.
raise e unless e.message == 'cert already in hash table'
end
true
else
fail OpenSSL::OpenSSLError.new("unable to verify the server certificate of `#{@account.host}'")
false
end
end
require 'rubygems'
require 'eventmachine'
require 'openssl'
class OpenSSL::X509::Certificate
def ==(other)
other.respond_to?(:to_pem) && to_pem == other.to_pem
end
# A serial *must* be unique for each certificate. Self-signed certificates,
# and thus root CA certificates, have the same `issuer' as `subject'.
def top_level?
serial == serial && issuer.to_s == subject.to_s
end
alias_method :root?, :top_level?
alias_method :self_signed?, :top_level?
end
# Verifies that the peer certificate is a valid chained certificate. That is,
# it's signed by a root CA or a CA signed by a root CA.
#
# This module will also perform hostname verification against the server’s
# certificate, but _only_ if an instance variable called +@hostname+ exists.
module SSLCertificateVerification
class << self
# In PEM format.
#
# Eg: http://curl.haxx.se/docs/caextract.html
attr_accessor :ca_cert_file
end
def ca_store
unless @ca_store
if file = SSLCertificateVerification.ca_cert_file
@ca_store = OpenSSL::X509::Store.new
@ca_store.add_file(file)
else
fail "you must specify a file with root CA certificates as `SSLCertificateVerification.ca_cert_file'"
end
end
@ca_store
end
# It's important that we try to not add a certificate to the store that's
# already in the store, because OpenSSL::X509::Store will raise an exception.
def ssl_verify_peer(cert_string)
cert = OpenSSL::X509::Certificate.new(cert_string)
# Some servers send the same certificate multiple times. I'm not even joking... (gmail.com)
return true if cert == @last_seen_cert
@last_seen_cert = cert
if ca_store.verify(@last_seen_cert)
# A server may send the root certifiacte, which we already have and thus
# should not be added to the store again.
ca_store.add_cert(@last_seen_cert) unless @last_seen_cert.root?
true
else
fail "unable to verify the server certificate of `#{@hostname}'"
false
end
end
def ssl_handshake_completed
if @hostname
unless OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, @hostname)
fail "the hostname `HOSTNAME' does not match the server certificate"
end
else
warn "Skipping hostname verification because `@hostname' is not available."
end
end
end
module Handler
include SSLCertificateVerification
attr_accessor :hostname
def self.connect(hostname, port)
conn = EventMachine.connect(hostname, port, self)
conn.hostname = hostname
conn
end
def post_init
start_tls(:verify_peer => true)
end
def receive_data(data)
super
puts data
unless @sent
send_data "x CAPABILITY\r\n"
@sent = true
end
end
end
SSLCertificateVerification.ca_cert_file = File.expand_path('../cacert.pem', __FILE__)
EventMachine.run do
Handler.connect("imap.gmail.com", 993)
#Handler.connect("imap.mail.yahoo.com", 993)
#Handler.connect("mx.easymail.ca", 993)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment