1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
|
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
import datetime
import ipaddress
import json
import os
import pytest
from cryptography import x509
from cryptography.x509 import load_pem_x509_certificate
from cryptography.x509.verification import (
ClientVerifier,
PolicyBuilder,
ServerVerifier,
Store,
VerificationError,
)
LIMBO_UNSUPPORTED_FEATURES = {
# NOTE: Path validation is required to reject wildcards on public suffixes,
# however this isn't practical and most implementations make no attempt to
# comply with this.
"pedantic-public-suffix-wildcard",
# TODO: We don't support Distinguished Name Constraints yet.
"name-constraint-dn",
# Our support for custom EKUs is limited, and we (like most impls.) don't
# handle all EKU conditions under CABF.
"pedantic-webpki-eku",
# Most CABF validators do not enforce the CABF key requirements on
# subscriber keys (i.e., in the leaf certificate).
"pedantic-webpki-subscriber-key",
# Tests that fail based on a strict reading of RFC 5280
# but are widely ignored by validators.
"pedantic-rfc5280",
# In rare circumstances, CABF relaxes RFC 5280's prescriptions in
# incompatible ways. Our validator always tries (by default) to comply
# closer to CABF, so we skip these.
"rfc5280-incompatible-with-webpki",
# We do not support policy constraints.
"has-policy-constraints",
}
LIMBO_SKIP_TESTCASES = {
# We unconditionally count intermediate certificates for pathlen and max
# depth constraint purposes, even when self-issued.
# This is a violation of RFC 5280, but is consistent with Go's crypto/x509
# and Rust's webpki crate do.
"pathlen::self-issued-certs-pathlen",
"pathlen::max-chain-depth-1-self-issued",
# We allow certificates with serial numbers of zero. This is
# invalid under RFC 5280 but is widely violated by certs in common
# trust stores.
"rfc5280::serial::zero",
# We allow CAs that don't have AKIs, which is forbidden under
# RFC 5280. This is consistent with what Go's crypto/x509 and Rust's
# webpki crate do.
"rfc5280::ski::root-missing-ski",
"rfc5280::ski::intermediate-missing-ski",
# We currently allow intermediate CAs that don't have AKIs, which
# is technically forbidden under CABF. This is consistent with what
# Go's crypto/x509 and Rust's webpki crate do.
"rfc5280::aki::intermediate-missing-aki",
# We allow root CAs where the AKI and SKI mismatch, which is technically
# forbidden under CABF. This is consistent with what
# Go's crypto/x509 and Rust's webpki crate do.
"webpki::aki::root-with-aki-ski-mismatch",
# We allow root CAs where the AKI contains fields other than keyIdentifier,
# which is technically forbidden under CABF. No other implementations
# enforce this requirement.
"webpki::aki::root-with-aki-authoritycertissuer",
"webpki::aki::root-with-aki-authoritycertserialnumber",
"webpki::aki::root-with-aki-all-fields",
# We allow RSA keys that aren't divisible by 8, which is technically
# forbidden under CABF. No other implementation checks this either.
"webpki::forbidden-rsa-not-divisable-by-8-in-root",
# We disallow CAs in the leaf position, which is explicitly forbidden
# by CABF (but implicitly permitted under RFC 5280). This is consistent
# with what webpki and rustls do, but inconsistent with Go and OpenSSL.
"rfc5280::ca-as-leaf",
"pathlen::validation-ignores-pathlen-in-leaf",
}
def _get_limbo_peer(expected_peer):
kind = expected_peer["kind"]
assert kind in ("DNS", "IP", "RFC822")
value = expected_peer["value"]
if kind == "DNS":
return x509.DNSName(value)
elif kind == "IP":
return x509.IPAddress(ipaddress.ip_address(value))
else:
return x509.RFC822Name(value)
def _limbo_testcase(id_, testcase):
if id_ in LIMBO_SKIP_TESTCASES:
pytest.skip(f"explicitly skipped testcase: {id_}")
features = testcase["features"]
unsupported = LIMBO_UNSUPPORTED_FEATURES.intersection(features)
if unsupported:
pytest.skip(f"explicitly skipped features: {unsupported}")
assert testcase["signature_algorithms"] == []
trusted_certs = [
load_pem_x509_certificate(cert.encode())
for cert in testcase["trusted_certs"]
]
untrusted_intermediates = [
load_pem_x509_certificate(cert.encode())
for cert in testcase["untrusted_intermediates"]
]
peer_certificate = load_pem_x509_certificate(
testcase["peer_certificate"].encode()
)
validation_time = testcase["validation_time"]
validation_time = (
datetime.datetime.fromisoformat(validation_time)
if validation_time is not None
else None
)
max_chain_depth = testcase["max_chain_depth"]
should_pass = testcase["expected_result"] == "SUCCESS"
builder = PolicyBuilder().store(Store(trusted_certs))
if validation_time is not None:
builder = builder.time(validation_time)
if max_chain_depth is not None:
builder = builder.max_chain_depth(max_chain_depth)
verifier: ServerVerifier | ClientVerifier
if testcase["validation_kind"] == "SERVER":
assert testcase["extended_key_usage"] == [] or testcase[
"extended_key_usage"
] == ["serverAuth"]
peer_name = _get_limbo_peer(testcase["expected_peer_name"])
# Some tests exercise invalid leaf SANs, which get caught before
# validation even begins.
try:
verifier = builder.build_server_verifier(peer_name)
except ValueError:
assert not should_pass
return
else:
assert testcase["extended_key_usage"] == ["clientAuth"]
verifier = builder.build_client_verifier()
if should_pass:
if isinstance(verifier, ServerVerifier):
built_chain = verifier.verify(
peer_certificate, untrusted_intermediates
)
else:
verified_client = verifier.verify(
peer_certificate, untrusted_intermediates
)
expected_subjects = [
_get_limbo_peer(p) for p in testcase["expected_peer_names"]
]
assert expected_subjects == verified_client.subjects
built_chain = verified_client.chain
# Assert that the verifier returns chains in [EE, ..., TA] order.
assert built_chain[0] == peer_certificate
for intermediate in built_chain[1:-1]:
assert intermediate in untrusted_intermediates
assert built_chain[-1] in trusted_certs
else:
with pytest.raises(VerificationError):
verifier.verify(peer_certificate, untrusted_intermediates)
def test_limbo(subtests, pytestconfig):
limbo_root = pytestconfig.getoption("--x509-limbo-root", skip=True)
limbo_path = os.path.join(limbo_root, "limbo.json")
with open(limbo_path, mode="rb") as limbo_file:
limbo = json.load(limbo_file)
testcases = limbo["testcases"]
for testcase in testcases:
with subtests.test():
# NOTE: Pass in the id separately to make pytest
# error renderings slightly nicer.
_limbo_testcase(testcase["id"], testcase)
|