好的,在对 OpenSSL 进行了一番探讨之后,我已经了解了以下公钥固定的简单实现。其实很简单。不幸的是,我没有看到流行的 HTTP 中间件库(如 Faraday 和 HTTPClient)提供对 verify_callback 的访问权限,这实际上在每个 OpenSSL 会话中都可用。
在此示例中,如果 PK 与您之前固定的 PK 不匹配,会话将立即终止。请注意,不会使用 OpenSSL::SSL::VERIFY_NONE 调用该块(无论如何都不应该使用它)。
require 'net/http'
require 'openssl'
# Grab the cert received out of band by pigeon post
cert_code = File.read 'github.com.cer'
downloaded_cert = OpenSSL::X509::Certificate.new(cert_code)
# Tells us whether the private keys on the passed certificates match
# and use the same algo
def same_public_key?(ref_cert, actual_cert)
pkr, pka = ref_cert.public_key, actual_cert.public_key
# First check if the public keys use the same crypto...
return false unless pkr.class == pka.class
# ...and then - that they have the same contents
return false unless pkr.to_pem == pka.to_pem
true
end
# Configure a new HTTP object
http = Net::HTTP.new('github.com', 443)
http.use_ssl = true
# We will verify against our CAs in the root store, and with VERIFY_NONE
# the verify_callback will not fire at all, which defeats the purpose.
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
# verify_callback will be called once for every certificate in the chain,
# starting with the top level certificate and ending with the actual certificate
# presented by the server we are contacting. Returning false from that callback
# will terminate the TLS session. Exceptions within the block will be suppressed.
#
# Citing the Ruby OpenSSL docs:
#
# A callback for additional certificate verification. The callback is invoked
# for each certificate in the chain.
#
# The callback is invoked with two values. preverify_ok indicates if the verification
# was passed (true) or not (false). store_context is an OpenSSL::X509::StoreContext
# containing the context used for certificate verification.
#
# If the callback returns false verification is stopped.
http.verify_callback = lambda do | preverify_ok, cert_store |
return false unless preverify_ok
# We only want to verify once, and fail the first time the callback
# is invoked (as opposed to checking only the last time it's called).
# Therefore we get at the whole authorization chain.
# The end certificate is at the beginning of the chain (the certificate
# for the host we are talking to)
end_cert = cert_store.chain[0]
# Only perform the checks if the current cert is the end certificate
# in the chain. We can compare using the DER representation
# (OpenSSL::X509::Certificate objects are not comparable, and for
# a good reason). If we don't we are going to perform the verification
# many times - once per certificate in the chain of trust, which is wasteful
return true unless end_cert.to_der == cert_store.current_cert.to_der
# And verify the public key.
same_public_key?(end_cert, downloaded_cert)
end
# This request will fail if the cert doesn't match
res = http.get '/'
如果您想进行整个证书固定并且证书不受轮换,您可以使用证书指纹:
def same_cert_fingerprint?(ref, actual)
OpenSSL::Digest::SHA256.hexdigest(ref.to_der) == OpenSSL::Digest::SHA256.hexdigest(actual.to_der)
end
编辑:看起来至少 excon 最近实现了这个:
https://github.com/geemus/excon/commit/12437b79bad2a0e51bb4ac5b79c155eb88128245