PyOpenSSL - how can I get SAN(Subject Alternative Names) list
Asked Answered
K

4

8

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?

Kenneth answered 26/3, 2018 at 12:41 Comment(0)
D
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
Dugan answered 17/6, 2018 at 7:23 Comment(8)
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())Rimskykorsakov
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 :)Dugan
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 :)Rimskykorsakov
anyway str() not break anything, I've edited the answer, thanks.Dugan
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.Barcroft
Warning the altnames are free form fields so a malicous third party could use 'DNS:altname1.net, DNS:altname2.net,, DNS:DNS:wtf' for exampleSarabia
@Rimskykorsakov or use "'subjectAltName' == ext.get_short_name().decode('utf-8')"Orran
Why not str(ext)?Orran
H
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'])
Hufford answered 12/3, 2020 at 21:55 Comment(2)
How would one extract just the value of common name? In the representation "<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='www.example.com')>], print just 'www.example.com' ?Avesta
@YoloPerdiem , try print(common_name[0].value)Hufford
S
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
Sarabia answered 7/4, 2021 at 16:40 Comment(0)
K
-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
Kenneth answered 4/4, 2018 at 12:5 Comment(3)
This may not work as "get_extension" method may get another index to get SAN data, please check my answer. Thanks.Dugan
Also playing with bytes seems different depending on the case.Dugan
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)Sarabia

© 2022 - 2024 — McMap. All rights reserved.