7

I'm trying to find a way to get the list of SAN from a given certificate, however, I couldn't find anything in the pyOpenSSL documentation.

Any Ideas on how can I pull this data from the certificate?

Elon Salfati
  • 1,537
  • 6
  • 23
  • 46

4 Answers4

9

I found a way where we first check extension by name, and then, when "SAN" data found we get str representation and return.

def get_certificate_san(x509cert):
    san = ''
    ext_count = x509cert.get_extension_count()
    for i in range(0, ext_count):
        ext = x509cert.get_extension(i)
        if 'subjectAltName' in str(ext.get_short_name()):
            san = ext.__str__()
    return san
  • 2
    This is almost correct. A string comparison won't work as `ext.get_short_name()` is of type byte. I got that function working with the following hacky adjustment: `if 'subjectAltName' in str(ext.get_short_name())` – Ashex Jun 25 '18 at 14:06
  • possible depend on version, for me, string comparison works well, to be honest, I don't think that it should return bytes, but if you have this error - then possible :) –  Jun 25 '18 at 15:58
  • 1
    I couldn't get it to return true until I made those changes. This was all done on python 3.6 with pyOpenSSL 18.0.0, likely a version issue :) – Ashex Jun 25 '18 at 20:06
  • anyway str() not break anything, I've edited the answer, thanks. –  Jun 25 '18 at 20:24
  • I would modify your answer abit; ext.get_short_name().decode('utf-8') to convert to a str and then ext.__str__().replace('DNS', '').replace(':', '') to remove "DNS" and ":" in the response string. – David Okwii Aug 07 '19 at 15:19
  • **Warning** the altnames are free form fields so a malicous third party could use `'DNS:altname1.net, DNS:altname2.net,, DNS:DNS:wtf'` for example – Romuald Brunet Apr 07 '21 at 15:06
  • @Ashex or use "'subjectAltName' == ext.get_short_name().decode('utf-8')" – Rony Tesler Apr 20 '22 at 23:53
  • Why not str(ext)? – Rony Tesler Apr 21 '22 at 00:04
6

PyOpenSSL recommends using cryptography as it provides a safer and better API. If you can install cryptography (it's a dependency of the requests library, so many projects already have it installed), here's how you get the SAN:

from cryptography import x509

# classes must be subtype of:
#   https://cryptography.io/en/latest/x509/reference/#cryptography.x509.ExtensionType
san = loaded_cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
print(san)

Here's a full example of retrieving a cert from a host and printing its common name and SAN.

import ssl

from cryptography import x509
from cryptography.hazmat.backends import default_backend

certificate: bytes = ssl.get_server_certificate(('example.com', 443)).encode('utf-8')
loaded_cert = x509.load_pem_x509_certificate(certificate, default_backend())

common_name = loaded_cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)
print(common_name)


# classes must be subtype of:
#   https://cryptography.io/en/latest/x509/reference/#cryptography.x509.ExtensionType
san = loaded_cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
san_dns_names = san.value.get_values_for_type(x509.DNSName)
print(san_dns_names)

Alternatively, if you're downloading a cert from a host, Python's built-in ssl library will parse the SANs for you (code from here):

from collections import defaultdict
import socket
import ssl

hostname = 'www.python.org'
context = ssl.create_default_context()

with socket.create_connection((hostname, 443)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
        # https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.getpeercert
        cert = ssock.getpeercert()

subject = dict(item[0] for item in cert['subject'])
print(subject['commonName'])

subjectAltName = defaultdict(set)
for type_, san in cert['subjectAltName']:
    subjectAltName[type_].add(san)
print(subjectAltName['DNS'])
Ben
  • 5,952
  • 4
  • 33
  • 44
1

Based on @anatolii-chmykhalo answer

This returns the altnames for DNS, based on the string representation.

def get_dns_altnames(req: OpenSSL.crypto.X509Req):
    """
    Get DNS altnames from a X509Req certificate

    """

    extensions = (ext for ext in req.get_extensions()
                  if ext.get_short_name() == b'subjectAltName')

    dns_names = []
    for ext in extensions:
        for alt in str(ext).split(', '):
            if alt.startswith('DNS:'):
                dns_names.append(alt[4:])

    return dns_names
Romuald Brunet
  • 5,595
  • 4
  • 38
  • 34
-3

I did some digging into it and I finally found something so if someone else will ever need the answer:

import OpenSSL

def extract_san_from_cert(cert_body):
    '''
    This function will extract the SAN (Subject Alt Names)
    from the certificate
    '''
    cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_body)
    try:
        crt = cert.get_extension(6)
        data = crt.get_data()
        # ignore first 4 bytes and split in \x82\18 (,)
        san = data[4:].split('\x82\x18')
    except IndexError as err:
        # No SAN in the certificate
        san = []

    return san
Elon Salfati
  • 1,537
  • 6
  • 23
  • 46
  • This may not work as "get_extension" method may get another index to get SAN data, please check my answer. Thanks. –  Jun 17 '18 at 07:20
  • Also playing with bytes seems different depending on the case. –  Jun 17 '18 at 07:30
  • The bytes are not OK in that case, `\x18` is the length of your DNS name (and I'm guessing that `\x82` is the type) – Romuald Brunet Apr 07 '21 at 16:31