Search code examples
rubyopensslssl-certificatecertificatecertificate-pinning

why ruby get the cert trust chain is different from gnutls-cli


I found a strange phenomenon when using the command gnutls-cli and ruby code to test the cert pinning of the website. Sometimes the number of certificate trust chains obtained by the two methods is different.

commandline gnutls-cli github-cloud.s3.amazonaws.com will get 4:

(I removed some redundant information)

Certificate[0] info:
subject `CN=*.s3.amazonaws.com'
pin-sha256="hK1awhGE7onU0O+/0pwyTCX1ngEBhLhdNNtD8P11+xY="

Certificate[1] info:
subject `CN=Amazon,OU=Server CA 1B,O=Amazon,C=US'
pin-sha256="JSMzqOOrtyOT1kmau6zKhgT676hGgczD5VMdRMyJZFA="

Certificate[2] info:
subject `CN=Amazon Root CA 1,O=Amazon,C=US'
pin-sha256="++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI="

Certificate[3] info:
subject `CN=Starfield Services Root Certificate Authority - G2,O=Starfield Technologies\
pin-sha256="KwccWaCgrnaw6tsrrSO61FgLacNgG2MMLq8GE6+oP5I="

Using ruby (github-cloud.s3.amazonaws.com):

/CN=*.s3.amazonaws.com
hK1awhGE7onU0O+/0pwyTCX1ngEBhLhdNNtD8P11+xY=
/C=US/O=Amazon/OU=Server CA 1B/CN=Amazon
JSMzqOOrtyOT1kmau6zKhgT676hGgczD5VMdRMyJZFA=
/C=US/O=Amazon/CN=Amazon Root CA 1
++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI=

commandline gnutls-cli www.netflix.com will get 2:

Certificate[0] info:
subject `CN=www.netflix.com,O=Netflix\, Inc.
pin-sha256:3TGagkVvINvo827M04z0YZlg5kctebcod1Qwb83pA0s=

Certificate[1] info:
subject `CN=DigiCert TLS RSA SHA256 2020 CA1,O=DigiCert Inc
pin-sha256="RQeZkB42znUfsDIIFWIRiYEcKl7nHwNFwWCrnMMJbVc="

Using ruby (www.netflix.com):

/C=US/ST=California/L=Los Gatos/O=Netflix, Inc./CN=www.netflix.com
3TGagkVvINvo827M04z0YZlg5kctebcod1Qwb83pA0s=
/C=US/O=DigiCert Inc/CN=DigiCert TLS RSA SHA256 2020 CA1
RQeZkB42znUfsDIIFWIRiYEcKl7nHwNFwWCrnMMJbVc=
/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA
r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=

Here is Ruby Code:

#!/usr/bin/env ruby
require 'colorize'
require 'net/http'
require 'openssl'
require 'base64'

domain = "www.netflix.com"

http = Net::HTTP.new(domain, 443)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.verify_callback = lambda do | preverify_ok, cert_store |
    return false unless preverify_ok
    end_cert = cert_store.chain[0]
    return true unless end_cert.to_der == cert_store.current_cert.to_der
    cert_store.chain.each do |i|
    sha256 = OpenSSL::Digest::SHA256.new
    digest = sha256.digest(i.public_key.to_der)
    spki = Base64.strict_encode64(digest)
    puts i.subject.to_s, spki
    end
    true
end
res = http.get '/'

ruby code reference to Implementing HTTPS certificate/pubkey pinning with Ruby

Thanks!


Solution

  • Let's take www.netflix.com as an example. There are three certificates used in the trust chain:

    1. The certificate for CN=www.netflix.com with the public key PIN 3TGagkVvINvo827M04z0YZlg5kctebcod1Qwb83pA0s=, which is signed by:
    2. The DigiCert intermediary certificate CN=DigiCert TLS RSA SHA256 2020 CA1 with the public key PIN RQeZkB42znUfsDIIFWIRiYEcKl7nHwNFwWCrnMMJbVc=, which is signed by:
    3. The DigiCert Root CA CN=DigiCert Global Root CA with the public key PIN r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=

    #1 & #2 are sent to you by the remote host when you establish a connection to it. #3 exists on your device and is used to verify that #1 & #2 are valid certificates that you can trust.

    When you use OpenSSL inside Net::HTTP to connect to these hosts it is being extra informative and printing the complete certificate chain of trust so that you know what Root CA signed #2. You weren't sent #3 over the wire along with #1 and #2 but it's telling you about it anyway because OpenSSL knows that it's part of the trust chain.

    When you use gnutls-cli www.netflix.com --print-cert </dev/null 2>&1 to connect it is being succinct and printing only #1 and #2 -- the certificates that were sent by the remote host -- and instead telling you:

    Status: The certificate is trusted.

    ... based on it knowing that the Root CA that you have on disk was used to sign the intermediary certificate and that the intermediary certificate was used to sign the Netflix certificate.

    There's nothing different about the connections or the responses received; there's only a difference in what the tools print out when they are run.