使用Ruby实现HTTPS证书/ pubkey固定

我有自己的HTTPS服务,我正在与另一个Ruby应用程序交谈。 我想在我的app repo中的已知时间点保存它的公钥证书,并将服务发送给我的公钥与存储的副本进行比较。 要在外部服务器上安装证书,我可能需要将其转换为某种格式,因此服务器发送的文件不会相同。

我想对那个特定的公钥进行各种修改。 我需要使用OpenSSL比较哪些证书字段来validation从服务收到的PK是否与从服务器收到的PK相同?

我想CN和签名必须至少匹配。 还有什么需要检查才能知道我所拥有的公共证书与我收到的证书完全一致(即证书是否相同)? 也许OSSL有一个内置的设施吗?

好吧,在对OpenSSL进行了一些讨论后,我得出了以下简单的公钥锁定实现。 它实际上非常简单。 不幸的是,我没有看到流行的HTTP中间件库(如Faraday和HTTPClient)提供对每个OpenSSL会话实际可用的verify_callback访问。

在此示例中,如果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

作为@ Julik的答案的后续,RestClient( https://github.com/rest-client/rest-client )支持自1.6.8以来的verify_callback

 # The value of ssl_verify_callback is assigned to Net::HTTP#verify_callback. RestClient::Resource.new(uri, ssl_verify_callback: ...).get