Timothée Chabat
Timothée Chabat

Reputation: 77

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.

Upvotes: 5

Views: 8258

Answers (3)

mayk
mayk

Reputation: 41

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

The certificate chain is available as soon as a handshake is done. The handshake is usually deferred until the first data should be sent or received. That's how @cmirza's answer works.

You may request the handshake without content on the wire:

connection.do_handshake()

Bonus Simplification

If there is nothing special about your socket setup, then PyOpenSSL will be happy to connect your socket as-default-as-possible. This yields:

with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as sock:
    ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLS_CLIENT_METHOD)
    connection = OpenSSL.SSL.Connection(
        context=ctx,
        socket=sock,
    )
    connection.connect(
        addr=('example.org',443),
    )
    connection.do_handshake()
    chain = connection.get_peer_cert_chain()
    chain = list(
        OpenSSL.crypto.dump_certificate(
            type=OpenSSL.crypto.FILETYPE_PEM,
            cert=cert,
        ).decode('utf-8')
        for cert in chain
    )

Upvotes: 2

cmirza
cmirza

Reputation: 21

Had this same question and had trouble finding a solution that gave the cert chain in PEM format as a string to be fed into a requests.session(). This function should return exactly what you're asking for.

from OpenSSL import SSL, crypto
import socket

def getPEMFile():

  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(str.encode(dst[0]))

  s.sendall(str.encode('HEAD / HTTP/1.0\n\n'))

  peerCertChain = s.get_peer_cert_chain()
  pemFile = ''

  for cert in peerCertChain:
      pemFile += crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")

  return pemFile

Upvotes: 2

Patrick Mevzek
Patrick Mevzek

Reputation: 12475

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:

  • it would be better to use a callback, see the set_info_callback() method, so that the diagnostics are run at the appropriate time (even type SSL.SSL_CB_HANDSHAKE_DONE)
  • I have observed you need to exchange some traffic (send/recv) before being able to call get_peer_cert_chain() (if you move it before sendall() in my example, it returns None)

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().

Upvotes: 5

Related Questions