puwei219
puwei219

Reputation: 567

How can I decode a SSL certificate using python?

How can I decode a pem-encoded (base64) certificate with Python? For example this here from github.com:

-----BEGIN CERTIFICATE-----
MIIHKjCCBhKgAwIBAgIQDnd2il0H8OV5WcoqnVCCtTANBgkqhkiG9w0BAQUFADBp
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSgwJgYDVQQDEx9EaWdpQ2VydCBIaWdoIEFzc3VyYW5j
ZSBFViBDQS0xMB4XDTExMDUyNzAwMDAwMFoXDTEzMDcyOTEyMDAwMFowgcoxHTAb
BgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGCNzwCAQMTAlVT
MRswGQYLKwYBBAGCNzwCAQITCkNhbGlmb3JuaWExETAPBgNVBAUTCEMzMjY4MTAy
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2Fu
IEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYDVQQDEwpnaXRo
dWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7dOJw11wcgnz
M08acnTZtlqVULtoYZ/3+x8Z4doEMa8VfBp/+XOvHeVDK1YJAEVpSujEW9/Cd1JR
GVvRK9k5ZTagMhkcQXP7MrI9n5jsglsLN2Q5LLcQg3LN8OokS/rZlC7DhRU5qTr2
iNr0J4mmlU+EojdOfCV4OsmDbQIXlXh9R6hVg+4TyBkaszzxX/47AuGF+xFmqwld
n0xD8MckXilyKM7UdWhPJHIprjko/N+NT02Dc3QMbxGbp91i3v/i6xfm/wy/wC0x
O9ZZovLdh0pIe20zERRNNJ8yOPbIGZ3xtj3FRu9RC4rGM+1IYcQdFxu9fLZn6TnP
pVKACvTqzQIDAQABo4IDajCCA2YwHwYDVR0jBBgwFoAUTFjLJfBBT1L0KMiBQ5um
qKDmkuUwHQYDVR0OBBYEFIfRjxlu5IdvU4x3kQdQ36O/VUcgMCUGA1UdEQQeMByC
CmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMIGBBggrBgEFBQcBAQR1MHMwJAYI
KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBLBggrBgEFBQcwAoY/
aHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ0FDZXJ0cy9EaWdpQ2VydEhpZ2hBc3N1
cmFuY2VFVkNBLTEuY3J0MAwGA1UdEwEB/wQCMAAwYQYDVR0fBFowWDAqoCigJoYk
aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL2V2MjAwOWEuY3JsMCqgKKAmhiRodHRw
Oi8vY3JsNC5kaWdpY2VydC5jb20vZXYyMDA5YS5jcmwwggHEBgNVHSAEggG7MIIB
tzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cuZGln
aWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggrBgEFBQcCAjCC
AVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABp
AGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBw
AHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQ
AC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQBy
AHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0
ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwBy
AHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBl
AG4AYwBlAC4wHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBEGCWCGSAGG
+EIBAQQEAwIGwDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggEBABRS
cR+GnW01Poa7ZhqLhZi5AEzLQrVG/AbnRDnI6FLYERQjs3KW6RSUni8AKPfVBEVA
AMb0V0JC3gmJlxENFFxrvQv3GKNfZwLzCThjv8ESnTC6jqVUdFlTZ6EbUFsm2v0T
flkXv0nvlH5FpP06STLwav+JjalhqaqblkbIHOAYHOb7gvQKq1KmyuhUItnbKj1a
InuA6gcF1PnH8FNZX7t3ft6TcEFOI8t4eXnELurXZioY99HFfOISeIKNHeyCngGi
5QK+eKG5WVjFTG9PpTG0SVtemB4uOPYZxDmiSvt5BbjyWeUmEnCtwOh1Ix8Y0Qvg
n2Xkw9dJh1tybLEvrG8=
-----END CERTIFICATE-----

According to ssl-shopper it should be something like this:

Common Name: github.com
Subject Alternative Names: github.com, www.github.com
Organization: GitHub, Inc.
Locality: San Francisco
State: California
Country: US
Valid From: May 26, 2011
Valid To: July 29, 2013

How can I get this plaintext using python?

Upvotes: 46

Views: 119601

Answers (10)

Linc
Linc

Reputation: 1

As I understand the problem, the OP wishes to digest information from a decoded pem file for use in other places in a python script. I had a similar use case where I needed to parse certs I generated locally to ensure certain details, and managed to solve my issue with the key parts that helped me below.

Python has a built in module called subprocess which can be used to call commands that the user has access to.

Using this we can call the openssl x509 command and parse the results:

import subprocess
import re # we'll need re later for another code snippet

# using the infile version here, depending on the source of the cert, update accordingly
cert_file = "./cert_dir/fullchain.pem"

command = ["openssl", "x509", "-in", cert_file, "--noout", "--text"]

result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print(result.stdout)

from the result.stdout we can use regex or other search logic to validate results:

subject_pattern = re.compile(r"Subject:\s*(.*)")
subject_match = subject_pattern.search(result.stdout)
print(subject_match)

the subject match should give results similar to: "Subject": "CN = foo.bar.com"

you can also check for any error codes from the command using result.returncode

if result.returncode != 0:
    print("Error:", result.stderr)
    exit(1)

I also found this handy for other x509 related commands:

SSL Shopper - The Most Common OpenSSL Commands

Upvotes: 0

bob
bob

Reputation: 11

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
import os

with open("es01.pem", "rb") as f:
    cert= x509.load_pem_x509_certificate(f.read(), default_backend())

print (f"Serial Number: {cert.serial_number}")
print (f"Extensions: {cert.extensions}")

fingerprint_hashed_byte_string = cert.fingerprint(hashes.SHA256())
fingerprint_hex_string = ':'.join([format(byte, '02x') for byte in fingerprint_hashed_byte_string])
print (f"Fingerprint: {fingerprint_hex_string}")
print (f"Issuer: {cert.issuer}")
print (f"Not Valid Before: {cert.not_valid_before}")
print (f"Not Valid After: {cert.not_valid_after}")

"""
OR SIMPLY USING OPENSSL...

openssl x509 -in es01.pem -noout -serial
serial=12D498EEF25477293EE0CB9A287E530FE2466EF2

openssl x509 -noout -fingerprint -sha256 -inform pem -in es01.pem
sha256 Fingerprint=69:B5:89:3E:7C:9F:D8:70:B1:B5:55:97:B3:87:43:CB:3F:55:D1:0E:8E:D6:58:9B:19:7E:CA:F6:B3:F5:17:50

openssl x509 -noout -ext basicConstraints -inform pem -in es01.pem
X509v3 Basic Constraints: 
    CA:FALSE

openssl x509 -in es01.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name: 
    DNS:es01, IP Address:127.0.0.1, DNS:localhost

openssl x509 -in es01.pem -noout -startdate
notBefore=Apr 18 15:36:36 2023 GMT

openssl x509 -in es01.pem -noout -enddate
notAfter=Apr 17 15:36:36 2025 GMT

openssl x509 -in es01.pem -noout -issuer
issuer=C = US, ST = Minneosta, L = Bloomington, O = TEST-CA, OU = CERTS, CN = INTERMEDIATE-CA
"""

Upvotes: 1

nijave
nijave

Reputation: 598

There's another way to use _test_decode_certificate without using the internal implementation. It's a bit hacky in a different way, though

import ssl

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

# The filepath to your PEM-encoded x509 cert
ctx.load_verify_locations("369fa1ef21f5476c02814c637d83f71d851f867348eef21d1eb0058671d0e5a6.crt")

certificate_details = ctx.get_ca_certs()

Under the hood, this is another entrypoint to the _decode_certificate function which _test_decode_certificate uses.

You can see how this works in the CPython source code https://github.com/python/cpython/blob/main/Modules/_ssl.c#L4578

Upvotes: 5

CristiFati
CristiFati

Reputation: 41167

Notes:

Regarding the certificate (PEM) from the question:

  • Saved it in a file called q016899247.crt (in the script (code00.py) dir)

  • The end tag: ("-----END CERTIFICATE----") was missing a hyphen (-) at the end; corrected in Question @VERSION #4.)

code00.py:

#!/usr/bin/env python

import os
import ssl
import sys
from pprint import pprint as pp


def main(*argv):
    cert_file_base_name = "q016899247.crt"
    cert_file_name = os.path.join(os.path.dirname(__file__), cert_file_base_name)
    try:
        cert_dict = ssl._ssl._test_decode_cert(cert_file_name)
    except Exception as e:
        print("Error decoding certificate: {:}".format(e))
    else:
        print("Certificate ({:s}) data:\n".format(cert_file_base_name))
        pp(cert_dict)


if __name__ == "__main__":
    print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                   64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print("\nDone.")
    sys.exit(rc)

Output:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q016899247]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" ./code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 064bit on win32

Certificate (q016899247.crt) data:

{'OCSP': ('http://ocsp.digicert.com',),
 'caIssuers': ('http://www.digicert.com/CACerts/DigiCertHighAssuranceEVCA-1.crt',),
 'crlDistributionPoints': ('http://crl3.digicert.com/ev2009a.crl',
                           'http://crl4.digicert.com/ev2009a.crl'),
 'issuer': ((('countryName', 'US'),),
            (('organizationName', 'DigiCert Inc'),),
            (('organizationalUnitName', 'www.digicert.com'),),
            (('commonName', 'DigiCert High Assurance EV CA-1'),)),
 'notAfter': 'Jul 29 12:00:00 2013 GMT',
 'notBefore': 'May 27 00:00:00 2011 GMT',
 'serialNumber': '0E77768A5D07F0E57959CA2A9D5082B5',
 'subject': ((('businessCategory', 'Private Organization'),),
             (('jurisdictionCountryName', 'US'),),
             (('jurisdictionStateOrProvinceName', 'California'),),
             (('serialNumber', 'C3268102'),),
             (('countryName', 'US'),),
             (('stateOrProvinceName', 'California'),),
             (('localityName', 'San Francisco'),),
             (('organizationName', 'GitHub, Inc.'),),
             (('commonName', 'github.com'),)),
 'subjectAltName': (('DNS', 'github.com'), ('DNS', 'www.github.com')),
 'version': 3}

Done.

Upvotes: 30

Marcin Kulik
Marcin Kulik

Reputation: 983

This allows to extract certain values from SSL certificate:

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

hostname = 'google.com.com'
port = 443
cert = ssl.get_server_certificate((hostname, port))
certDecoded = x509.load_pem_x509_certificate(str.encode(cert), 
default_backend())
print(certDecoded.issuer)
print(certDecoded.subject)
print(certDecoded.not_valid_after)
print(certDecoded.not_valid_before)

Upvotes: 6

Zaheeb Shamsi
Zaheeb Shamsi

Reputation: 21

You can download the code from here. It is purely extracting data from a .pem & .cer type certificates.

Else, pem certificate can be decoded using the following snippet:

    #import pem & pyOpenSSL module

    certs = pem.parse_file(file_path)  # using pem module
    for pem_certificates in certs:
        strcert = str(pem_certificates)
        loadCert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,strcert)
        print(loadCert.get_issuer())```

 

Upvotes: 2

andviro
andviro

Reputation: 321

You can use pyasn1 and pyasn1-modules packages to parse this kind of data. For instance:

from pyasn1_modules import pem, rfc2459
from pyasn1.codec.der import decoder

substrate = pem.readPemFromFile(open('cert.pem'))
cert = decoder.decode(substrate, asn1Spec=rfc2459.Certificate())[0]
print(cert.prettyPrint())

Read the docs for pyasn1 for the rest.

Upvotes: 32

newbee
newbee

Reputation: 1

I am not Sure how you received it but another simple way for getting it installed is to just write it as a binary file and then run it using os

import os

cert= function_gives_binary_cert()
with open('RecvdCert.der','wb') as file:
     file.write(cert)

os.startfile('RecvdCert.der')

Beware of running received binary from an unknown source. Just want to decode then use OpenSSL as mentioned in other answers.

Upvotes: -3

Roberto Mier
Roberto Mier

Reputation: 59

This code dumps a cert file content:

import OpenSSL.crypto

cert = OpenSSL.crypto.load_certificate(
      OpenSSL.crypto.FILETYPE_PEM,
      open('/path/to/cert/file.crt').read()
)

print OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, cert)

Give it a go.

Upvotes: 5

zwol
zwol

Reputation: 140806

Python's standard library, even in the latest version, does not include anything that can decode X.509 certificates. However, the add-on cryptography package does support this. Quoting an example from the documentation:

>>> from cryptography import x509
>>> from cryptography.hazmat.backends import default_backend
>>> cert = x509.load_pem_x509_certificate(pem_data, default_backend())
>>> cert.serial_number
2

Another add-on package that might be an option is pyopenssl. This is a thin wrapper around the OpenSSL C API, which means it will be possible to do what you want, but expect to spend a couple days tearing your hair out at the documentation.

If you can't install Python add-on packages, but you do have the openssl command-line utility,

import subprocess
cert_txt = subprocess.check_output(["openssl", "x509", "-text", "-noout", 
                                    "-in", certificate])

should produce roughly the same stuff you got from your web utility in cert_txt.

Incidentally, the reason doing a straight-up base64 decode gives you binary gobbledygook is that there are two layers of encoding here. X.509 certificates are ASN.1 data structures, serialized to X.690 DER format and then, since DER is a binary format, base64-armored for ease of file transfer. (A lot of the standards in this area were written way back in the nineties when you couldn’t reliably ship anything but seven-bit ASCII around.)

Upvotes: 67

Related Questions