rustdesk/vendor/rustls-webpki-0.101.7/tests/generate.py

1672 lines
61 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Generates test cases that aim to validate name constraints and other
name-related parts of webpki.
Run this script from tests/. It edits the bottom part of some .rs files and
drops testcase data into subdirectories as required.
"""
import argparse
import os
from typing import TextIO, Optional, Union, Any, Callable, Iterable, List
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519, padding
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
import ipaddress
import datetime
import subprocess
ROOT_PRIVATE_KEY: rsa.RSAPrivateKey = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
ROOT_PUBLIC_KEY: rsa.RSAPublicKey = ROOT_PRIVATE_KEY.public_key()
NOT_BEFORE: datetime.datetime = datetime.datetime.utcfromtimestamp(0x1FEDF00D - 30)
NOT_AFTER: datetime.datetime = datetime.datetime.utcfromtimestamp(0x1FEDF00D + 30)
ANY_PRIV_KEY = Union[
ed25519.Ed25519PrivateKey | ec.EllipticCurvePrivateKey | rsa.RSAPrivateKey
]
ANY_PUB_KEY = Union[
ed25519.Ed25519PublicKey | ec.EllipticCurvePublicKey | rsa.RSAPublicKey
]
SIGNER = Callable[
[Any, bytes], Any
] # Note: a bit loosey-goosey here but good enough for tests.
def trim_top(file_name: str) -> TextIO:
"""
Reads `file_name`, then writes lines up to a particular comment (the "top"
of the file) back to it and returns the file object for further writing.
"""
with open(file_name, "r") as f:
top = f.readlines()
top = top[: top.index("// DO NOT EDIT BELOW: generated by tests/generate.py\n") + 1]
output = open(file_name, "w")
for line in top:
output.write(line)
return output
def key_or_generate(key: Optional[ANY_PRIV_KEY] = None) -> ANY_PRIV_KEY:
return (
key
if key is not None
else rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend(),
)
)
def write_der(path: str, content: bytes, force: bool) -> None:
# Avoid churn from regenerating existing on-disk resources unless force is enabled.
out_path = Path(path)
if out_path.exists() and not force:
return None
with out_path.open("wb") as f:
f.write(content)
def end_entity_cert(
*,
subject_name: x509.Name,
issuer_name: x509.Name,
issuer_key: Optional[ANY_PRIV_KEY] = None,
subject_key: Optional[ANY_PRIV_KEY] = None,
sans: Optional[Iterable[x509.GeneralName]] = None,
ekus: Optional[Iterable[x509.ObjectIdentifier]] = None,
serial: Optional[int] = None,
) -> x509.Certificate:
subject_priv_key = key_or_generate(subject_key)
subject_key_pub: ANY_PUB_KEY = subject_priv_key.public_key()
ee_builder: x509.CertificateBuilder = x509.CertificateBuilder()
ee_builder = ee_builder.subject_name(subject_name)
ee_builder = ee_builder.issuer_name(issuer_name)
ee_builder = ee_builder.not_valid_before(NOT_BEFORE)
ee_builder = ee_builder.not_valid_after(NOT_AFTER)
ee_builder = ee_builder.serial_number(
x509.random_serial_number() if serial is None else serial
)
ee_builder = ee_builder.public_key(subject_key_pub)
if sans:
ee_builder = ee_builder.add_extension(
x509.SubjectAlternativeName(sans), critical=False
)
if ekus:
ee_builder = ee_builder.add_extension(
x509.ExtendedKeyUsage(ekus), critical=False
)
ee_builder = ee_builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
return ee_builder.sign(
private_key=issuer_key if issuer_key is not None else ROOT_PRIVATE_KEY,
algorithm=hashes.SHA256(),
backend=default_backend(),
)
def subject_name_for_test(subject_cn: str, test_name: str) -> x509.Name:
return x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, test_name),
]
)
def issuer_name_for_test(test_name: str) -> x509.Name:
return x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, "issuer.example.com"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, test_name),
]
)
def ca_cert(
*,
subject_name: x509.Name,
subject_key: Optional[ANY_PRIV_KEY] = None,
issuer_name: Optional[x509.Name] = None,
issuer_key: Optional[ANY_PRIV_KEY] = None,
permitted_subtrees: Optional[Iterable[x509.GeneralName]] = None,
excluded_subtrees: Optional[Iterable[x509.GeneralName]] = None,
key_usage: Optional[x509.KeyUsage] = None,
) -> x509.Certificate:
subject_priv_key = key_or_generate(subject_key)
subject_key_pub: ANY_PUB_KEY = subject_priv_key.public_key()
ca_builder: x509.CertificateBuilder = x509.CertificateBuilder()
ca_builder = ca_builder.subject_name(subject_name)
ca_builder = ca_builder.issuer_name(issuer_name if issuer_name else subject_name)
ca_builder = ca_builder.not_valid_before(NOT_BEFORE)
ca_builder = ca_builder.not_valid_after(NOT_AFTER)
ca_builder = ca_builder.serial_number(x509.random_serial_number())
ca_builder = ca_builder.public_key(subject_key_pub)
ca_builder = ca_builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
)
if permitted_subtrees is not None or excluded_subtrees is not None:
ca_builder = ca_builder.add_extension(
x509.NameConstraints(permitted_subtrees, excluded_subtrees), critical=True
)
if key_usage is not None:
ca_builder = ca_builder.add_extension(
key_usage,
critical=True,
)
return ca_builder.sign(
private_key=issuer_key if issuer_key else subject_priv_key,
algorithm=hashes.SHA256(),
backend=default_backend(),
)
def generate_tls_server_cert_test(
output: TextIO,
test_name: str,
expected_error: Optional[str] = None,
subject_common_name: Optional[str] = None,
extra_subject_names: Optional[List[x509.NameAttribute]] = None,
valid_names: Optional[List[str]] = None,
invalid_names: Optional[List[str]] = None,
sans: Optional[Iterable[x509.GeneralName]] = None,
permitted_subtrees: Optional[Iterable[x509.GeneralName]] = None,
excluded_subtrees: Optional[Iterable[x509.GeneralName]] = None,
force: bool = False,
) -> None:
"""
Generate a test case, writing a rust '#[test]' function into
tls_server_certs.rs, and writing supporting files into the current
directory.
- `test_name`: name of the test, must be a rust identifier.
- `expected_error`: item in `webpki::Error` enum, expected error from
webpki `verify_is_valid_tls_server_cert` function. Leave absent to
expect success.
- `subject_common_name`: optional string to put in end-entity certificate
subject common name.
- `extra_subject_names`: optional sequence of `x509.NameAttributes` to add
to end-entity certificate subject.
- `valid_names`: optional sequence of valid names that the end-entity
certificate is expected to pass `verify_is_valid_for_subject_name` for.
- `invalid_names`: optional sequence of invalid names that the end-entity
certificate is expected to fail `verify_is_valid_for_subject_name` with
`CertNotValidForName`.
- `sans`: optional sequence of `x509.GeneralName`s that are the contents of
the subjectAltNames extension. If empty or not provided the end-entity
certificate does not have a subjectAltName extension.
- `permitted_subtrees`: optional sequence of `x509.GeneralName`s that are
the `permittedSubtrees` contents of the `nameConstraints` extension.
If this and `excluded_subtrees` are empty/absent then the end-entity
certificate does not have a `nameConstraints` extension.
- `excluded_subtrees`: optional sequence of `x509.GeneralName`s that are
the `excludedSubtrees` contents of the `nameConstraints` extension.
If this and `permitted_subtrees` are both empty/absent then the
end-entity certificate does not have a `nameConstraints` extension.
"""
if invalid_names is None:
invalid_names = []
if valid_names is None:
valid_names = []
if extra_subject_names is None:
extra_subject_names = []
issuer_name: x509.Name = issuer_name_for_test(test_name)
# end-entity
ee_subject = x509.Name(
(
[x509.NameAttribute(NameOID.COMMON_NAME, subject_common_name)]
if subject_common_name
else []
)
+ [x509.NameAttribute(NameOID.ORGANIZATION_NAME, test_name)]
+ extra_subject_names
)
ee_certificate: x509.Certificate = end_entity_cert(
subject_name=ee_subject,
issuer_name=issuer_name,
sans=sans,
)
output_dir: str = "tls_server_certs"
ee_cert_path: str = os.path.join(output_dir, f"{test_name}.ee.der")
ca_cert_path: str = os.path.join(output_dir, f"{test_name}.ca.der")
if not os.path.isdir(output_dir):
os.mkdir(output_dir)
write_der(ee_cert_path, ee_certificate.public_bytes(Encoding.DER), force)
# issuer
ca: x509.Certificate = ca_cert(
subject_name=issuer_name,
subject_key=ROOT_PRIVATE_KEY,
permitted_subtrees=permitted_subtrees,
excluded_subtrees=excluded_subtrees,
)
write_der(ca_cert_path, ca.public_bytes(Encoding.DER), force)
expected: str = ""
if expected_error is None:
expected = "Ok(())"
else:
expected = "Err(webpki::Error::" + expected_error + ")"
valid_names_str: str = ", ".join('"' + name + '"' for name in valid_names)
invalid_names_str: str = ", ".join('"' + name + '"' for name in invalid_names)
print(
"""
#[test]
fn %(test_name)s() {
let ee = include_bytes!("%(ee_cert_path)s");
let ca = include_bytes!("%(ca_cert_path)s");
assert_eq!(
check_cert(ee, ca, &[%(valid_names_str)s], &[%(invalid_names_str)s]),
%(expected)s
);
}"""
% locals(),
file=output,
)
def tls_server_certs(force: bool) -> None:
with trim_top("tls_server_certs.rs") as output:
generate_tls_server_cert_test(
output,
"no_name_constraints",
subject_common_name="subject.example.com",
valid_names=["dns.example.com"],
invalid_names=["subject.example.com"],
sans=[x509.DNSName("dns.example.com")],
)
generate_tls_server_cert_test(
output,
"additional_dns_labels",
subject_common_name="subject.example.com",
valid_names=["host1.example.com", "host2.example.com"],
invalid_names=["subject.example.com"],
sans=[x509.DNSName("host1.example.com"), x509.DNSName("host2.example.com")],
permitted_subtrees=[x509.DNSName(".example.com")],
)
generate_tls_server_cert_test(
output,
"disallow_dns_san",
expected_error="NameConstraintViolation",
sans=[x509.DNSName("disallowed.example.com")],
excluded_subtrees=[x509.DNSName("disallowed.example.com")],
)
generate_tls_server_cert_test(
output,
"allow_subject_common_name",
subject_common_name="allowed.example.com",
invalid_names=["allowed.example.com"],
permitted_subtrees=[x509.DNSName("allowed.example.com")],
)
generate_tls_server_cert_test(
output,
"allow_dns_san",
valid_names=["allowed.example.com"],
sans=[x509.DNSName("allowed.example.com")],
permitted_subtrees=[x509.DNSName("allowed.example.com")],
)
generate_tls_server_cert_test(
output,
"allow_dns_san_and_subject_common_name",
valid_names=["allowed-san.example.com"],
invalid_names=["allowed-cn.example.com"],
sans=[x509.DNSName("allowed-san.example.com")],
subject_common_name="allowed-cn.example.com",
permitted_subtrees=[
x509.DNSName("allowed-san.example.com"),
x509.DNSName("allowed-cn.example.com"),
],
)
generate_tls_server_cert_test(
output,
"disallow_dns_san_and_allow_subject_common_name",
expected_error="NameConstraintViolation",
sans=[
x509.DNSName("allowed-san.example.com"),
x509.DNSName("disallowed-san.example.com"),
],
subject_common_name="allowed-cn.example.com",
permitted_subtrees=[
x509.DNSName("allowed-san.example.com"),
x509.DNSName("allowed-cn.example.com"),
],
excluded_subtrees=[x509.DNSName("disallowed-san.example.com")],
)
# XXX: ideally this test case would be a negative one, because the name constraints
# should apply to the subject name.
# however, because we don't look at email addresses in subjects, it is accepted.
generate_tls_server_cert_test(
output,
"we_incorrectly_ignore_name_constraints_on_name_in_subject",
extra_subject_names=[
x509.NameAttribute(NameOID.EMAIL_ADDRESS, "joe@notexample.com")
],
permitted_subtrees=[x509.RFC822Name("example.com")],
)
# this does work, however, because we process all SANs
generate_tls_server_cert_test(
output,
"reject_constraints_on_unimplemented_names",
expected_error="NameConstraintViolation",
sans=[x509.RFC822Name("joe@example.com")],
permitted_subtrees=[x509.RFC822Name("example.com")],
)
# RFC5280 4.2.1.10:
# "If no name of the type is in the certificate,
# the certificate is acceptable."
generate_tls_server_cert_test(
output,
"we_ignore_constraints_on_names_that_do_not_appear_in_cert",
sans=[x509.DNSName("notexample.com")],
valid_names=["notexample.com"],
invalid_names=["example.com"],
permitted_subtrees=[x509.RFC822Name("example.com")],
)
generate_tls_server_cert_test(
output,
"wildcard_san_accepted_if_in_subtree",
sans=[x509.DNSName("*.example.com")],
valid_names=["bob.example.com", "jane.example.com"],
invalid_names=["example.com", "uh.oh.example.com"],
permitted_subtrees=[x509.DNSName("example.com")],
)
generate_tls_server_cert_test(
output,
"wildcard_san_rejected_if_in_excluded_subtree",
expected_error="NameConstraintViolation",
sans=[x509.DNSName("*.example.com")],
excluded_subtrees=[x509.DNSName("example.com")],
)
generate_tls_server_cert_test(
output,
"ip4_address_san_rejected_if_in_excluded_subtree",
expected_error="NameConstraintViolation",
sans=[x509.IPAddress(ipaddress.ip_address("12.34.56.78"))],
excluded_subtrees=[x509.IPAddress(ipaddress.ip_network("12.34.56.0/24"))],
)
generate_tls_server_cert_test(
output,
"ip4_address_san_allowed_if_outside_excluded_subtree",
valid_names=["12.34.56.78"],
sans=[x509.IPAddress(ipaddress.ip_address("12.34.56.78"))],
excluded_subtrees=[x509.IPAddress(ipaddress.ip_network("12.34.56.252/30"))],
)
sparse_net_addr = ipaddress.ip_network("12.34.56.78/24", strict=False)
sparse_net_addr.netmask = ipaddress.ip_address("255.255.255.1")
generate_tls_server_cert_test(
output,
"ip4_address_san_rejected_if_excluded_is_sparse_cidr_mask",
expected_error="InvalidNetworkMaskConstraint",
sans=[
# inside excluded network, if netmask is allowed to be sparse
x509.IPAddress(ipaddress.ip_address("12.34.56.79")),
],
excluded_subtrees=[x509.IPAddress(sparse_net_addr)],
)
generate_tls_server_cert_test(
output,
"ip4_address_san_allowed",
valid_names=["12.34.56.78"],
invalid_names=[
"12.34.56.77",
"12.34.56.79",
"0000:0000:0000:0000:0000:ffff:0c22:384e",
],
sans=[x509.IPAddress(ipaddress.ip_address("12.34.56.78"))],
permitted_subtrees=[x509.IPAddress(ipaddress.ip_network("12.34.56.0/24"))],
)
generate_tls_server_cert_test(
output,
"ip6_address_san_rejected_if_in_excluded_subtree",
expected_error="NameConstraintViolation",
sans=[x509.IPAddress(ipaddress.ip_address("2001:db8::1"))],
excluded_subtrees=[x509.IPAddress(ipaddress.ip_network("2001:db8::/48"))],
)
generate_tls_server_cert_test(
output,
"ip6_address_san_allowed_if_outside_excluded_subtree",
valid_names=["2001:0db9:0000:0000:0000:0000:0000:0001"],
sans=[x509.IPAddress(ipaddress.ip_address("2001:db9::1"))],
excluded_subtrees=[x509.IPAddress(ipaddress.ip_network("2001:db8::/48"))],
)
generate_tls_server_cert_test(
output,
"ip6_address_san_allowed",
valid_names=["2001:0db9:0000:0000:0000:0000:0000:0001"],
invalid_names=["12.34.56.78"],
sans=[x509.IPAddress(ipaddress.ip_address("2001:db9::1"))],
permitted_subtrees=[x509.IPAddress(ipaddress.ip_network("2001:db9::/48"))],
)
generate_tls_server_cert_test(
output,
"ip46_mixed_address_san_allowed",
valid_names=["12.34.56.78", "2001:0db9:0000:0000:0000:0000:0000:0001"],
invalid_names=[
"12.34.56.77",
"12.34.56.79",
"0000:0000:0000:0000:0000:ffff:0c22:384e",
],
sans=[
x509.IPAddress(ipaddress.ip_address("12.34.56.78")),
x509.IPAddress(ipaddress.ip_address("2001:db9::1")),
],
permitted_subtrees=[
x509.IPAddress(ipaddress.ip_network("12.34.56.0/24")),
x509.IPAddress(ipaddress.ip_network("2001:db9::/48")),
],
)
generate_tls_server_cert_test(
output,
"permit_directory_name_not_implemented",
expected_error="NameConstraintViolation",
permitted_subtrees=[
x509.DirectoryName(
x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "CN")])
)
],
)
generate_tls_server_cert_test(
output,
"exclude_directory_name_not_implemented",
expected_error="NameConstraintViolation",
excluded_subtrees=[
x509.DirectoryName(
x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "CN")])
)
],
)
generate_tls_server_cert_test(
output,
"invalid_dns_name_matching",
valid_names=["dns.example.com"],
subject_common_name="subject.example.com",
sans=[
x509.DNSName("{invalid}.example.com"),
x509.DNSName("dns.example.com"),
],
)
def signatures(force: bool) -> None:
rsa_pub_exponent: int = 0x10001
backend: Any = default_backend()
all_key_types: dict[str, ANY_PRIV_KEY] = {
"ed25519": ed25519.Ed25519PrivateKey.generate(),
"ecdsa_p256": ec.generate_private_key(ec.SECP256R1(), backend),
"ecdsa_p384": ec.generate_private_key(ec.SECP384R1(), backend),
"ecdsa_p521_not_supported": ec.generate_private_key(ec.SECP521R1(), backend),
"rsa_1024_not_supported": rsa.generate_private_key(
rsa_pub_exponent, 1024, backend
),
"rsa_2048": rsa.generate_private_key(rsa_pub_exponent, 2048, backend),
"rsa_3072": rsa.generate_private_key(rsa_pub_exponent, 3072, backend),
"rsa_4096": rsa.generate_private_key(rsa_pub_exponent, 4096, backend),
}
rsa_types: list[str] = [
"RSA_PKCS1_2048_8192_SHA256",
"RSA_PKCS1_2048_8192_SHA384",
"RSA_PKCS1_2048_8192_SHA512",
"RSA_PSS_2048_8192_SHA256_LEGACY_KEY",
"RSA_PSS_2048_8192_SHA384_LEGACY_KEY",
"RSA_PSS_2048_8192_SHA512_LEGACY_KEY",
]
webpki_algs: dict[str, Iterable[str]] = {
"ed25519": ["ED25519"],
"ecdsa_p256": ["ECDSA_P256_SHA384", "ECDSA_P256_SHA256"],
"ecdsa_p384": ["ECDSA_P384_SHA384", "ECDSA_P384_SHA256"],
"rsa_2048": rsa_types,
"rsa_3072": rsa_types + ["RSA_PKCS1_3072_8192_SHA384"],
"rsa_4096": rsa_types + ["RSA_PKCS1_3072_8192_SHA384"],
}
pss_sha256: padding.PSS = padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), salt_length=32
)
pss_sha384: padding.PSS = padding.PSS(
mgf=padding.MGF1(hashes.SHA384()), salt_length=48
)
pss_sha512: padding.PSS = padding.PSS(
mgf=padding.MGF1(hashes.SHA512()), salt_length=64
)
how_to_sign: dict[str, SIGNER] = {
"ED25519": lambda key, message: key.sign(message),
"ECDSA_P256_SHA256": lambda key, message: key.sign(
message, ec.ECDSA(hashes.SHA256())
),
"ECDSA_P256_SHA384": lambda key, message: key.sign(
message, ec.ECDSA(hashes.SHA384())
),
"ECDSA_P384_SHA256": lambda key, message: key.sign(
message, ec.ECDSA(hashes.SHA256())
),
"ECDSA_P384_SHA384": lambda key, message: key.sign(
message, ec.ECDSA(hashes.SHA384())
),
"RSA_PKCS1_2048_8192_SHA256": lambda key, message: key.sign(
message, padding.PKCS1v15(), hashes.SHA256()
),
"RSA_PKCS1_2048_8192_SHA384": lambda key, message: key.sign(
message, padding.PKCS1v15(), hashes.SHA384()
),
"RSA_PKCS1_2048_8192_SHA512": lambda key, message: key.sign(
message, padding.PKCS1v15(), hashes.SHA512()
),
"RSA_PKCS1_3072_8192_SHA384": lambda key, message: key.sign(
message, padding.PKCS1v15(), hashes.SHA384()
),
"RSA_PSS_2048_8192_SHA256_LEGACY_KEY": lambda key, message: key.sign(
message, pss_sha256, hashes.SHA256()
),
"RSA_PSS_2048_8192_SHA384_LEGACY_KEY": lambda key, message: key.sign(
message, pss_sha384, hashes.SHA384()
),
"RSA_PSS_2048_8192_SHA512_LEGACY_KEY": lambda key, message: key.sign(
message, pss_sha512, hashes.SHA512()
),
}
output_dir: str = "signatures"
if not os.path.isdir(output_dir):
os.mkdir(output_dir)
message = b"hello world!"
message_path: str = os.path.join(output_dir, "message.bin")
write_der(message_path, message, force)
def _cert_path(cert_type: str) -> str:
return os.path.join(output_dir, f"{cert_type}.ee.der")
for name, private_key in all_key_types.items():
ee_subject = x509.Name(
[x509.NameAttribute(NameOID.ORGANIZATION_NAME, name + " test")]
)
issuer_subject = x509.Name(
[x509.NameAttribute(NameOID.ORGANIZATION_NAME, name + " issuer")]
)
certificate: x509.Certificate = end_entity_cert(
subject_name=ee_subject,
subject_key=private_key,
issuer_name=issuer_subject,
)
write_der(_cert_path(name), certificate.public_bytes(Encoding.DER), force)
def _test(
test_name: str, cert_type: str, algorithm: str, signature: bytes, expected: str
) -> None:
nonlocal message_path
cert_path: str = _cert_path(cert_type)
lower_test_name: str = test_name.lower()
sig_path: str = os.path.join(output_dir, f"{lower_test_name}.sig.bin")
write_der(sig_path, signature, force)
print(
"""
#[test]
#[cfg(feature = "alloc")]
fn %(lower_test_name)s() {
let ee = include_bytes!("%(cert_path)s");
let message = include_bytes!("%(message_path)s");
let signature = include_bytes!("%(sig_path)s");
assert_eq!(
check_sig(ee, &webpki::%(algorithm)s, message, signature),
%(expected)s
);
}"""
% locals(),
file=output,
)
def good_signature(
test_name: str, cert_type: str, algorithm: str, signer: SIGNER
) -> None:
signature: bytes = signer(all_key_types[cert_type], message)
_test(test_name, cert_type, algorithm, signature, expected="Ok(())")
def good_signature_but_rejected(
test_name: str, cert_type: str, algorithm: str, signer: SIGNER
) -> None:
signature: bytes = signer(all_key_types[cert_type], message)
_test(
test_name,
cert_type,
algorithm,
signature,
expected="Err(webpki::Error::InvalidSignatureForPublicKey)",
)
def bad_signature(
test_name: str, cert_type: str, algorithm: str, signer: SIGNER
) -> None:
signature: bytes = signer(all_key_types[cert_type], message + b"?")
_test(
test_name,
cert_type,
algorithm,
signature,
expected="Err(webpki::Error::InvalidSignatureForPublicKey)",
)
def bad_algorithms_for_key(
test_name: str, cert_type: str, unusable_algs: set[str]
) -> None:
cert_path: str = _cert_path(cert_type)
test_name_lower: str = test_name.lower()
unusable_algs_str: str = ", ".join(
"&webpki::" + alg for alg in sorted(unusable_algs)
)
print(
"""
#[test]
#[cfg(feature = "alloc")]
fn %(test_name_lower)s() {
let ee = include_bytes!("%(cert_path)s");
for algorithm in &[ %(unusable_algs_str)s ] {
assert_eq!(
check_sig(ee, algorithm, b"", b""),
Err(webpki::Error::UnsupportedSignatureAlgorithmForPublicKey)
);
}
}"""
% locals(),
file=output,
)
with trim_top("signatures.rs") as output:
# compute all webpki algorithms covered by these tests
all_webpki_algs: set[str] = set(
[item for algs in webpki_algs.values() for item in algs]
)
for type, algs in webpki_algs.items():
for alg in algs:
signer: SIGNER = how_to_sign[alg]
good_signature(
type + "_key_and_" + alg + "_good_signature",
cert_type=type,
algorithm=alg,
signer=signer,
)
bad_signature(
type + "_key_and_" + alg + "_detects_bad_signature",
cert_type=type,
algorithm=alg,
signer=signer,
)
unusable_algs = set(all_webpki_algs)
for alg in algs:
unusable_algs.remove(alg)
# special case: tested separately below
if type == "rsa_2048":
unusable_algs.remove("RSA_PKCS1_3072_8192_SHA384")
bad_algorithms_for_key(
type + "_key_rejected_by_other_algorithms",
cert_type=type,
unusable_algs=unusable_algs,
)
good_signature_but_rejected(
"rsa_2048_key_rejected_by_RSA_PKCS1_3072_8192_SHA384",
cert_type="rsa_2048",
algorithm="RSA_PKCS1_3072_8192_SHA384",
signer=signer,
)
def generate_client_auth_test(
output: TextIO,
test_name: str,
ekus: Optional[Iterable[x509.ObjectIdentifier]],
expected_error: Optional[str] = None,
force: bool = False,
) -> None:
issuer_name: x509.Name = issuer_name_for_test(test_name)
# end-entity
ee_subject: x509.Name = x509.Name(
[x509.NameAttribute(NameOID.ORGANIZATION_NAME, test_name)]
)
ee_certificate: x509.Certificate = end_entity_cert(
subject_name=ee_subject,
ekus=ekus,
issuer_name=issuer_name,
)
output_dir: str = "client_auth"
if not os.path.isdir(output_dir):
os.mkdir(output_dir)
ee_cert_path: str = os.path.join(output_dir, f"{test_name}.ee.der")
write_der(ee_cert_path, ee_certificate.public_bytes(Encoding.DER), force)
# issuer
ca: x509.Certificate = ca_cert(
subject_name=issuer_name, subject_key=ROOT_PRIVATE_KEY
)
ca_cert_path: str = os.path.join(output_dir, f"{test_name}.ca.der")
write_der(ca_cert_path, ca.public_bytes(Encoding.DER), force)
expected: str = ""
if expected_error is None:
expected = "Ok(())"
else:
expected = "Err(webpki::Error::" + expected_error + ")"
print(
"""
#[test]
#[cfg(feature = "alloc")]
fn %(test_name)s() {
let ee = include_bytes!("%(ee_cert_path)s");
let ca = include_bytes!("%(ca_cert_path)s");
assert_eq!(
check_cert(ee, ca),
%(expected)s
);
}"""
% locals(),
file=output,
)
def client_auth(force: bool) -> None:
with trim_top("client_auth.rs") as output:
generate_client_auth_test(
output, "cert_with_no_eku_accepted_for_client_auth", ekus=None
)
generate_client_auth_test(
output,
"cert_with_clientauth_eku_accepted_for_client_auth",
ekus=[ExtendedKeyUsageOID.CLIENT_AUTH],
)
generate_client_auth_test(
output,
"cert_with_both_ekus_accepted_for_client_auth",
ekus=[ExtendedKeyUsageOID.CLIENT_AUTH, ExtendedKeyUsageOID.SERVER_AUTH],
)
generate_client_auth_test(
output,
"cert_with_serverauth_eku_rejected_for_client_auth",
ekus=[ExtendedKeyUsageOID.SERVER_AUTH],
expected_error="RequiredEkuNotFound",
)
def client_auth_revocation(force: bool) -> None:
output_dir: str = "client_auth_revocation"
if not os.path.isdir(output_dir):
os.mkdir(output_dir)
# KeyUsage for a CA that sets crl_sign.
crl_sign_ku = x509.KeyUsage(
digital_signature=True,
key_cert_sign=True,
crl_sign=True, # NB: crl_sign set.
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False,
)
# KeyUsage for a CA that omits crl_sign.
no_crl_sign_ku = x509.KeyUsage(
digital_signature=True,
key_cert_sign=True,
crl_sign=False, # NB: no crl_sign.
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False,
)
def _chain(
*, chain_name: str, key_usage: Optional[x509.KeyUsage]
) -> list[tuple[x509.Certificate, str, ANY_PRIV_KEY]]:
"""
Generate a short test certificate chain:
ee -> intermediate -> root.
:param chain_name: the name of the certificate chain. Used to generate subject/issuer names and to
choose the filename to write DER contents to disk.
:param key_usage: the KeyUsage to include in the issuer certificates (both the intermediate and the root).
:return: Return a list comprising the chain starting from the end entity and ending at the root trust anchor.
Each entry in the list is a tuple of three values: the x509.Certificate object, the path the DER encoding
was written to, and lastly the corresponding private key.
"""
ee_subj: x509.Name = subject_name_for_test("test.example.com", chain_name)
int_a_subj: x509.Name = issuer_name_for_test(f"int.a.{chain_name}")
int_b_subj: x509.Name = issuer_name_for_test(f"int.b.{chain_name}")
ca_subj: x509.Name = issuer_name_for_test(f"ca.{chain_name}")
ee_key: ec.EllipticCurvePrivateKey = ec.generate_private_key(
ec.SECP256R1(), default_backend()
)
int_a_key: ec.EllipticCurvePrivateKey = ec.generate_private_key(
ec.SECP256R1(), default_backend()
)
int_b_key: ec.EllipticCurvePrivateKey = ec.generate_private_key(
ec.SECP256R1(), default_backend()
)
root_key: ec.EllipticCurvePrivateKey = ec.generate_private_key(
ec.SECP256R1(), default_backend()
)
# EE cert issued by intermediate.
ee_cert: x509.Certificate = end_entity_cert(
subject_name=ee_subj, issuer_name=int_a_subj, issuer_key=int_a_key
)
ee_cert_path: str = os.path.join(output_dir, f"{chain_name}.ee.der")
write_der(ee_cert_path, ee_cert.public_bytes(Encoding.DER), force)
# Second EE cert issued by intermediate, with top-bit set in serial.
ee_cert_topbit: x509.Certificate = end_entity_cert(
subject_name=ee_subj,
issuer_name=int_a_subj,
issuer_key=int_a_key,
serial=0x80DEADBEEFF00D,
)
ee_cert_topbit_path: str = os.path.join(
output_dir, f"{chain_name}.topbit.ee.der"
)
write_der(ee_cert_topbit_path, ee_cert_topbit.public_bytes(Encoding.DER), force)
# intermediate a cert issued by intermediate b.
int_a_cert: x509.Certificate = ca_cert(
subject_name=int_a_subj,
subject_key=int_a_key,
issuer_name=int_b_subj,
issuer_key=int_b_key,
key_usage=key_usage,
)
int_a_cert_path: str = os.path.join(output_dir, f"{chain_name}.int.a.ca.der")
write_der(int_a_cert_path, int_a_cert.public_bytes(Encoding.DER), force)
# intermediate b cert issued by root cert.
int_b_cert: x509.Certificate = ca_cert(
subject_name=int_b_subj,
subject_key=int_b_key,
issuer_name=ca_subj,
issuer_key=root_key,
key_usage=key_usage,
)
int_b_cert_path: str = os.path.join(output_dir, f"{chain_name}.int.b.ca.der")
write_der(int_b_cert_path, int_b_cert.public_bytes(Encoding.DER), force)
# root cert issued by itself.
root_cert: x509.Certificate = ca_cert(
subject_name=ca_subj,
subject_key=root_key,
key_usage=key_usage,
)
root_cert_path: str = os.path.join(output_dir, f"{chain_name}.root.ca.der")
write_der(root_cert_path, root_cert.public_bytes(Encoding.DER), force)
return [
(ee_cert, ee_cert_path, ee_key),
(int_a_cert, int_a_cert_path, int_a_key),
(int_b_cert, int_b_cert_path, int_b_key),
(root_cert, root_cert_path, root_key),
(ee_cert_topbit, ee_cert_topbit_path, ee_key),
]
def _crl(
*,
serials: Iterable[int],
issuer_name: x509.Name,
issuer_key: Optional[ANY_PRIV_KEY],
) -> x509.CertificateRevocationList:
"""
Generate a certificate revocation list.
:param serials: list of serial numbers to include in the CRL as revoked certificates.
:param issuer_name: the name of the CRL issuer.
:param issuer_key: the key used to sign the CRL.
:return: a generated x509.CertificateRevocationList.
"""
issuer_priv_key: ANY_PRIV_KEY = key_or_generate(issuer_key)
crl_builder: x509.CertificateRevocationListBuilder = (
x509.CertificateRevocationListBuilder()
)
crl_builder = crl_builder.issuer_name(issuer_name)
crl_builder = crl_builder.last_update(NOT_BEFORE)
crl_builder = crl_builder.next_update(NOT_AFTER)
for serial in serials:
revoked_cert_builder: x509.RevokedCertificateBuilder = (
x509.RevokedCertificateBuilder()
)
revoked_cert_builder = revoked_cert_builder.serial_number(serial)
revoked_cert_builder = revoked_cert_builder.revocation_date(NOT_BEFORE)
revoked_cert_builder = revoked_cert_builder.add_extension(
x509.CRLReason(x509.ReasonFlags.key_compromise), critical=False
)
crl_builder = crl_builder.add_revoked_certificate(
revoked_cert_builder.build()
)
crl_builder = crl_builder.add_extension(
x509.CRLNumber(x509.random_serial_number()), critical=False
)
return crl_builder.sign(
private_key=issuer_priv_key,
algorithm=hashes.SHA256(),
)
def _revocation_test(
*,
test_name: str,
chain: list[tuple[x509.Certificate, str, ANY_PRIV_KEY]],
crl_paths: Iterable[str],
owned: bool,
expected_error: Optional[str],
ee_topbit_serial: bool = False,
) -> None:
"""
Generate a Rust unit test for a revocation checking scenario and write it to the output file.
:param test_name: the name of the unit test.
:param chain: the certificate chain to use for validation.
:param crl_paths: paths to zero or more CRLs.
:param owned: whether to use the owned or borrowed CRL representation.
:param expected_error: an optional error to expect to be returned from validation.
"""
if len(chain) != 5:
raise RuntimeError("invalid chain length")
ee_cert, ee_cert_path, _ = chain[4] if ee_topbit_serial else chain[0]
int_a_cert, int_a_cert_path, _ = chain[1]
int_b_cert, int_b_cert_path, _ = chain[2]
root_cert, root_cert_path, _ = chain[3]
int_a_str = f'include_bytes!("{int_a_cert_path}").as_slice()'
int_b_str = f'include_bytes!("{int_b_cert_path}").as_slice()'
intermediates_str: str = f"&[{int_a_str}, {int_b_str}]"
owned_convert: str = ".to_owned().unwrap()" if owned else ""
crl_includes: str = "\n".join(
[
f"""
&webpki::BorrowedCertRevocationList::from_der(include_bytes!("{path}").as_slice())
.unwrap()
{owned_convert}
as &dyn webpki::CertRevocationList,
"""
for path in crl_paths
]
)
crls: str = f"&[{crl_includes}]"
expected: str = (
f"Err(webpki::Error::{expected_error})" if expected_error else "Ok(())"
)
feature_gate = '#[cfg(feature = "alloc")]' if owned else ""
print(
"""
%(feature_gate)s
#[test]
fn %(test_name)s() {
let ee = include_bytes!("%(ee_cert_path)s");
let intermediates = %(intermediates_str)s;
let ca = include_bytes!("%(root_cert_path)s");
let crls = %(crls)s;
assert_eq!(check_cert(ee, intermediates, ca, crls), %(expected)s);
}
"""
% locals(),
file=output,
)
# Build a simple certificate chain where the issuers don't have a key usage specified.
no_ku_chain = _chain(chain_name="no_ku_chain", key_usage=None)
# Build a simple certificate chain where the issuers have key usage specified, but don't include the
# cRLSign bit.
no_crl_ku_chain = _chain(chain_name="no_crl_ku_chain", key_usage=no_crl_sign_ku)
# Build a simple certificate chain where the issuers have key usage specified, and include the cRLSign bit.
crl_ku_chain = _chain(chain_name="ku_chain", key_usage=crl_sign_ku)
def _no_crls_test_ee_depth() -> None:
# Providing no CRLs means the EE cert should verify without err.
_revocation_test(
test_name="no_crls_test_ee_depth",
chain=no_ku_chain,
crl_paths=[],
owned=False,
expected_error=None,
)
_revocation_test(
test_name="no_crls_test_ee_depth_owned",
chain=no_ku_chain,
crl_paths=[],
owned=True,
expected_error=None,
)
def _no_relevant_crl_ee_depth() -> None:
test_name = "no_relevant_crl_ee_depth"
# Generate a CRL that includes the EE cert's serial, but that is issued by an unknown issuer.
ee_cert = no_ku_chain[0][0]
no_match_crl = _crl(
serials=[ee_cert.serial_number],
issuer_name=subject_name_for_test("whatev", test_name),
issuer_key=None,
)
no_match_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(no_match_crl_path, no_match_crl.public_bytes(Encoding.DER), force)
# Providing no relevant CRL means the EE cert should verify without err.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[no_match_crl_path],
owned=False,
expected_error=None,
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[no_match_crl_path],
owned=True,
expected_error=None,
)
def _ee_not_revoked_ee_depth() -> None:
test_name = "ee_not_revoked_ee_depth"
ee_cert = no_ku_chain[0][0]
int_a_key = no_ku_chain[1][2]
# Generate a CRL that doesn't include the EE cert's serial, but that is issued the same issuer.
ee_not_revoked_crl = _crl(
serials=[12345], # Some serial that isn't the ee_cert.serial.
issuer_name=ee_cert.issuer,
issuer_key=int_a_key,
)
ee_not_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
ee_not_revoked_crl_path,
ee_not_revoked_crl.public_bytes(Encoding.DER),
force,
)
# Providing a CRL that's relevant and verifies, but that doesn't include the EE cert serial should verify
# without err.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[ee_not_revoked_crl_path],
owned=False,
expected_error=None,
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[ee_not_revoked_crl_path],
owned=True,
expected_error=None,
)
def _ee_revoked_badsig_ee_depth() -> None:
test_name = "ee_revoked_badsig_ee_depth"
ee_cert = no_ku_chain[0][0]
# Generate a CRL that includes the EE cert's serial, and that is issued the same issuer, but that
# has an invalid signature.
rand_key: ec.EllipticCurvePrivateKey = ec.generate_private_key(
ec.SECP256R1(), default_backend()
)
ee_revoked_badsig = _crl(
serials=[ee_cert.serial_number],
issuer_name=ee_cert.issuer,
issuer_key=rand_key, # NB: Using a random key to sign, not int_a_key.
)
ee_revoked_badsig_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
ee_revoked_badsig_path, ee_revoked_badsig.public_bytes(Encoding.DER), force
)
# Providing a relevant CRL that includes the EE cert serial but does not verify should error.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[ee_revoked_badsig_path],
owned=False,
expected_error="InvalidCrlSignatureForPublicKey",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[ee_revoked_badsig_path],
owned=True,
expected_error="InvalidCrlSignatureForPublicKey",
)
def _ee_revoked_wrong_ku_ee_depth() -> None:
test_name = "ee_revoked_wrong_ku_ee_depth"
ee_cert = no_crl_ku_chain[0][0]
int_a_key = no_crl_ku_chain[1][2]
# Generate a CRL that includes the EE cert's serial, and that is issued by the same issuer (with a KU specified
# but no cRLSign bit)
ee_revoked_crl = _crl(
serials=[ee_cert.serial_number],
issuer_name=ee_cert.issuer,
issuer_key=int_a_key,
)
ee_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(ee_revoked_crl_path, ee_revoked_crl.public_bytes(Encoding.DER), force)
# Providing a relevant CRL that includes the EE cert serial but was issued by a CA that has a KU specified
# that doesn't include cRLSign should error indicating the CRL issuer can't sign CRLs.
_revocation_test(
test_name=test_name,
chain=no_crl_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=False,
expected_error="IssuerNotCrlSigner",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_crl_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=True,
expected_error="IssuerNotCrlSigner",
)
def _ee_not_revoked_wrong_ku_ee_depth() -> None:
test_name = "ee_not_revoked_wrong_ku_ee_depth"
ee_cert = no_crl_ku_chain[0][0]
int_a_key = no_crl_ku_chain[1][2]
# Generate a CRL that doesn't include the EE cert's serial, but that is issued the same issuer.
ee_not_revoked_crl = _crl(
serials=[12345], # Some serial that isn't the ee_cert.serial.
issuer_name=ee_cert.issuer,
issuer_key=int_a_key,
)
ee_not_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
ee_not_revoked_crl_path,
ee_not_revoked_crl.public_bytes(Encoding.DER),
force,
)
# Providing a relevant CRL that includes the EE cert serial but was issued by a CA that has a KU specified
# that doesn't include cRLSign should error indicating the CRL issuer can't sign CRLs.
_revocation_test(
test_name=test_name,
chain=no_crl_ku_chain,
crl_paths=[ee_not_revoked_crl_path],
owned=False,
expected_error="IssuerNotCrlSigner",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_crl_ku_chain,
crl_paths=[ee_not_revoked_crl_path],
owned=True,
expected_error="IssuerNotCrlSigner",
)
def _ee_revoked_no_ku_ee_depth() -> None:
test_name = "ee_revoked_no_ku_ee_depth"
ee_cert = no_ku_chain[0][0]
int_a_key = no_ku_chain[1][2]
# Generate a CRL that includes the EE cert's serial, and that is issued by the same issuer (without any KU
# specified).
ee_revoked_crl = _crl(
serials=[ee_cert.serial_number],
issuer_name=ee_cert.issuer,
issuer_key=int_a_key,
)
ee_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(ee_revoked_crl_path, ee_revoked_crl.public_bytes(Encoding.DER), force)
# Providing a relevant CRL that includes the EE cert serial and verifies should error indicating the cert
# was revoked.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=False,
expected_error="CertRevoked",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=True,
expected_error="CertRevoked",
)
def _ee_revoked_crl_ku_ee_depth() -> None:
test_name = "ee_revoked_crl_ku_ee_depth"
ee_cert = crl_ku_chain[0][0]
int_a_key = crl_ku_chain[1][2]
# Generate a CRL that includes the EE cert's serial, and that is issued by the same issuer (with a KU
# specified that includes cRLSign).
ee_revoked_crl = _crl(
serials=[ee_cert.serial_number],
issuer_name=ee_cert.issuer,
issuer_key=int_a_key,
)
ee_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(ee_revoked_crl_path, ee_revoked_crl.public_bytes(Encoding.DER), force)
# Providing a relevant CRL that includes the EE cert serial and verifies should error indicating the cert
# was revoked.
_revocation_test(
test_name=test_name,
chain=crl_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=False,
expected_error="CertRevoked",
)
_revocation_test(
test_name=test_name + "_owned",
chain=crl_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=True,
expected_error="CertRevoked",
)
def _no_crls_test_chain_depth() -> None:
# Providing no CRLs means the chain should verify without err.
_revocation_test(
test_name="no_crls_test_chain_depth",
chain=no_ku_chain,
crl_paths=[],
owned=False,
expected_error=None,
)
def _no_relevant_crl_chain_depth() -> None:
test_name = "no_relevant_crl_chain_depth"
# Generate a CRL that includes the first intermediate cert's serial, but that is issued by an unknown issuer.
int_a_cert = no_ku_chain[1][0]
no_match_crl = _crl(
serials=[int_a_cert.serial_number],
issuer_name=subject_name_for_test("whatev", test_name),
issuer_key=None,
)
no_match_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(no_match_crl_path, no_match_crl.public_bytes(Encoding.DER), force)
# Providing no relevant CRL means the chain should verify without err.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[no_match_crl_path],
owned=False,
expected_error=None,
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[no_match_crl_path],
owned=True,
expected_error=None,
)
def _int_not_revoked_chain_depth() -> None:
test_name = "int_not_revoked_chain_depth"
int_a_cert = no_ku_chain[1][0]
int_b_key = no_ku_chain[2][2]
# Generate a CRL that doesn't include the intermediate A cert's serial, but that is issued the same issuer.
int_not_revoked_crl = _crl(
serials=[12345], # Some serial that isn't the int_a_cert.serial.
issuer_name=int_a_cert.issuer,
issuer_key=int_b_key,
)
int_not_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
int_not_revoked_crl_path,
int_not_revoked_crl.public_bytes(Encoding.DER),
force,
)
# Providing a CRL that's relevant and verifies, but that doesn't include the intermediate cert serial should
# verify the chain without err.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[int_not_revoked_crl_path],
owned=False,
expected_error=None,
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[int_not_revoked_crl_path],
owned=True,
expected_error=None,
)
def _int_revoked_badsig_chain_depth() -> None:
test_name = "int_revoked_badsig_chain_depth"
int_a_cert = no_ku_chain[1][0]
# Generate a CRL that includes the intermediate cert's serial, and that is issued the same issuer, but that
# has an invalid signature.
rand_key: ec.EllipticCurvePrivateKey = ec.generate_private_key(
ec.SECP256R1(), default_backend()
)
int_revoked_badsig = _crl(
serials=[int_a_cert.serial_number],
issuer_name=int_a_cert.issuer,
issuer_key=rand_key, # NB: Using a random key to sign, not CA cert's key.
)
int_revoked_badsig_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
int_revoked_badsig_path,
int_revoked_badsig.public_bytes(Encoding.DER),
force,
)
# Providing a relevant CRL that includes the EE cert serial but does not verify should error.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[int_revoked_badsig_path],
owned=False,
expected_error="InvalidCrlSignatureForPublicKey",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[int_revoked_badsig_path],
owned=True,
expected_error="InvalidCrlSignatureForPublicKey",
)
def _int_revoked_wrong_ku_chain_depth() -> None:
test_name = "int_revoked_wrong_ku_chain_depth"
int_a_cert = no_crl_ku_chain[1][0]
int_b_key = no_crl_ku_chain[2][2]
# Generate a CRL that includes the intermediate A cert's serial, and that is issued by the same issuer (with a
# KU specified but no cRLSign bit)
int_revoked_crl = _crl(
serials=[int_a_cert.serial_number],
issuer_name=int_a_cert.issuer,
issuer_key=int_b_key,
)
int_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
int_revoked_crl_path, int_revoked_crl.public_bytes(Encoding.DER), force
)
# Providing a relevant CRL that includes the intermediate cert serial but was issued by a CA that has a KU
# specified that doesn't include cRLSign should error indicating the CRL issuer can't sign CRLs.
_revocation_test(
test_name=test_name,
chain=no_crl_ku_chain,
crl_paths=[int_revoked_crl_path],
owned=False,
expected_error="IssuerNotCrlSigner",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_crl_ku_chain,
crl_paths=[int_revoked_crl_path],
owned=True,
expected_error="IssuerNotCrlSigner",
)
def _ee_revoked_chain_depth() -> None:
test_name = "ee_revoked_chain_depth"
ee_cert = no_ku_chain[0][0]
int_a_key = no_ku_chain[1][2]
# Generate a CRL that includes the EE cert's serial, and that is issued by the same issuer (without any KU
# specified).
ee_revoked_crl = _crl(
serials=[ee_cert.serial_number],
issuer_name=ee_cert.issuer,
issuer_key=int_a_key,
)
ee_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(ee_revoked_crl_path, ee_revoked_crl.public_bytes(Encoding.DER), force)
# Providing a relevant CRL that includes the EE cert serial and verifies should error indicating the cert
# was revoked when using the Chain revocation check depth (since it implies EndEntity).
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=False,
expected_error="CertRevoked",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=True,
expected_error="CertRevoked",
)
def _int_revoked_no_ku_chain_depth() -> None:
test_name = "int_revoked_no_ku_chain_depth"
int_a_cert = no_ku_chain[1][0]
int_b_key = no_ku_chain[2][2]
# Generate a CRL that includes the intermediate cert's serial, and that is issued by the same issuer
# (without any KU specified).
int_revoked_crl = _crl(
serials=[int_a_cert.serial_number],
issuer_name=int_a_cert.issuer,
issuer_key=int_b_key,
)
int_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
int_revoked_crl_path, int_revoked_crl.public_bytes(Encoding.DER), force
)
# Providing a relevant CRL that includes the intermediate cert serial and verifies, and using the
# Chain depth should error since an intermediate cert is revoked.
_revocation_test(
test_name=test_name,
chain=no_ku_chain,
crl_paths=[int_revoked_crl_path],
owned=False,
expected_error="CertRevoked",
)
_revocation_test(
test_name=test_name + "_owned",
chain=no_ku_chain,
crl_paths=[int_revoked_crl_path],
owned=True,
expected_error="CertRevoked",
)
def _int_revoked_crl_ku_chain_depth() -> None:
test_name = "int_revoked_crl_ku_chain_depth"
int_a_cert = crl_ku_chain[1][0]
int_b_key = crl_ku_chain[2][2]
# Generate a CRL that includes the intermediate cert's serial, and that is issued by the same issuer (with a KU
# specified that includes cRLSign).
int_revoked_crl = _crl(
serials=[int_a_cert.serial_number],
issuer_name=int_a_cert.issuer,
issuer_key=int_b_key,
)
int_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(
int_revoked_crl_path, int_revoked_crl.public_bytes(Encoding.DER), force
)
# Providing a relevant CRL that includes the EE cert serial and verifies should error indicating the cert
# was revoked.
_revocation_test(
test_name=test_name,
chain=crl_ku_chain,
crl_paths=[int_revoked_crl_path],
owned=False,
expected_error="CertRevoked",
)
_revocation_test(
test_name=test_name + "_owned",
chain=crl_ku_chain,
crl_paths=[int_revoked_crl_path],
owned=True,
expected_error="CertRevoked",
)
def _ee_with_top_bit_set_serial_revoked() -> None:
test_name = "ee_with_top_bit_set_serial_revoked"
ee_cert_topbit = crl_ku_chain[4][0]
int_a_key = crl_ku_chain[1][2]
ee_revoked_crl = _crl(
serials=[ee_cert_topbit.serial_number],
issuer_name=ee_cert_topbit.issuer,
issuer_key=int_a_key,
)
ee_revoked_crl_path = os.path.join(output_dir, f"{test_name}.crl.der")
write_der(ee_revoked_crl_path, ee_revoked_crl.public_bytes(Encoding.DER), force)
_revocation_test(
test_name=test_name,
chain=crl_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=False,
ee_topbit_serial=True,
expected_error="CertRevoked",
)
_revocation_test(
test_name=test_name + "_owned",
chain=crl_ku_chain,
crl_paths=[ee_revoked_crl_path],
owned=True,
ee_topbit_serial=True,
expected_error="CertRevoked",
)
with trim_top("client_auth_revocation.rs") as output:
_no_crls_test_ee_depth()
_no_relevant_crl_ee_depth()
_ee_not_revoked_ee_depth()
_ee_revoked_badsig_ee_depth()
_ee_revoked_wrong_ku_ee_depth()
_ee_not_revoked_wrong_ku_ee_depth()
_ee_revoked_no_ku_ee_depth()
_ee_revoked_crl_ku_ee_depth()
_no_crls_test_chain_depth()
_no_relevant_crl_chain_depth()
_int_not_revoked_chain_depth()
_int_revoked_badsig_chain_depth()
_int_revoked_wrong_ku_chain_depth()
_ee_revoked_chain_depth()
_int_revoked_no_ku_chain_depth()
_int_revoked_crl_ku_chain_depth()
_ee_with_top_bit_set_serial_revoked()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--tls-server-certs",
action=argparse.BooleanOptionalAction,
default=True,
help="Generate TLS server certificate testcases",
)
parser.add_argument(
"--signatures",
action=argparse.BooleanOptionalAction,
default=True,
help="Generate signature testcases",
)
parser.add_argument(
"--client-auth",
action=argparse.BooleanOptionalAction,
default=True,
help="Generate client auth testcases",
)
parser.add_argument(
"--client-auth-revocation",
action=argparse.BooleanOptionalAction,
default=True,
help="Generate client auth revocation testcases",
)
parser.add_argument(
"--format",
action=argparse.BooleanOptionalAction,
default=True,
help="Run cargo fmt post-generation",
)
parser.add_argument(
"--test",
action=argparse.BooleanOptionalAction,
default=True,
help="Run cargo test post-generation",
)
parser.add_argument(
"--force",
action=argparse.BooleanOptionalAction,
default=False,
help="Overwrite existing test keys/certs",
)
args = parser.parse_args()
if args.tls_server_certs:
tls_server_certs(args.force)
if args.signatures:
signatures(args.force)
if args.client_auth:
client_auth(args.force)
if args.client_auth_revocation:
client_auth_revocation(args.force)
if args.format:
subprocess.run("cargo fmt", shell=True, check=True)
if args.test:
subprocess.run("cargo test", shell=True, check=True)