add CRL functionality from authnid
This commit is contained in:
0
atst/domain/authnid/__init__.py
Normal file
0
atst/domain/authnid/__init__.py
Normal file
72
atst/domain/authnid/crl/util.py
Normal file
72
atst/domain/authnid/crl/util.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import requests
|
||||
import re
|
||||
import os
|
||||
from html.parser import HTMLParser
|
||||
|
||||
_DISA_CRLS = "https://iasecontent.disa.mil/pki-pke/data/crls/dod_crldps.htm"
|
||||
|
||||
|
||||
def fetch_disa():
|
||||
response = requests.get(_DISA_CRLS)
|
||||
return response.text
|
||||
|
||||
|
||||
class DISAParser(HTMLParser):
|
||||
crl_list = []
|
||||
_CRL_MATCH = re.compile("DOD(ROOT|EMAIL|ID)?CA")
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "a":
|
||||
href = [pair[1] for pair in attrs if pair[0] == "href"].pop()
|
||||
if re.search(self._CRL_MATCH, href):
|
||||
self.crl_list.append(href)
|
||||
|
||||
|
||||
def crl_list_from_disa_html(html):
|
||||
parser = DISAParser()
|
||||
parser.reset()
|
||||
parser.feed(html)
|
||||
return parser.crl_list
|
||||
|
||||
|
||||
def write_crl(out_dir, crl_location):
|
||||
name = re.split("/", crl_location)[-1]
|
||||
crl = os.path.join(out_dir, name)
|
||||
with requests.get(crl_location, stream=True) as r:
|
||||
with open(crl, "wb") as crl_file:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
crl_file.write(chunk)
|
||||
|
||||
|
||||
def refresh_crls(out_dir, logger=None):
|
||||
disa_html = fetch_disa()
|
||||
crl_list = crl_list_from_disa_html(disa_html)
|
||||
for crl_location in crl_list:
|
||||
if logger:
|
||||
logger.info("updating CRL from {}".format(crl_location))
|
||||
try:
|
||||
write_crl(out_dir, crl_location)
|
||||
except requests.exceptions.ChunkedEncodingError:
|
||||
if logger:
|
||||
logger.error(
|
||||
"Error downloading {}, continuing anyway".format(crl_location)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="[%(asctime)s]:%(levelname)s: %(message)s"
|
||||
)
|
||||
logger = logging.getLogger()
|
||||
logger.info("Updating CRLs")
|
||||
try:
|
||||
refresh_crls(sys.argv[1], logger=logger)
|
||||
except Exception as err:
|
||||
logger.exception("Fatal error encountered, stopping")
|
||||
sys.exit(1)
|
||||
logger.info("Finished updating CRLs")
|
124
atst/domain/authnid/crl/validator.py
Normal file
124
atst/domain/authnid/crl/validator.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
from OpenSSL import crypto, SSL
|
||||
|
||||
|
||||
def sha256_checksum(filename, block_size=65536):
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filename, "rb") as f:
|
||||
for block in iter(lambda: f.read(block_size), b""):
|
||||
sha256.update(block)
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
class Validator:
|
||||
|
||||
_PEM_RE = re.compile(
|
||||
b"-----BEGIN CERTIFICATE-----\r?.+?\r?-----END CERTIFICATE-----\r?\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def __init__(self, crl_locations=[], roots=[], base_store=crypto.X509Store):
|
||||
self.errors = []
|
||||
self.crl_locations = crl_locations
|
||||
self.roots = roots
|
||||
self.base_store = base_store
|
||||
self._reset()
|
||||
|
||||
def _reset(self):
|
||||
self.cache = {}
|
||||
self.store = self.base_store()
|
||||
self._add_crls(self.crl_locations)
|
||||
self._add_roots(self.roots)
|
||||
self.store.set_flags(crypto.X509StoreFlags.CRL_CHECK)
|
||||
|
||||
def _add_crls(self, locations):
|
||||
for filename in locations:
|
||||
try:
|
||||
self._add_crl(filename)
|
||||
except crypto.Error as err:
|
||||
self.errors.append(
|
||||
"CRL could not be parsed. Filename: {}, Error: {}, args: {}".format(
|
||||
filename, type(err), err.args
|
||||
)
|
||||
)
|
||||
|
||||
# This caches the CRL issuer with the CRL filepath and a checksum, in addition to adding the CRL to the store.
|
||||
|
||||
def _add_crl(self, filename):
|
||||
with open(filename, "rb") as crl_file:
|
||||
crl = crypto.load_crl(crypto.FILETYPE_ASN1, crl_file.read())
|
||||
self.cache[crl.get_issuer().der()] = (filename, sha256_checksum(filename))
|
||||
self._add_carefully("add_crl", crl)
|
||||
|
||||
def _parse_roots(self, root_str):
|
||||
return [match.group(0) for match in self._PEM_RE.finditer(root_str)]
|
||||
|
||||
def _add_roots(self, roots):
|
||||
for filename in roots:
|
||||
with open(filename, "rb") as f:
|
||||
for raw_ca in self._parse_roots(f.read()):
|
||||
ca = crypto.load_certificate(crypto.FILETYPE_PEM, raw_ca)
|
||||
self._add_carefully("add_cert", ca)
|
||||
|
||||
# in testing, it seems that openssl is maintaining a local cache of certs
|
||||
# in a hash table and throws errors if you try to add redundant certs or
|
||||
# CRLs. For now, we catch and ignore that error with great specificity.
|
||||
|
||||
def _add_carefully(self, method_name, obj):
|
||||
try:
|
||||
getattr(self.store, method_name)(obj)
|
||||
except crypto.Error as error:
|
||||
if self._is_preloaded_error(error):
|
||||
pass
|
||||
else:
|
||||
raise error
|
||||
|
||||
PRELOADED_CRL = (
|
||||
[
|
||||
(
|
||||
"x509 certificate routines",
|
||||
"X509_STORE_add_crl",
|
||||
"cert already in hash table",
|
||||
)
|
||||
],
|
||||
)
|
||||
PRELOADED_CERT = (
|
||||
[
|
||||
(
|
||||
"x509 certificate routines",
|
||||
"X509_STORE_add_cert",
|
||||
"cert already in hash table",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def _is_preloaded_error(self, error):
|
||||
return error.args == self.PRELOADED_CRL or error.args == self.PRELOADED_CERT
|
||||
|
||||
# Checks that the CRL currently in-memory is up-to-date via the checksum.
|
||||
|
||||
def refresh_cache(self, cert):
|
||||
der = cert.get_issuer().der()
|
||||
if der in self.cache:
|
||||
filename, checksum = self.cache[der]
|
||||
if sha256_checksum(filename) != checksum:
|
||||
self._reset()
|
||||
|
||||
def validate(self, cert):
|
||||
parsed = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
|
||||
self.refresh_cache(parsed)
|
||||
context = crypto.X509StoreContext(self.store, parsed)
|
||||
try:
|
||||
context.verify_certificate()
|
||||
return True
|
||||
|
||||
except crypto.X509StoreContextError as err:
|
||||
self.errors.append(
|
||||
"Certificate revoked or errored. Error: {}. Args: {}".format(
|
||||
type(err), err.args
|
||||
)
|
||||
)
|
||||
return False
|
Reference in New Issue
Block a user