pythonsslssl-certificateroot-certificate

Is Google returning different certificates for different clients?


for a class I'm looking at the TLS certificate chain for google.com. When I click in the Chrome or Firefox browser, the root certificate is shown as GTS Root R1 with a validity up to 2036, self-signed, so it must be a root-certificate.

However, if I check the same in Python using the following code, I get a GTS Root R1 certificate with a validity of 2028, which is signed by GlobalSign nv-sa, so this time it's NOT a root-certificate!

Is it possible that Google.com returns two different certificate chains, depending on which client does the request? If it supposes that the client accepts the GTS Root R1 as root certificate, it returns this one, else it returns one signed by GlobalSign nv-sa?

If so, why?

The following is the chain with the certificates and their digest/sha256. Now when I look at the certificate chain in the browser, the first two have the same digest / sha-256, but the third one has a different digest. So I definitely think I'm getting a different chain depending on the client...

Certificate #0
Subject b'CN': b'*.google.com'
notBefore: b'20211101021952Z'
notAfter: b'20220124021951Z'
version:2
sigAlg: b'sha256WithRSAEncryption'
digest: b'E9:7C:86:18:34:DE:F4:11:4D:2D:5E:6F:1A:49:22:A1:04:EE:9E:7C:8D:CB:72:3F:6D:67:58:8F:7E:F3:4B:AB'
issuer: <X509Name object '/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3'>

Certificate #1
Subject b'C': b'US'
Subject b'O': b'Google Trust Services LLC'
Subject b'CN': b'GTS CA 1C3'
notBefore: b'20200813000042Z'
notAfter: b'20270930000042Z'
version:2
sigAlg: b'sha256WithRSAEncryption'
digest: b'23:EC:B0:3E:EC:17:33:8C:4E:33:A6:B4:8A:41:DC:3C:DA:12:28:1B:BC:3F:F8:13:C0:58:9D:6C:C2:38:75:22'
issuer: <X509Name object '/C=US/O=Google Trust Services LLC/CN=GTS Root R1'>

Certificate #2
Subject b'C': b'US'
Subject b'O': b'Google Trust Services LLC'
Subject b'CN': b'GTS Root R1'
notBefore: b'20200619000042Z'
notAfter: b'20280128000042Z'
version:2
sigAlg: b'sha256WithRSAEncryption'
digest: b'3E:E0:27:8D:F7:1F:A3:C1:25:C4:CD:48:7F:01:D7:74:69:4E:6F:C5:7E:0C:D9:4C:24:EF:D7:69:13:39:18:E5'
issuer: <X509Name object '/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA'>

The python code I used to fetch the certificate:

from OpenSSL import SSL, crypto
import socket, certifi

def dump_cert(cert):
    for component in cert.get_subject().get_components():
        print("Subject %s: %s" % (component))
             
    print("notBefore:", cert.get_notBefore())
    print("notAfter:", cert.get_notAfter())
    print("version:" + str(cert.get_version()))
    print("sigAlg:", cert.get_signature_algorithm())
    print("digest:", cert.digest('sha256'))
    print("issuer:", cert.get_issuer())
    print()
    
def get_connection_chain(host, port = 443):
    dst = (str.encode(host), port)
    ctx = SSL.Context(SSL.TLSv1_2_METHOD)
    s = socket.create_connection(dst)
    s = SSL.Connection(ctx, s)
    s.set_connect_state()
    s.set_tlsext_host_name(dst[0])

    s.sendall(b'HEAD / HTTP/1.2\n\n')
    s.recv(16)
    return (s, s.get_peer_cert_chain())

def dump_chain(chain):
    for pos, cert in enumerate(chain):
        print("Certificate #" + str(pos))
        dump_cert(cert)

conn, chain = get_connection_chain("google.ch")
dump_chain(chain)

Solution

  • So thanks to President James K. Polk I think I better understand what's happening:

    So the browser receives the certificate chain as given in the question. Then it starts by the leaf node of the certificate chain, which is the Certificate #0. The browser goes through its list of stored root certificates to validate the leaf certificate. If it doesn't find one, it goes to the next entry, Certificate #1.

    In the example of google.com, it finds that it has a root certificate for the Certificate #1 and uses this one, ignoring the Certificate #2. Even though I'm not really sure why it does this:

    One blog entry which describes this is here: https://scotthelme.co.uk/cross-signing-alternate-trust-paths-how-they-work/