Commit d17dd692 authored by DOE CODE's avatar DOE CODE Committed by Tim Sowers
Browse files

Initial commit.

parents
Pipeline #210 failed with stages
in 0 seconds
This diff is collapsed.
# x509 certificate validation libraries
** Note, this is currenlty BETA software **
These libraries can be used to validate x509 libraries. Supported functionality includes:
1) certificate chain validations
2) certificate expiration validations
3) Certificate Revocation list validations
4) common name matching
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID
from whoswho import who
from fuzzywuzzy import fuzz
from libcertvalidate.lib.subject_dn import subject_dn
###############################################################################################
# CN_match function: #
# CN Matching verifies that the common name in the subject of a given x509 certs is #
# assocaited with an identity.
# This is a hard problem. For example, CHRISTOPHER != CHRIS, so we can't do #
# simple string parsing. Instead I leveraged some fuzzy text parsing to solve the problem #
# for me. One such tool is taken from the feild of information Theory and called #
# Levenshtein distance: https://en.wikipedia.org/wiki/Levenshtein_distance #
# However this has the clear downside of the result being probabilistic #
# PRECONDITION: Accepts a PEM ecoded certificate and one or two idnetifier strings #
# POSTCONDITION: Returns a bool: #
# True = one of the identifier strings is in the Common name (valid). #
# False = both of the identifier strings are not in the Common Name #
# (invalid -- cross assignment has happened here). #
###############################################################################################
def CN_match(certificate, identity1='', identity2=''):
#load the pem into a certobject
certobject = x509.load_pem_x509_certificate(certificate.encode(), default_backend())
#convert certobject into subject string
subject=subject_dn(certobject.subject)
commonName=''
for attribute in certobject.subject:
if attribute.oid == NameOID.COMMON_NAME:
commonName=attribute.value
# Use some fuzzy logic string matching libs (leveraging Levenshtein distance)
# to determine if common name matches the identity
# Note: this is probabilistic. So not perfect: we can't rule out the possibility
# of cross posting certificates. However it is now unlikely, unless two
# People have similiar names.
if who.ratio(commonName, identity1) < 65:
if fuzz.token_sort_ratio(commonName, identity1) < 75:
LOG.warning: ("{0} does not match the common name for this cert: ({1})!\
Skipping over cert.'".format(identity1, commonName))
return False
return True
#!/usr/bin/env python3
import urllib.request
import logging
from cryptography import x509
from cryptography.hazmat.backends import default_backend
# from cryptography.hazmat.primitives import hashes
from libcertvalidate.lib.crl_distribution_point import parse_CRL_distribution_point
from libcertvalidate.lib.subject_dn import subject_dn
from libcertvalidate.lib.validate_crl_signature import validate_CRL_signature
LOG = logging.getLogger(__name__)
###############################################################################################
# crl_check() function: #
# Checks a certificate against its Certificate Revocation List (CRL) #
# It parses out the CRL distribution point for you, downloads the CRL, digitally validates #
# The signature of the CRL, then checks to see if your cert is in the CRL. #
# PRECONDITION: Accepts PEM encoded cert. #
# POSTCONDITION: Returns a bool: #
# - True = cert is revoked #
# - False = cert is not revoked, #
###############################################################################################
def crl_check(certificate):
crl_distribution_point = parse_CRL_distribution_point(certificate)
if crl_distribution_point == 1:
return 0 #this happens when ITSD has improperly formatted certs.
#We can either reject the use of these certs or do
#"opportunistic" crypto..... :sadpanda:
certobject = x509.load_pem_x509_certificate(certificate.encode(), default_backend())
#TODO: Add exceptio handler here to detect if the urllib thing fails.
try:
response = urllib.request.urlopen(crl_distribution_point)
except Exception as err:
LOG.warning("Could not download CRL from %s. Debug info: %s", crl_distribution_point, err)
return 1
der_crl_data = response.read() # a `bytes` object
crl = x509.load_der_x509_crl(der_crl_data, default_backend())
sub_dn_cert = subject_dn(certobject.subject)
if validate_CRL_signature(der_crl_data):
#https://cryptography.io/en/latest/x509/reference/?highlight=crl#cryptography.x509.CertificateRevocationList.get_revoked_certificate_by_serial_number
#The Python NoneType indicates no hit in the CRL
if crl.get_revoked_certificate_by_serial_number(certobject.serial_number) is None:
LOG.info("%s's certificate has not been revoked.", sub_dn_cert)
return 0
LOG.error("%s's certificate has been revoked.", sub_dn_cert)
return 1
LOG.error("Unable to validate CRL signature for %s' certificate.", sub_dn_cert)
return 1
#Below is the list of CRLs for NCCS user certificates in the MFA4 repo FYI
'''mac111578:enrollees sm7$ cat output.txt | sort | uniq -c | sort -n
1 URI:http://crl.ornl.gov/ORNL%20CA%201%20v1.crl
10 URI:http://crlint.ornl.gov/ornl/ORNLCASC(1).crl
15 URI:ldap:///CN=ORNLCASC(1),CN=ORNLCASC,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=ornl,DC=gov?certificateRevocationList?base?objectClass=cRLDistributionPoint
30 URI:http://crlint.ornl.gov/ornl/ORNLCASC(2).crl
60 URI:http://sspweb.managed.entrust.com/CRLs/EMSSSPCA2.crl
60 URI:ldap://sspdir.managed.entrust.com/cn=WinCombined2,ou=Entrust%20Managed%20Services%20SSP%20CA,ou=Certification%20Authorities,o=Entrust,c=US?certificateRevocationList;binary
'''
#proxy = {'http': 'http://proxy.ccs.ornl.gov:3128/'}
import OpenSSL
import sys
from OpenSSL import crypto
from asn1crypto.core import Sequence
###############################################################################################
# parse_CRL_distribution_point() function: #
# Parses out the URL where we can download the CRL from. #
# PRECONDITION: Accepts PEM encoded cert. #
# POSTCONDITION: Returns a string which is the URL #
###############################################################################################
def parse_CRL_distribution_point(certificate):
cert = crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
# firstly, parse out the x509 extension "X509v3 CRL Distribution Points"
# the "CRL Distribution Point" is a fancy way of saying the URL where we
# can download the CRL from
try:
# iterate over all x509 extensions
for x509_extension_index in range(cert.get_extension_count()):
# if the x509 extension is named "crlDistributionPoints", then we have a hit!
if cert.get_extension(x509_extension_index).get_short_name() == 'crlDistributionPoints'.encode():
ext = cert.get_extension(x509_extension_index)
break
except:
LOG.warning("This error message seems bad, but really isnt.")
# some x509 extensions ITSD has do not have a name and traceback.
# I don't know why the hell they do this.
# we don't care about unnamed extensions anywho.
data=ext.get_data()
# ASN deserialization
parsed = Sequence.load(data)
serialized = parsed.dump()
# Could not figure out proper deserialization; using this jank to get a string.
# some magic, courtesey of stack overflow...
# this takes the bytes object and converts into a string and parses the URL out
# https://stackoverflow.com/questions/606191/convert-bytes-to-a-string
stream = [parsed[0].contents]
PY3K = sys.version_info >= (3, 0)
lines = []
for line in stream:
if not PY3K:
lines.append(line)
else:
lines.append(line.decode('utf-8', 'backslashreplace'))
try:
CRL_distribution_point="http"+lines[0].split("http")[1].split(".crl")[0]+".crl"
except Exception as e:
#LOG.warning("Invalid CRL distribution Point for user: {0}. More debug info here: {1}. You Had one job ITSD.".format(subject_dn(certobject.subject), e))
LOG.warning("Invalid CRL distribution Point for user: {0}. More debug info here: {1}. You Had one job ITSD.".format("...", e))
return(1)
return(CRL_distribution_point)
from cryptography.x509.oid import NameOID
###############################################################################################
# subject_dn() function: #
# This addresses a shortcomming of how the python cryptography library parses x509 certs #
# It does not pull out data very well, so some preocessing is required. #
# PRECONDITION: Accepts a crypgraphy object of attributes. #
# POSTCONDITION: Returns a string that looks like a proper subject line #
###############################################################################################
def subject_dn(attributes, reverse=False):
subject = []
# Keep track of where we find the UID and CN so we can insert into the subject attribute list
cn = None
cn_place = 0
uid = None
uid_place = 0
if reverse:
insert_into = 0
else:
insert_into = len(attributes)
place = len(attributes)
for attribute in attributes:
place -= 1
if attribute.oid == NameOID.COUNTRY_NAME:
subject.insert(insert_into, "C=" + attribute.value)
elif attribute.oid == NameOID.DOMAIN_COMPONENT:
subject.insert(insert_into, "DC=" + attribute.value)
elif attribute.oid == NameOID.ORGANIZATION_NAME:
subject.insert(insert_into, "O=" + attribute.value)
elif attribute.oid == NameOID.ORGANIZATIONAL_UNIT_NAME:
subject.insert(insert_into, "OU=" + attribute.value)
elif attribute.oid == NameOID.COMMON_NAME:
if reverse:
cn = "CN=" + attribute.value
cn_place = place
else:
subject.insert(insert_into, "CN=" + attribute.value)
elif attribute.oid == NameOID.USER_ID:
if reverse:
uid = "UID=" + attribute.value
uid_place = place
else:
subject.insert(insert_into, "UID=" + attribute.value)
else:
LOG.critical("error could not find {0}".format(attribute))
sys.exit(1)
if not reverse:
return "/".join(subject)
LOG.debug(subject)
LOG.debug("cn value: {0}, uid value: {1}, cn_place: {2}, uid_place: {3}".format(cn, uid, cn_place, uid_place))
# Looks like: subject= /C=US/O=U.S. Government/OU=Department of Energy/UID=89001000743346/CN=RYAN ADAMSON (Affiliate)
# We want this to go into the mapper file: UID=89001000743346+CN=RYAN ADAMSON (Affiliate),OU=Department of Energy,O=U.S. Government,C=US
if cn != None and uid != None:
subject.insert(min(cn_place, uid_place), "{0}+{1}".format(uid, cn))
elif cn != None:
subject.insert(cn_place, cn)
return "/".join(subject)
import os
import pkg_resources
import OpenSSL
#TODO: Parse this from a yaml config file
CA_TRUST = [("ROOT1.crt", "LEAF1.crt"),
("ROOT2.crt", "LEAF2.crt")]
CA_DIR = 'cadir'
###############################################################################################
# validate_CRL_signature() function: #
# Validates the digital signature of the CRL. #
# PRECONDITION: Accepts DER formatted CRL. #
# POSTCONDITION: Returns a bool: #
# True: Valid #
# False: invalid #
###############################################################################################
def validate_CRL_signature(der_crl_data):
valid_signature=False
for CA in CA_TRUST:
# grab the issuing CA PEM Cert from file
file = open(os.path.join(CA_DIR, CA[1]), mode='r')
cacert = file.read()
file.close()
# load the DER encoded CRL into a pyOpenSSL crl object
crl = OpenSSL.crypto.load_crl(OpenSSL.crypto.FILETYPE_ASN1, der_crl_data) #resp.content)
# Export CRL as a cryptography CRL.
crl_crypto = crl.to_cryptography()
# Load CA CERTIFICATE into the CA cert object
ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cacert.encode())
# Get CA Public Key as _RSAPublicKey
ca_pub_key = ca.get_pubkey().to_cryptography_key()
# Validate CRL against CA
valid_signature = crl_crypto.is_signature_valid(ca_pub_key)
if valid_signature:
break
return valid_signature
import logging
import os
import subprocess
#TODO: Parse this from a yaml config file
CA_TRUST = [("ROOT1.crt", "LEAF1.crt"),
("ROOT2.crt", "LEAF2.crt"),
CA_DIR = 'cadir'
LOG = logging.getLogger(__name__)
###############################################################################################
# verify function: #
# Cryptographically verifies the chain of trust for a given certificate to validate whether#
# or not it is a properly issued Certificate. #
# PRECONDITION: Accepts a PEM ecoded certificate. #
# POSTCONDITION: Returns 3 things: #
# 1) a Boolean Value: #
# True = a valid certificate. #
# False = a Invalid certificate #
# 2) found_ca: a string which contains the name of the CA which issued this cert #
# 3) openssl_error: the output of openssl if it returns an error code #
###############################################################################################
def verify(certificate):
LOG.debug("Trying to verify against known CAs")
valid = False
found_ca = None
openssl_error = None
for cert_auth, intermediate in CA_TRUST:
command = [
"openssl", "verify", "-CAfile",
os.path.join(CA_DIR, cert_auth), "-untrusted",
os.path.join(CA_DIR, intermediate),
]
LOG.debug(" ".join(command))
verify_proc = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE
)
out, err = verify_proc.communicate(certificate.encode())
verify_proc.wait()
# if "%s: OK" % temp_file.name in out.decode():
if "stdin: OK" in out.decode():
LOG.info("Successful verification against known CAs")
found_ca = cert_auth
valid = True
break
# local issuer is a error that usually just means that the intermediate is wrong, which is expected and unhelpful
if "OK" in out.decode() and "unable to get local issuer certificate" not in out.decode():
openssl_error = out
valid = False
else:
LOG.debug(out)
LOG.debug(err)
valid = False
return valid, found_ca, openssl_error
print("hello world!!")
from libcertvalidate.cn_match import CN_match
from libcertvalidate.crl_check import crl_check
from libcertvalidate.verify import verify
f = open("cert.pem", "r")
sm7_cert=f.read()
#This works!!
#print(CN_match(sm7_cert, "Stefan Maerz"))
#This works!!
#print(crl_check(sm7_cert))
#This works!!
print(verify(sm7_cert))
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment