Maintain static list of CRL URIs and issuers.

The previous solution (ad-hoc stream-parsing the CRLs to obtain their
issuers and nextUpdate) was too cute. It began breaking on CRLs that had
an addition hex 0x30 byte somewhere in their header. I thought that 0x30
was a reserved character only to be used for tags in ASN1 encoded with
DER; turns out that's not true. Rather than write a full-fledged ASN1
stream-parser, the simplest solution is to just maintain the list of
issuers as a constant in the codebase. This is fine because the issuer
for a specific CRL URI should not change. If it does, we've probably got
bigger problems.

This also removes the Flask app's functionality for updating the local
CRL cache. This is being handled out-of-band by a Kubernetes CronJob
and is not a concern of the app's. This means that instances of the
CRLCache do not have to explicitly track expirations for CRLs.
Previously, the in-memory dictionary or CRL issuers and locations
included expirations; now it is flattened to not include that
information.

The CRLCache class has been updated to accept a crl_list kwargs so that
unit tests can provide their own alternative CRL lists, since we now
hard-code the expected CRLs and issuers. The nightly CRL check job has
been updated to check that the hard-coded list of issuers matches what
we get when we actually sync the CRLs.
This commit is contained in:
dandds
2019-11-11 09:49:01 -05:00
parent 3d92ac4840
commit 1b6239893b
6 changed files with 307 additions and 274 deletions

View File

@@ -1,19 +1,16 @@
import os
import pytest
from atst.domain.authnid.crl.util import scan_for_issuer_and_next_update
from atst.domain.authnid.crl.util import crl_local_path, CRL_LIST
from tests.utils import parse_for_issuer_and_next_update
CRL_DIR = "crls"
_CRLS = [
"{}/{}".format(CRL_DIR, file_) for file_ in os.listdir(CRL_DIR) if ".crl" in file_
]
@pytest.mark.parametrize("crl_path", _CRLS)
def test_crl_scan_against_parse(crl_path):
@pytest.mark.parametrize("crl_uri, issuer", CRL_LIST)
def test_crl_scan_against_parse(crl_uri, issuer):
crl_path = crl_local_path(CRL_DIR, crl_uri)
parsed_der = parse_for_issuer_and_next_update(crl_path)
scanned_der = scan_for_issuer_and_next_update(crl_path)
assert parsed_der == scanned_der
assert issuer == parsed_der.hex()

View File

@@ -14,16 +14,14 @@ from atst.domain.authnid.crl import (
NoOpCRLCache,
)
from atst.domain.authnid.crl.util import (
scan_for_issuer_and_next_update,
build_crl_locations_cache,
serialize_crl_locations_cache,
load_crl_locations_cache,
serialize_crl_locations_cache,
CRLParseError,
JSON_CACHE,
)
from tests.mocks import FIXTURE_EMAIL_ADDRESS, DOD_CN
from tests.utils import FakeLogger, parse_for_issuer_and_next_update
from tests.utils import FakeLogger, parse_for_issuer_and_next_update, make_crl_list
class MockX509Store:
@@ -43,14 +41,13 @@ class MockX509Store:
def test_can_build_crl_list(crl_file, ca_key, ca_file, make_crl, tmpdir):
crl = make_crl(ca_key)
dir_ = os.path.dirname(crl_file)
serialize_crl_locations_cache(dir_)
cache = CRLCache(ca_file, dir_, store_class=MockX509Store)
issuer_der = crl.issuer.public_bytes(default_backend())
dir_ = os.path.dirname(crl_file)
serialize_crl_locations_cache(dir_, crl_list=[(str(crl_file), issuer_der.hex())])
cache = CRLCache(ca_file, dir_, store_class=MockX509Store)
assert len(cache.crl_cache.keys()) == 1
assert issuer_der in cache.crl_cache
assert cache.crl_cache[issuer_der]["location"] == crl_file
assert cache.crl_cache[issuer_der]["expires"] == crl.next_update
assert cache.crl_cache[issuer_der] == crl_file
def test_can_build_trusted_root_list(app):
@@ -81,7 +78,8 @@ def test_crl_validation_on_login(
serialize_pki_object_to_disk(crl, crl_file, encoding=Encoding.DER)
crl_dir = os.path.dirname(crl_file)
cache = CRLCache(ca_file, crl_dir)
crl_list = make_crl_list(good_cert, crl_file)
cache = CRLCache(ca_file, crl_dir, crl_list=crl_list)
assert cache.crl_check(good_cert.public_bytes(Encoding.PEM).decode())
with pytest.raises(CRLRevocationException):
cache.crl_check(bad_cert.public_bytes(Encoding.PEM).decode())
@@ -97,9 +95,10 @@ def test_can_dynamically_update_crls(
serialize_pki_object_to_disk,
):
crl_dir = os.path.dirname(crl_file)
cache = CRLCache(ca_file, crl_dir)
client_cert = make_x509(rsa_key(), signer_key=ca_key, cn="chewbacca")
client_pem = client_cert.public_bytes(Encoding.PEM)
crl_list = make_crl_list(client_cert, crl_file)
cache = CRLCache(ca_file, crl_dir, crl_list=crl_list)
assert cache.crl_check(client_pem)
revoked_crl = make_crl(ca_key, expired_serials=[client_cert.serial_number])
@@ -147,7 +146,8 @@ def test_expired_crl_raises_CRLInvalidException_with_failover_config_false(
client_cert = make_x509(rsa_key(), signer_key=ca_key, cn="chewbacca")
client_pem = client_cert.public_bytes(Encoding.PEM)
crl_dir = os.path.dirname(expired_crl_file)
crl_cache = CRLCache(ca_file, crl_dir)
crl_list = make_crl_list(client_cert, expired_crl_file)
crl_cache = CRLCache(ca_file, crl_dir, crl_list=crl_list)
with pytest.raises(CRLInvalidException):
crl_cache.crl_check(client_pem)
@@ -158,72 +158,13 @@ def test_expired_crl_passes_with_failover_config_true(
client_cert = make_x509(rsa_key(), signer_key=ca_key, cn="chewbacca")
client_pem = client_cert.public_bytes(Encoding.PEM)
crl_dir = os.path.dirname(expired_crl_file)
crl_cache = CRLCache(ca_file, crl_dir)
crl_list = make_crl_list(client_cert, expired_crl_file)
crl_cache = CRLCache(ca_file, crl_dir, crl_list=crl_list)
assert crl_cache.crl_check(client_pem)
def test_updates_expired_certs(
rsa_key, ca_file, expired_crl_file, crl_file, ca_key, make_x509
):
"""
Given a CRLCache object with an expired CRL and a function for updating the
CRLs, the CRLCache should run the update function before checking a
certificate that requires the expired CRL.
"""
client_cert = make_x509(rsa_key(), signer_key=ca_key, cn="chewbacca")
client_pem = client_cert.public_bytes(Encoding.PEM)
def _crl_update_func():
shutil.copyfile(crl_file, expired_crl_file)
crl_dir = os.path.dirname(expired_crl_file)
crl_cache = CRLCache(ca_file, crl_dir, crl_update_func=_crl_update_func)
crl_cache.crl_check(client_pem)
def test_scan_for_issuer_and_next_update(crl_file):
parsed = parse_for_issuer_and_next_update(crl_file)
scanned = scan_for_issuer_and_next_update(crl_file)
assert parsed == scanned
@pytest.fixture
def bad_crl(tmpdir):
bad_file = tmpdir.join("bad.crl")
with open(bad_file, "wb") as bad:
bad.write(b"definitely not a crl")
return bad_file
def test_scan_for_issuer_and_next_update_with_bad_data(bad_crl):
with pytest.raises(CRLParseError):
scan_for_issuer_and_next_update(bad_crl)
def test_build_crl_locations_cache(crl_file):
issuer_der, next_update = parse_for_issuer_and_next_update(crl_file)
cache = build_crl_locations_cache([crl_file])
assert cache == {issuer_der: {"location": crl_file, "expires": next_update}}
def test_build_crl_locations_cache_with_bad_data(crl_file, bad_crl):
logger = FakeLogger()
issuer_der, next_update = parse_for_issuer_and_next_update(crl_file)
cache = build_crl_locations_cache([crl_file, bad_crl], logger=logger)
assert cache == {issuer_der: {"location": crl_file, "expires": next_update}}
assert logger.messages
assert str(bad_crl) in logger.messages[0]
def test_serialize_crl_locations_cache(crl_file, bad_crl):
dir_ = os.path.dirname(crl_file)
serialize_crl_locations_cache(dir_)
assert os.path.isfile("{}/{}".format(dir_, JSON_CACHE))
def test_load_crl_locations_cache(crl_file, bad_crl):
def test_load_crl_locations_cache(crl_file):
dir_ = os.path.dirname(crl_file)
serialize_crl_locations_cache(dir_)
cache = load_crl_locations_cache(dir_)

View File

@@ -6,14 +6,16 @@ from datetime import datetime
from flask import session, url_for
from cryptography.hazmat.primitives.serialization import Encoding
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
from atst.domain.users import Users
from atst.domain.permission_sets import PermissionSets
from atst.domain.exceptions import NotFoundError
from atst.domain.authnid.crl import CRLInvalidException
from atst.domain.auth import UNPROTECTED_ROUTES
from atst.domain.authnid.crl import CRLCache
from .factories import UserFactory
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
from .utils import make_crl_list
MOCK_USER = {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"}
@@ -149,7 +151,8 @@ def swap_crl_cache(
crl = make_crl(ca_key)
serialize_pki_object_to_disk(crl, crl_file, encoding=Encoding.DER)
crl_dir = os.path.dirname(crl_file)
app.crl_cache = CRLCache(ca_file, crl_dir)
crl_list = make_crl_list(crl, crl_file)
app.crl_cache = CRLCache(ca_file, crl_dir, crl_list=crl_list)
yield _swap_crl_cache
@@ -175,7 +178,8 @@ def test_crl_validation_on_login(
serialize_pki_object_to_disk(crl, crl_file, encoding=Encoding.DER)
crl_dir = os.path.dirname(crl_file)
cache = CRLCache(ca_file, crl_dir)
crl_list = make_crl_list(good_cert, crl_file)
cache = CRLCache(ca_file, crl_dir, crl_list=crl_list)
swap_crl_cache(cache)
# bad cert is on the test CRL

View File

@@ -1,7 +1,10 @@
from flask import template_rendered
from contextlib import contextmanager
import os
from unittest.mock import Mock
from OpenSSL import crypto
from cryptography.hazmat.backends import default_backend
from flask import template_rendered
from atst.utils.notification_sender import NotificationSender
@@ -49,5 +52,10 @@ FakeNotificationSender = lambda: Mock(spec=NotificationSender)
def parse_for_issuer_and_next_update(crl):
with open(crl, "rb") as crl_file:
parsed = crypto.load_crl(crypto.FILETYPE_ASN1, crl_file.read())
next_update = parsed.to_cryptography().next_update
return (parsed.get_issuer().der(), next_update)
return parsed.get_issuer().der()
def make_crl_list(x509_obj, x509_path):
issuer = x509_obj.issuer.public_bytes(default_backend())
filename = os.path.basename(x509_path)
return [(filename, issuer.hex())]