pythonsslpem

Get or build PEM certificate chain in Python


Is it possible to get the whole certificate chain in a PEM format using ssl with Python ? I can get the specific one with :

import ssl
addr = '192.0.2.1'
cert_str = ssl.get_server_certificate((addr, 443))

which give me something like :

-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----

But I'd like to have :

-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----

I'm pretty sure this is possible since I can download this from my web browser. Any idea ?

(I have already checked Getting certificate chain with Python 3.3 SSL module but I'm not sure this is what I want ...)

EDIT: What I tried after Patrick Mevzek answer :

from OpenSSL import SSL
import socket
dst = ('192.0.2.1', 443)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.set_alpn_protos([b'http/1.1'])
if sock.connect_ex(dst) == 0:
    connection = SSL.Connection(ctx, sock)
    cert_str = connection.get_peer_cert_chain()

But cert_str is None. I think this is because i'm missing something with the use of OpenSSL.


Solution

  • If you use the OpenSSL library in Python, you have get_peer_cert_chain that you can apply on your connection object, it will give you a list of certificates as sent by the server, so the end certificate with all intermediates one if needed.

    See https://pyopenssl.org/en/stable/api/ssl.html#connection-objects :

    get_peer_cert_chain()

    Retrieve the other side’s certificate (if any)

    Returns: A list of X509 instances giving the peer’s certificate chain, or None if it does not have one.

    Here is one crude example (without any error handling):

    from OpenSSL import SSL
    import socket
    
    dst = ('www.google.com', 443)
    ctx = SSL.Context(SSL.SSLv23_METHOD)
    s = socket.create_connection(dst)
    s = SSL.Connection(ctx, s)
    s.set_connect_state()
    s.set_tlsext_host_name(dst[0])
    
    s.sendall('HEAD / HTTP/1.0\n\n')
    s.recv(16)
    
    certs = s.get_peer_cert_chain()
    for pos, cert in enumerate(certs):
       print "Certificate #" + str(pos)
       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')
    

    which gives:

    Certificate #0
    Subject C: US
    Subject ST: California
    Subject L: Mountain View
    Subject O: Google LLC
    Subject CN: www.google.com
    notBefore:20180612133452Z
    notAfter:20180821121300Z
    version:2
    sigAlg:sha256WithRSAEncryption
    digest:06:C5:12:EB:3C:B1:7F:AB:18:E0:D5:22:E4:25:12:A7:30:AA:27:16:0B:3A:99:CB:3D:11:CF:12:EF:95:2E:41
    Certificate #1
    Subject C: US
    Subject O: Google Trust Services
    Subject CN: Google Internet Authority G3
    notBefore:20170615000042Z
    notAfter:20211215000042Z
    version:2
    sigAlg:sha256WithRSAEncryption
    digest:BE:0C:CD:54:D4:CE:CD:A1:BD:5E:5D:9E:CC:85:A0:4C:2C:1F:93:A5:22:0D:77:FD:E8:8F:E9:AD:08:1F:64:1B
    

    So you have the full detailed content of the certificate, see https://pyopenssl.org/en/stable/api/crypto.html#x509-objects for available info. Then you have to_cryptography() to be able to get its PEM version with something like: cert.to_cryptography().public_bytes(serialization.Encoding.PEM)

    But take into account also that:

    From the link in your question, it seems you have the equivalent with getpeercertchain() when just using ssl and not OpenSSL; however this still seems to be recorded as a bug with some patches available and may not be released. In fact the latest documentation at https://docs.python.org/3.8/library/ssl.html does not list getpeercertchain().