feat: break up validate_tls_keys and add unit tests
I've split the existing code in several new functions: - load_certificates_from_pem (takes pem data as bytes) - build_certificate_chain (takes a list of Certificates) - validate_certificate_chain (takes a list of Certificates) - validate_key (takes pem data as a string) - validate_key_matches_cert (now takes a pem key string and a Certificate) - extract_sans (now takes a Certificate) - validate_end_entity_expired (now takes a Certificate) - validate_end_entity_not_yet_valid (now takes a Certificate) When a relevant exception arises, these functions raise a type of TLSValidationError, these are appended to the list of errors when validating a cert.
This commit is contained in:
parent
5275a2a882
commit
d5fa521fa1
10 changed files with 1091 additions and 120 deletions
332
app/util/x509.py
332
app/util/x509.py
|
@ -6,19 +6,77 @@ from cryptography import x509
|
||||||
from cryptography.hazmat._oid import ExtensionOID
|
from cryptography.hazmat._oid import ExtensionOID
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
from cryptography.hazmat.primitives.asymmetric import padding, rsa, ec
|
||||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||||
|
EllipticCurvePublicKey,
|
||||||
|
EllipticCurvePrivateKey,
|
||||||
|
)
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
|
||||||
|
|
||||||
|
class TLSValidationError(ValueError):
|
||||||
|
key: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
def as_dict(self) -> Dict[str, str]:
|
||||||
|
return {"key": self.key, "message": self.message}
|
||||||
|
|
||||||
|
|
||||||
|
class TLSCertificateParsingError(TLSValidationError):
|
||||||
|
key = "could_not_parse_certificate"
|
||||||
|
message = "TLS certificate parsing error"
|
||||||
|
|
||||||
|
|
||||||
|
class TLSInvalidPrivateKeyError(TLSValidationError):
|
||||||
|
key = "invalid_private_key"
|
||||||
|
message = "Private key is invalid"
|
||||||
|
|
||||||
|
|
||||||
|
class TLSNoEndEntityError(TLSValidationError):
|
||||||
|
key = "could_not_identify_end_entity"
|
||||||
|
message = "Cannot identify the end-entity certificate."
|
||||||
|
|
||||||
|
|
||||||
|
class TLSChainNotValidError(TLSValidationError):
|
||||||
|
key = "invalid_tls_chain"
|
||||||
|
message = "Certificates do not form a valid chain."
|
||||||
|
|
||||||
|
|
||||||
|
class TLSUnsupportedAlgorithmError(TLSValidationError):
|
||||||
|
key = "invalid_algorithm"
|
||||||
|
message = "Certificate using unsupported algorithm"
|
||||||
|
|
||||||
|
|
||||||
|
class TLSMissingHashError(TLSValidationError):
|
||||||
|
key = "missing_hash_algorithm"
|
||||||
|
message = "Certificate missing hash algorithm."
|
||||||
|
|
||||||
|
|
||||||
|
class TLSUntrustedRootCAError(TLSValidationError):
|
||||||
|
key = "untrusted_root_ca"
|
||||||
|
message = "Certificate chain does not terminate at a trusted root CA."
|
||||||
|
|
||||||
|
|
||||||
def load_certificates_from_pem(pem_data: bytes) -> list[x509.Certificate]:
|
def load_certificates_from_pem(pem_data: bytes) -> list[x509.Certificate]:
|
||||||
certificates = []
|
certificates = []
|
||||||
|
try:
|
||||||
for pem_block in pem_data.split(b"-----END CERTIFICATE-----"):
|
for pem_block in pem_data.split(b"-----END CERTIFICATE-----"):
|
||||||
pem_block = pem_block.strip()
|
pem_block = pem_block.strip()
|
||||||
if pem_block:
|
if pem_block:
|
||||||
pem_block += b"-----END CERTIFICATE-----"
|
pem_block += b"-----END CERTIFICATE-----"
|
||||||
certificate = x509.load_pem_x509_certificate(pem_block, default_backend())
|
certificate = x509.load_pem_x509_certificate(
|
||||||
|
pem_block, default_backend()
|
||||||
|
)
|
||||||
certificates.append(certificate)
|
certificates.append(certificate)
|
||||||
|
except ValueError:
|
||||||
|
raise TLSCertificateParsingError()
|
||||||
return certificates
|
return certificates
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,17 +91,22 @@ def build_certificate_chain(
|
||||||
(
|
(
|
||||||
cert
|
cert
|
||||||
for cert in certificates
|
for cert in certificates
|
||||||
if cert.subject.rfc4514_string() not in cert_map
|
if not any(cert.subject == other_cert.issuer for other_cert in certificates)
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if not end_entity:
|
if not end_entity:
|
||||||
raise ValueError("Cannot identify the end-entity certificate.")
|
raise TLSNoEndEntityError()
|
||||||
chain.append(end_entity)
|
chain.append(end_entity)
|
||||||
current_cert = end_entity
|
current_cert = end_entity
|
||||||
while current_cert.issuer.rfc4514_string() in cert_map:
|
# when there is 1 item left in cert_map, that will be the Root CA
|
||||||
next_cert = cert_map[current_cert.issuer.rfc4514_string()]
|
while len(cert_map) > 1:
|
||||||
|
issuer_key = current_cert.issuer.rfc4514_string()
|
||||||
|
if issuer_key not in cert_map:
|
||||||
|
raise TLSChainNotValidError()
|
||||||
|
next_cert = cert_map[issuer_key]
|
||||||
chain.append(next_cert)
|
chain.append(next_cert)
|
||||||
|
cert_map.pop(current_cert.subject.rfc4514_string())
|
||||||
current_cert = next_cert
|
current_cert = next_cert
|
||||||
return chain
|
return chain
|
||||||
|
|
||||||
|
@ -56,28 +119,142 @@ def validate_certificate_chain(chain: list[x509.Certificate]) -> bool:
|
||||||
|
|
||||||
for i in range(len(chain) - 1):
|
for i in range(len(chain) - 1):
|
||||||
next_public_key = chain[i + 1].public_key()
|
next_public_key = chain[i + 1].public_key()
|
||||||
if not (isinstance(next_public_key, RSAPublicKey)):
|
if not isinstance(next_public_key, RSAPublicKey) and not isinstance(
|
||||||
raise ValueError(
|
next_public_key, EllipticCurvePublicKey
|
||||||
f"Certificate using unsupported algorithm: {type(next_public_key)}"
|
):
|
||||||
)
|
raise TLSUnsupportedAlgorithmError()
|
||||||
hash_algorithm = chain[i].signature_hash_algorithm
|
hash_algorithm = chain[i].signature_hash_algorithm
|
||||||
|
if TYPE_CHECKING:
|
||||||
if hash_algorithm is None:
|
if hash_algorithm is None:
|
||||||
raise ValueError("Certificate missing hash algorithm")
|
raise TLSMissingHashError()
|
||||||
|
if isinstance(next_public_key, RSAPublicKey):
|
||||||
next_public_key.verify(
|
next_public_key.verify(
|
||||||
chain[i].signature,
|
chain[i].signature,
|
||||||
chain[i].tbs_certificate_bytes,
|
chain[i].tbs_certificate_bytes,
|
||||||
PKCS1v15(),
|
PKCS1v15(),
|
||||||
hash_algorithm,
|
hash_algorithm,
|
||||||
)
|
)
|
||||||
|
elif isinstance(next_public_key, EllipticCurvePublicKey):
|
||||||
|
digest = hashes.Hash(hash_algorithm)
|
||||||
|
digest.update(chain[i].tbs_certificate_bytes)
|
||||||
|
digest_value = digest.finalize()
|
||||||
|
next_public_key.verify(
|
||||||
|
chain[i].signature,
|
||||||
|
digest_value,
|
||||||
|
ec.ECDSA(Prehashed(hash_algorithm)),
|
||||||
|
)
|
||||||
|
|
||||||
end_cert = chain[-1]
|
end_cert = chain[-1]
|
||||||
if not any(
|
if not any(
|
||||||
end_cert.issuer == trusted_cert.subject for trusted_cert in trusted_certificates
|
end_cert.issuer == trusted_cert.subject for trusted_cert in trusted_certificates
|
||||||
):
|
):
|
||||||
raise ValueError("Certificate chain does not terminate at a trusted root CA.")
|
raise TLSUntrustedRootCAError()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_key(tls_private_key_pem: Optional[str]) -> bool:
|
||||||
|
if tls_private_key_pem:
|
||||||
|
try:
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
tls_private_key_pem.encode("utf-8"),
|
||||||
|
password=None,
|
||||||
|
backend=default_backend(),
|
||||||
|
)
|
||||||
|
return isinstance(private_key, rsa.RSAPrivateKey) or isinstance(
|
||||||
|
private_key, ec.EllipticCurvePrivateKey
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
raise TLSInvalidPrivateKeyError()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def extract_sans(cert: x509.Certificate) -> List[str]:
|
||||||
|
try:
|
||||||
|
san_extension = cert.extensions.get_extension_for_oid(
|
||||||
|
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
||||||
|
)
|
||||||
|
sans: List[str] = san_extension.value.get_values_for_type(x509.DNSName) # type: ignore[attr-defined]
|
||||||
|
return sans
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def validate_end_entity_expired(certificate: x509.Certificate) -> bool:
|
||||||
|
if certificate.not_valid_after_utc < datetime.now(timezone.utc):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_end_entity_not_yet_valid(certificate: x509.Certificate) -> bool:
|
||||||
|
if certificate.not_valid_before_utc > datetime.now(timezone.utc):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_key_matches_cert(
|
||||||
|
tls_private_key_pem: Optional[str], certificate: x509.Certificate
|
||||||
|
) -> bool:
|
||||||
|
if not tls_private_key_pem or not certificate:
|
||||||
|
return False
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
tls_private_key_pem.encode("utf-8"),
|
||||||
|
password=None,
|
||||||
|
backend=default_backend(),
|
||||||
|
)
|
||||||
|
public_key = certificate.public_key()
|
||||||
|
signature_hash_algorithm = certificate.signature_hash_algorithm
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(public_key, rsa.RSAPublicKey) or isinstance(
|
||||||
|
public_key, EllipticCurvePublicKey
|
||||||
|
) # nosec: B101
|
||||||
|
assert isinstance(private_key, rsa.RSAPrivateKey) or isinstance(
|
||||||
|
public_key, EllipticCurvePrivateKey
|
||||||
|
) # nosec: B101
|
||||||
|
assert signature_hash_algorithm is not None # nosec: B101
|
||||||
|
if not (
|
||||||
|
(
|
||||||
|
isinstance(private_key, rsa.RSAPrivateKey)
|
||||||
|
and isinstance(public_key, rsa.RSAPublicKey)
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
isinstance(private_key, ec.EllipticCurvePrivateKey)
|
||||||
|
and isinstance(public_key, ec.EllipticCurvePublicKey)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
test_message = b"test"
|
||||||
|
if isinstance(public_key, RSAPublicKey) and isinstance(
|
||||||
|
private_key, rsa.RSAPrivateKey
|
||||||
|
):
|
||||||
|
signature = private_key.sign(
|
||||||
|
test_message,
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
signature_hash_algorithm,
|
||||||
|
)
|
||||||
|
public_key.verify(
|
||||||
|
signature,
|
||||||
|
test_message,
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
signature_hash_algorithm,
|
||||||
|
)
|
||||||
|
if isinstance(public_key, EllipticCurvePublicKey) and isinstance(
|
||||||
|
private_key, ec.EllipticCurvePrivateKey
|
||||||
|
):
|
||||||
|
signature = private_key.sign(
|
||||||
|
test_message,
|
||||||
|
ec.ECDSA(signature_hash_algorithm),
|
||||||
|
)
|
||||||
|
public_key.verify(
|
||||||
|
signature,
|
||||||
|
test_message,
|
||||||
|
ec.ECDSA(signature_hash_algorithm),
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validate_tls_keys(
|
def validate_tls_keys(
|
||||||
tls_private_key_pem: Optional[str],
|
tls_private_key_pem: Optional[str],
|
||||||
tls_certificate_pem: Optional[str],
|
tls_certificate_pem: Optional[str],
|
||||||
|
@ -91,114 +268,75 @@ def validate_tls_keys(
|
||||||
|
|
||||||
skip_chain_verification = skip_chain_verification or False
|
skip_chain_verification = skip_chain_verification or False
|
||||||
skip_name_verification = skip_name_verification or False
|
skip_name_verification = skip_name_verification or False
|
||||||
|
certificates = []
|
||||||
try:
|
try:
|
||||||
private_key = None
|
|
||||||
if tls_private_key_pem:
|
|
||||||
private_key = serialization.load_pem_private_key(
|
|
||||||
tls_private_key_pem.encode("utf-8"),
|
|
||||||
password=None,
|
|
||||||
backend=default_backend(),
|
|
||||||
)
|
|
||||||
if not isinstance(private_key, rsa.RSAPrivateKey):
|
|
||||||
errors.append(
|
|
||||||
{
|
|
||||||
"Error": "tls_private_key_invalid",
|
|
||||||
"Message": "Private key must be RSA.",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if tls_certificate_pem:
|
if tls_certificate_pem:
|
||||||
certificates = list(
|
certificates = load_certificates_from_pem(
|
||||||
load_certificates_from_pem(tls_certificate_pem.encode("utf-8"))
|
tls_certificate_pem.encode("utf-8")
|
||||||
)
|
)
|
||||||
if not certificates:
|
except TLSValidationError as e:
|
||||||
errors.append(
|
errors.append(e.as_dict())
|
||||||
{
|
if len(certificates) > 0:
|
||||||
"Error": "tls_certificate_invalid",
|
try:
|
||||||
"Message": "No valid certificate found.",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
chain = build_certificate_chain(certificates)
|
chain = build_certificate_chain(certificates)
|
||||||
end_entity_cert = chain[0]
|
end_entity_cert = chain[0]
|
||||||
|
# validate expiry
|
||||||
if end_entity_cert.not_valid_after_utc < datetime.now(timezone.utc):
|
if validate_end_entity_expired(end_entity_cert):
|
||||||
errors.append(
|
errors.append(
|
||||||
{
|
{
|
||||||
"Error": "tls_public_key_expired",
|
"key": "public_key_expired",
|
||||||
"Message": "TLS public key is expired.",
|
"message": "TLS public key is expired.",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# validate beginning
|
||||||
if end_entity_cert.not_valid_before_utc > datetime.now(timezone.utc):
|
if validate_end_entity_not_yet_valid(end_entity_cert):
|
||||||
errors.append(
|
errors.append(
|
||||||
{
|
{
|
||||||
"Error": "tls_public_key_future",
|
"key": "public_key_future",
|
||||||
"Message": "TLS public key is not yet valid.",
|
"message": "TLS public key is not yet valid.",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# chain verification
|
||||||
if private_key:
|
|
||||||
public_key = end_entity_cert.public_key()
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert isinstance(public_key, rsa.RSAPublicKey) # nosec: B101
|
|
||||||
assert isinstance(private_key, rsa.RSAPrivateKey) # nosec: B101
|
|
||||||
assert (
|
|
||||||
end_entity_cert.signature_hash_algorithm is not None
|
|
||||||
) # nosec: B101
|
|
||||||
try:
|
|
||||||
test_message = b"test"
|
|
||||||
signature = private_key.sign(
|
|
||||||
test_message,
|
|
||||||
padding.PKCS1v15(),
|
|
||||||
end_entity_cert.signature_hash_algorithm,
|
|
||||||
)
|
|
||||||
public_key.verify(
|
|
||||||
signature,
|
|
||||||
test_message,
|
|
||||||
padding.PKCS1v15(),
|
|
||||||
end_entity_cert.signature_hash_algorithm,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
errors.append(
|
|
||||||
{
|
|
||||||
"Error": "tls_key_mismatch",
|
|
||||||
"Message": "Private key does not match certificate.",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not skip_chain_verification:
|
if not skip_chain_verification:
|
||||||
try:
|
|
||||||
validate_certificate_chain(chain)
|
validate_certificate_chain(chain)
|
||||||
except ValueError as e:
|
# name verification
|
||||||
errors.append(
|
|
||||||
{"Error": "certificate_chain_invalid", "Message": str(e)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not skip_name_verification:
|
if not skip_name_verification:
|
||||||
san_list = extract_sans(end_entity_cert)
|
san_list = extract_sans(end_entity_cert)
|
||||||
for expected_hostname in [hostname, f"*.{hostname}"]:
|
for expected_hostname in [hostname, f"*.{hostname}"]:
|
||||||
if expected_hostname not in san_list:
|
if expected_hostname not in san_list:
|
||||||
errors.append(
|
errors.append(
|
||||||
{
|
{
|
||||||
"Error": "hostname_not_in_san",
|
"key": "hostname_not_in_san",
|
||||||
"Message": f"{expected_hostname} not found in SANs.",
|
"message": f"{expected_hostname} not found in SANs.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# check if key is valid
|
||||||
|
if validate_key(tls_private_key_pem):
|
||||||
|
# check if key matches cert
|
||||||
|
if not validate_key_matches_cert(tls_private_key_pem, end_entity_cert):
|
||||||
|
errors.append(
|
||||||
|
{
|
||||||
|
"key": "key_mismatch",
|
||||||
|
"message": "Private key does not match certificate.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
errors.append(
|
||||||
|
{
|
||||||
|
"key": "private_key_not_rsa_or_ec",
|
||||||
|
"message": "Private key must be RSA or Elliptic-Curve.",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except TLSValidationError as e:
|
||||||
errors.append({"Error": "tls_validation_error", "Message": str(e)})
|
errors.append(e.as_dict())
|
||||||
|
|
||||||
|
else:
|
||||||
|
errors.append(
|
||||||
|
{
|
||||||
|
"key": "no_valid_certificates",
|
||||||
|
"message": "No valid certificates supplied.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return chain, san_list, errors
|
return chain, san_list, errors
|
||||||
|
|
||||||
|
|
||||||
def extract_sans(cert: x509.Certificate) -> List[str]:
|
|
||||||
try:
|
|
||||||
san_extension = cert.extensions.get_extension_for_oid(
|
|
||||||
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
|
||||||
)
|
|
||||||
sans: List[str] = san_extension.value.get_values_for_type(x509.DNSName) # type: ignore[attr-defined]
|
|
||||||
return sans
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
15
tests/data/invalid-algorithm/dsa_private_key.pem
Normal file
15
tests/data/invalid-algorithm/dsa_private_key.pem
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIICXAIBADCCAjUGByqGSM44BAEwggIoAoIBAQCBw6/R4rq+pgDdS7neUlWAEggZ
|
||||||
|
zX388OjRI0cxk3HuI6gbfpPrWGKhTk3QUkepTTTIJB4OIneoBTwldOAMvoGpmkon
|
||||||
|
U4yyuGKsSEZD21OH9uPCAYRj7+D+qk7FzGL2ynXerxc8QvSEONrcnEDxhMiioBXx
|
||||||
|
CQM3HxTBhTu6MMYjtA09uwPtkULgC8MslLNjJYMFXl7xLK2rMmpI46shH9OYWGQJ
|
||||||
|
0MVMNzrBi/OvSy+1AJ6B0FtK94UR7uRvr0JBvdAaohes1T9DHkuO7UKf3mjXXaJv
|
||||||
|
2Ukd37zEjpphFekxyjBx7wpDpo/+p6xycm3YseokAu7uVfbhWZ0chrWbg2kBAh0A
|
||||||
|
7YfQiEV/y0zDdGeQf8lWR5EpzPmDlJk58B7YhwKCAQAzk3tubCoZVR3BM9P9yrId
|
||||||
|
toSOKS1bet8JS3SLsahfeHu4Q3aSoEZYP+/Oj3Qh0Zz5DaG85ME9RPlVZ5so3rCF
|
||||||
|
55CIceSeE5HwTRr0uMVYblHQcyjn9pcW/p8JNr2thSj8MHbhLaMBYJa35V2deNhR
|
||||||
|
Kn1Iv9lT4IBSvqxkMAkJmFQ8m7UQKec+mzWe5d1EYk0nlqpDdO7x787TJbT1y8QJ
|
||||||
|
ymXoLBlbXHch37bGOjEDwRSXNvAnZmPYdDECWkFejAGMbFIuO8TGdj5HIwDTHOE1
|
||||||
|
G9fIljlOlnc9PGHF7Qin1Ugu/CfmgumD4bJUelO6PV+Xwe+zkO3B2B2Sy38DTRDP
|
||||||
|
BB4CHB1aXGj21WgZAhGIqRztHvYPUtwoPhXH1YFwgbA=
|
||||||
|
-----END PRIVATE KEY-----
|
64
tests/data/invalid-algorithm/fullchain.pem
Normal file
64
tests/data/invalid-algorithm/fullchain.pem
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICPjCCAeugAwIBAgIUMtswhWE3qOep0Tr/lZyJbfzGyx0wCwYJYIZIAWUDBAMC
|
||||||
|
MBsxGTAXBgNVBAMMEEludGVybWVkaWF0ZSBEU0EwHhcNMjQxMjEyMTA1NDM4WhcN
|
||||||
|
MjUxMjEyMTA1NDM4WjAaMRgwFgYDVQQDDA9FbmQgQ2VydGlmaWNhdGUwggEiMA0G
|
||||||
|
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC20ip7ZPS9sR/lrg2thvH4aSMebaHF
|
||||||
|
w/6b4azLDhLWXIHlqU4cYw/MntKQCdfqPSVatVB/FmEzH92dSQQC3+yxalSdqiXD
|
||||||
|
nvTN7AfD2WUrFJYBfCVspo0LdrP4fATmaVq9BhjW/NXHKwchw/RmUIBhQW7dxXQM
|
||||||
|
l5y49bWnO2r1p5gN+uRN41mm7CVQZIe3jOP5TwN+eVpJKohGdycjwLI+piTZe0Mq
|
||||||
|
JWJvsminRVQagPxtqeq9JKz+pf/Bl3acAt/8gOCK4fm46CcU0Pe1B+Em2a8IeboE
|
||||||
|
gkZXAOAfHCd6gWR1VMVhmXKqwDhVHg2jMnk0/kNRm929G6qP2hdyk7XVAgMBAAGj
|
||||||
|
QjBAMB0GA1UdDgQWBBT8kjOpmCxOBxSdZ4dJzlhTA0UMJDAfBgNVHSMEGDAWgBRt
|
||||||
|
fRgaeqy5LNjf7r3lAu/ppFw9EjALBglghkgBZQMEAwIDQAAwPQIcON9Tuq2KrNbL
|
||||||
|
8InxxFHT3RJVL3J3ZgRnkSVmsgIdAIBAYzCTRCsGy5a28eaWBZlzhdpvlc12R+3Z
|
||||||
|
LXs=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFHjCCBAagAwIBAgIUJDqOTxBoNrRRcLl6Ymb5fVG52WQwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEjEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yNDEyMTIxMDU0MjJaFw0yNTEyMTIx
|
||||||
|
MDU0MjJaMBsxGTAXBgNVBAMMEEludGVybWVkaWF0ZSBEU0EwggNDMIICNQYHKoZI
|
||||||
|
zjgEATCCAigCggEBAL0A7VybU8lkzHwn8gL5smYSQURgULnHX+pNyGkoOAJgvR6/
|
||||||
|
ChA49GpL930IezhkSJ4zEoFmOdwbtk3k9Kld/6J2ZPwaZAF7/JSt13Y9Al35ZWD7
|
||||||
|
TEyWoviXRsdgYVK2jONmKtLPyLRK3kMq9aYhmBqlFN7vP3D1w0GJf1fVvWTwVxI1
|
||||||
|
X05rqv92DI22WcVOdOUoE6mbGJ7g4umL9TEXmcfvMEbf8xqOIwrlUUNLbt12lUN1
|
||||||
|
6kxu2/eaHE/1HWVrmOsSQvF2uIQEG3TXAN8fVOdLvuwYTuOpCcq4quu5Y0FwyKPH
|
||||||
|
wabxurWXTgY/jDT7bcHI+iqm3ri+fZL1qTKoiK8CHQClITgtGFBKNcd5bF+5CU6X
|
||||||
|
llBSIxgMEdpOHrFDAoIBACCUHjZKhPszIMfKvw5YtaxEn5ZcP9/hRHblLLBQWrYc
|
||||||
|
5kCa5t6RefJANdVjPhck1y5K1Jj8XhYZ2Hjs6f5WxwG4UL7gnLu4roXM70XxL6Ym
|
||||||
|
kbh0wy2LQENbU4nSezr4egI2w88gx9O3+tXiXt43PLB/lN614hU2gVGvyj/bLKrZ
|
||||||
|
RlWsx3JW2RkNKlj2gZ25Xu4fBwIhZB99rRubLxaGOz0qDNJ2Uw1AEXriVQt/g59l
|
||||||
|
hxbv7lARsRyrOgFSknrtBKJMFNNaDzBfVWGvQB+C7rkyuOYqiPDIydgvpdAjpX6+
|
||||||
|
LjxUMHV94m5stQEUzi409jaurfRr8vrTO8iRz4dWYjYDggEGAAKCAQEAmi2zHuGJ
|
||||||
|
ZfWhZr5IihkDORCrLvPRveyJ4sZg34DsuACGpxfDhggI8z0cVpboRSpBrxLLm7h+
|
||||||
|
tgW7/Z5HBBZrkkI5sBm5ZGqk5vn/v61NFJUhtLxmauvJq7+IaAp0rsDvezyf8lhS
|
||||||
|
4gjJNRdcWG7pFaOoFknhEFq2jYszqn5Bks8M/jO6GrmukYoWrczc3IJZh7/5L69c
|
||||||
|
ZxNAj03FMOUxOTfVQA253HhDfv3KNsniP3eCdBdzq6OAJyKtEdeBQ7ebj0eKBP6r
|
||||||
|
4WomtgtdxMxDX96xJ5rUS1n8VtRWZ8nxKMQazSlCp57jzdezFhO/vnXXhZjMFAnQ
|
||||||
|
CW2666KTx5ecwqNCMEAwHQYDVR0OBBYEFG19GBp6rLks2N/uveUC7+mkXD0SMB8G
|
||||||
|
A1UdIwQYMBaAFGElSAN6N667yAHK4qkJYYyzN+QLMA0GCSqGSIb3DQEBCwUAA4IB
|
||||||
|
AQCCqDtIXD5tfONmnCeT9pIqnf3lNBcrTJF+mJIq7lGSV9fJMdCIp1sGznsJv6OQ
|
||||||
|
cI/BilUsqF4uar6YqcQ3fIRrc76vArb007D4kj/JskanZ7xFq8EdGabINwZw3T7q
|
||||||
|
p6C7DoIME3bevic8FmDLsiVLwbqnMh2GpbOYd3R91EyfDD5Bw54+ymLcofxcajzt
|
||||||
|
Uj2yh0S7ThhL0+iXavNDme4G4MybBR8csLWy8QiQo+MwqGvF2n9RzjLkUN0M7ujE
|
||||||
|
WC6ZKYpw8jVpEKc/JQY7G+XsYnp2Gt/QUARMs7ElUm19XB4tF50uwEitGsBk6XxN
|
||||||
|
7dSfBqsFCFe2Kh+mys7WYFyJ
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDBTCCAe2gAwIBAgIUasHgLn4F+lxGqCoR1QstduimLXgwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEjEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yNDEyMTIxMDUyMTlaFw0zNDEyMTAx
|
||||||
|
MDUyMTlaMBIxEDAOBgNVBAMMB1Jvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||||
|
DwAwggEKAoIBAQCcIH9On+m7J0OI4R6MesITPVCtXk9a9NfGD5K568rxvzYo1UUE
|
||||||
|
AnmLlKgtiIU83MienWRnDxulyYX8wLzvR4Ykiv2tW24pK+FFrh3cPO8TChqgtG9Z
|
||||||
|
JC1V4bA8k+h1Rqm4C5mn4GbDdiH8zWm8GNMW1l7qqZI2G6zepxZhPboSnz0EapTd
|
||||||
|
mZkv7InW0V5Hw30vPpOYWjdIy6n5sdZ/ZwiwXfzma8RoBg5dUbVmaMt/rBZxu8iC
|
||||||
|
sdmmQNTLqnWKh3pG6Ys7eRADuYYLTzz8SXYQCM10pNP+rVXgbyAJESmWkHZwkev2
|
||||||
|
U6DPtOqFg+21YmN6sd/7WlAU5KDWj/iIhdfVAgMBAAGjUzBRMB0GA1UdDgQWBBRh
|
||||||
|
JUgDejeuu8gByuKpCWGMszfkCzAfBgNVHSMEGDAWgBRhJUgDejeuu8gByuKpCWGM
|
||||||
|
szfkCzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCZcfXUJsnH
|
||||||
|
4U+11dKR8/PgrTePP4CvYzpW7/nWAGCD6yqlxkkf/1fycBIRj4JGbOJdNB/E6Uux
|
||||||
|
huNzmBIl4Y7Rz48pCxJkdDTAMIuRJ9RQSZEN5HdgLCL4K/2RCTKjOTVorJsv8FOl
|
||||||
|
W5WoSIDujNVls2+aoMAANNP9rp19y+6X2ak3B4mqj/yAN16IAtL6nq01uVNs9S4u
|
||||||
|
x4xNgjVabH5Ycl16lsvtn8ohvBnhgp16YmHEm1kNSRyAtA3vZOh3+26DyqQ+VJb8
|
||||||
|
i3EH0xI53yuqXftetnX0cm4lRFi7SZIckojGLCPwpOjVMGkJFjIfU8ysHBVhRN0i
|
||||||
|
rROJJzlybSil
|
||||||
|
-----END CERTIFICATE-----
|
22
tests/data/letsencrypt-issued/cert.pem
Normal file
22
tests/data/letsencrypt-issued/cert.pem
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDqTCCAzCgAwIBAgISAyOnEK9hzGpwd7AWHdk5kO8tMAoGCCqGSM49BAMDMDIx
|
||||||
|
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||||
|
NTAeFw0yNDEyMTExNDE2MjRaFw0yNTAzMTExNDE2MjNaMDAxLjAsBgNVBAMTJXhh
|
||||||
|
aG02YWVjaDFtaWU1cXVleW84LmNlbnNvcnNoaXAuZ3VpZGUwWTATBgcqhkjOPQIB
|
||||||
|
BggqhkjOPQMBBwNCAAQ9qRr1MEI3IFrA1il9d10Mu3J+cP/vyk07nT7k4Qo25Ie3
|
||||||
|
1umSk5dUJBki4vaBVFQH9aa0N/xbdYyZFKiamfQco4ICJjCCAiIwDgYDVR0PAQH/
|
||||||
|
BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8E
|
||||||
|
AjAAMB0GA1UdDgQWBBT5aUwD70GI2Dw6DWh/BMVDADhgZjAfBgNVHSMEGDAWgBSf
|
||||||
|
K1/PPCFPnQS37SssxMZwi9LXDTBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGG
|
||||||
|
FWh0dHA6Ly9lNS5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL2U1Lmku
|
||||||
|
bGVuY3Iub3JnLzAwBgNVHREEKTAngiV4YWhtNmFlY2gxbWllNXF1ZXlvOC5jZW5z
|
||||||
|
b3JzaGlwLmd1aWRlMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBAwYKKwYBBAHWeQIE
|
||||||
|
AgSB9ASB8QDvAHUAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65Ay/ZDowuebgAAAGT
|
||||||
|
tkmQ5wAABAMARjBEAiAa/F5n5AzoazNkBxe4gdBf973aS0eR+68+jrlTyHW1ygIg
|
||||||
|
O6gt85PdB0sCLg4iNakV10Tvt10yuDrXflUfSFIrZC8AdgDPEVbu1S58r/OHW9lp
|
||||||
|
LpvpGnFnSrAX7KwB0lt3zsw7CAAAAZO2SZEZAAAEAwBHMEUCICRbjyc8lDW4g4Y6
|
||||||
|
C6dFKLu+C5nvuyQuRw18sx7x/2ZLAiEA7tBM3Ut1ITIsKpPi1J+0e6NMWI5xYH8m
|
||||||
|
PoV2bWr0UjEwCgYIKoZIzj0EAwMDZwAwZAIwA17A//MH+iYcl1erjHWmyb5RwfUv
|
||||||
|
zKoRHQHHZRIHv+SY7UDBNeEYmBgClXVUl1fpAjBhoZp0Riw4EtEfrsFDKjAuFUj8
|
||||||
|
B5/Cjw4Dvg5aqYGT/LmvFRubeALiKFwHNIuVQjs=
|
||||||
|
-----END CERTIFICATE-----
|
26
tests/data/letsencrypt-issued/chain.pem
Normal file
26
tests/data/letsencrypt-issued/chain.pem
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEVzCCAj+gAwIBAgIRAIOPbGPOsTmMYgZigxXJ/d4wDQYJKoZIhvcNAQELBQAw
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||||
|
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||||
|
RW5jcnlwdDELMAkGA1UEAxMCRTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNCzqK
|
||||||
|
a2GOtu/cX1jnxkJFVKtj9mZhSAouWXW0gQI3ULc/FnncmOyhKJdyIBwsz9V8UiBO
|
||||||
|
VHhbhBRrwJCuhezAUUE8Wod/Bk3U/mDR+mwt4X2VEIiiCFQPmRpM5uoKrNijgfgw
|
||||||
|
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||||
|
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSfK1/PPCFPnQS37SssxMZw
|
||||||
|
i9LXDTAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
||||||
|
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
||||||
|
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
||||||
|
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAH3KdNEVCQdqk0LKyuNImTKdRJY1C
|
||||||
|
2uw2SJajuhqkyGPY8C+zzsufZ+mgnhnq1A2KVQOSykOEnUbx1cy637rBAihx97r+
|
||||||
|
bcwbZM6sTDIaEriR/PLk6LKs9Be0uoVxgOKDcpG9svD33J+G9Lcfv1K9luDmSTgG
|
||||||
|
6XNFIN5vfI5gs/lMPyojEMdIzK9blcl2/1vKxO8WGCcjvsQ1nJ/Pwt8LQZBfOFyV
|
||||||
|
XP8ubAp/au3dc4EKWG9MO5zcx1qT9+NXRGdVWxGvmBFRAajciMfXME1ZuGmk3/GO
|
||||||
|
koAM7ZkjZmleyokP1LGzmfJcUd9s7eeu1/9/eg5XlXd/55GtYjAM+C4DG5i7eaNq
|
||||||
|
cm2F+yxYIPt6cbbtYVNJCGfHWqHEQ4FYStUyFnv8sjyqU8ypgZaNJ9aVcWSICLOI
|
||||||
|
E1/Qv/7oKsnZCWJ926wU6RqG1OYPGOi1zuABhLw61cuPVDT28nQS/e6z95cJXq0e
|
||||||
|
K1BcaJ6fJZsmbjRgD5p3mvEf5vdQM7MCEvU0tHbsx2I5mHHJoABHb8KVBgWp/lcX
|
||||||
|
GWiWaeOyB7RP+OfDtvi2OsapxXiV7vNVs7fMlrRjY1joKaqmmycnBvAq14AEbtyL
|
||||||
|
sVfOS66B8apkeFX2NY4XPEYV4ZSCe8VHPrdrERk2wILG3T/EGmSIkCYVUMSnjmJd
|
||||||
|
VQD9F6Na/+zmXCc=
|
||||||
|
-----END CERTIFICATE-----
|
48
tests/data/letsencrypt-issued/fullchain.pem
Normal file
48
tests/data/letsencrypt-issued/fullchain.pem
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDqTCCAzCgAwIBAgISAyOnEK9hzGpwd7AWHdk5kO8tMAoGCCqGSM49BAMDMDIx
|
||||||
|
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||||
|
NTAeFw0yNDEyMTExNDE2MjRaFw0yNTAzMTExNDE2MjNaMDAxLjAsBgNVBAMTJXhh
|
||||||
|
aG02YWVjaDFtaWU1cXVleW84LmNlbnNvcnNoaXAuZ3VpZGUwWTATBgcqhkjOPQIB
|
||||||
|
BggqhkjOPQMBBwNCAAQ9qRr1MEI3IFrA1il9d10Mu3J+cP/vyk07nT7k4Qo25Ie3
|
||||||
|
1umSk5dUJBki4vaBVFQH9aa0N/xbdYyZFKiamfQco4ICJjCCAiIwDgYDVR0PAQH/
|
||||||
|
BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8E
|
||||||
|
AjAAMB0GA1UdDgQWBBT5aUwD70GI2Dw6DWh/BMVDADhgZjAfBgNVHSMEGDAWgBSf
|
||||||
|
K1/PPCFPnQS37SssxMZwi9LXDTBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGG
|
||||||
|
FWh0dHA6Ly9lNS5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL2U1Lmku
|
||||||
|
bGVuY3Iub3JnLzAwBgNVHREEKTAngiV4YWhtNmFlY2gxbWllNXF1ZXlvOC5jZW5z
|
||||||
|
b3JzaGlwLmd1aWRlMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBAwYKKwYBBAHWeQIE
|
||||||
|
AgSB9ASB8QDvAHUAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65Ay/ZDowuebgAAAGT
|
||||||
|
tkmQ5wAABAMARjBEAiAa/F5n5AzoazNkBxe4gdBf973aS0eR+68+jrlTyHW1ygIg
|
||||||
|
O6gt85PdB0sCLg4iNakV10Tvt10yuDrXflUfSFIrZC8AdgDPEVbu1S58r/OHW9lp
|
||||||
|
LpvpGnFnSrAX7KwB0lt3zsw7CAAAAZO2SZEZAAAEAwBHMEUCICRbjyc8lDW4g4Y6
|
||||||
|
C6dFKLu+C5nvuyQuRw18sx7x/2ZLAiEA7tBM3Ut1ITIsKpPi1J+0e6NMWI5xYH8m
|
||||||
|
PoV2bWr0UjEwCgYIKoZIzj0EAwMDZwAwZAIwA17A//MH+iYcl1erjHWmyb5RwfUv
|
||||||
|
zKoRHQHHZRIHv+SY7UDBNeEYmBgClXVUl1fpAjBhoZp0Riw4EtEfrsFDKjAuFUj8
|
||||||
|
B5/Cjw4Dvg5aqYGT/LmvFRubeALiKFwHNIuVQjs=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEVzCCAj+gAwIBAgIRAIOPbGPOsTmMYgZigxXJ/d4wDQYJKoZIhvcNAQELBQAw
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||||
|
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||||
|
RW5jcnlwdDELMAkGA1UEAxMCRTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNCzqK
|
||||||
|
a2GOtu/cX1jnxkJFVKtj9mZhSAouWXW0gQI3ULc/FnncmOyhKJdyIBwsz9V8UiBO
|
||||||
|
VHhbhBRrwJCuhezAUUE8Wod/Bk3U/mDR+mwt4X2VEIiiCFQPmRpM5uoKrNijgfgw
|
||||||
|
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||||
|
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSfK1/PPCFPnQS37SssxMZw
|
||||||
|
i9LXDTAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
||||||
|
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
||||||
|
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
||||||
|
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAH3KdNEVCQdqk0LKyuNImTKdRJY1C
|
||||||
|
2uw2SJajuhqkyGPY8C+zzsufZ+mgnhnq1A2KVQOSykOEnUbx1cy637rBAihx97r+
|
||||||
|
bcwbZM6sTDIaEriR/PLk6LKs9Be0uoVxgOKDcpG9svD33J+G9Lcfv1K9luDmSTgG
|
||||||
|
6XNFIN5vfI5gs/lMPyojEMdIzK9blcl2/1vKxO8WGCcjvsQ1nJ/Pwt8LQZBfOFyV
|
||||||
|
XP8ubAp/au3dc4EKWG9MO5zcx1qT9+NXRGdVWxGvmBFRAajciMfXME1ZuGmk3/GO
|
||||||
|
koAM7ZkjZmleyokP1LGzmfJcUd9s7eeu1/9/eg5XlXd/55GtYjAM+C4DG5i7eaNq
|
||||||
|
cm2F+yxYIPt6cbbtYVNJCGfHWqHEQ4FYStUyFnv8sjyqU8ypgZaNJ9aVcWSICLOI
|
||||||
|
E1/Qv/7oKsnZCWJ926wU6RqG1OYPGOi1zuABhLw61cuPVDT28nQS/e6z95cJXq0e
|
||||||
|
K1BcaJ6fJZsmbjRgD5p3mvEf5vdQM7MCEvU0tHbsx2I5mHHJoABHb8KVBgWp/lcX
|
||||||
|
GWiWaeOyB7RP+OfDtvi2OsapxXiV7vNVs7fMlrRjY1joKaqmmycnBvAq14AEbtyL
|
||||||
|
sVfOS66B8apkeFX2NY4XPEYV4ZSCe8VHPrdrERk2wILG3T/EGmSIkCYVUMSnjmJd
|
||||||
|
VQD9F6Na/+zmXCc=
|
||||||
|
-----END CERTIFICATE-----
|
5
tests/data/letsencrypt-issued/privkey.pem
Normal file
5
tests/data/letsencrypt-issued/privkey.pem
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1IXBCW4hoVNlI+nb
|
||||||
|
Vmr0GL1Z7n607+GVTz9PlhkrhS2hRANCAAQ9qRr1MEI3IFrA1il9d10Mu3J+cP/v
|
||||||
|
yk07nT7k4Qo25Ie31umSk5dUJBki4vaBVFQH9aa0N/xbdYyZFKiamfQc
|
||||||
|
-----END PRIVATE KEY-----
|
54
tests/data/no-end-entity-chain/fullchain.pem
Normal file
54
tests/data/no-end-entity-chain/fullchain.pem
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC8DCCAdigAwIBAgIUJUTd4WRtEtcpAxNbg16AAyDdYuQwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEDEOMAwGA1UEAwwFQ2VydEMwHhcNMjQxMjEyMTUyNzQwWhcNMzQxMjEwMTUy
|
||||||
|
NzQwWjAQMQ4wDAYDVQQDDAVDZXJ0QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBAOtbosZu34pTFdlQZDJ9TE6qVtInel5l8AOkiHqzsgp82xPvqqgVDfo4
|
||||||
|
6azPHgrCDChylpNDSVrsJnZ+AFs0rLhJhy6184nnYAIGF6y5RrDAOyIu4QqrDlaF
|
||||||
|
dENdmBwyNZsM11TMj5Ims56w2m7SMiIU+yIxBlKEFlsy6Xd8DODuIjmhBxcw4k1f
|
||||||
|
HCAZQ98CcERaq22TTmGaKCL2o1navm4ku0xd3ZYbEd8xSkcXE/oymcevcfvAWAOD
|
||||||
|
Hf7wfYqgpnXn8NW47QUBiVCer7bvYcgh6Fll4MuTFnOY50gDblRcdn+My5m28Bct
|
||||||
|
Ilq8rmbx4cWcz/4FpcuttmRoM7KJgvkCAwEAAaNCMEAwHQYDVR0OBBYEFLUcB3bI
|
||||||
|
hsjEj3iGFZ7JCf8zVuezMB8GA1UdIwQYMBaAFHazPU3yY+LBXuhI31moiPRY9W+V
|
||||||
|
MA0GCSqGSIb3DQEBCwUAA4IBAQBBMBuxEOOuRDxxdMpGE8cvOXQpQt5xzPZO/+K5
|
||||||
|
ACXrH5cQBoHUTQL3MtnjhFroKhlFY4VuU5VgyAsdKcnA/MWM7KZTt/pP9VQCTFeC
|
||||||
|
sicWS/s8/A+y/8wKloQjP0AIjwAvxAfOjjsPEV5ztb3nt36kGFMd/3Cuz6GYnF9R
|
||||||
|
agloB+997/XuRKWHfDmAEdz4tPoslFEQEc9gYI2PRpuePu4I5FpM7/8B2WowyHD+
|
||||||
|
+yUPQNZCoW6ZJdp5WM3BGcw5B5T4K3h6TvEw8+BzbYachYp5BHu0odrow7tlP6c1
|
||||||
|
YQCaRCUPWiINpidfjKnXMfWAiqp6PzMF4AGyy4/C0FEI/dYB
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC8DCCAdigAwIBAgIUB6S7PZ3pqNT2EHzNrr2bSnxDR4cwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEDEOMAwGA1UEAwwFQ2VydEEwHhcNMjQxMjEyMTUyNzIxWhcNMzQxMjEwMTUy
|
||||||
|
NzIxWjAQMQ4wDAYDVQQDDAVDZXJ0QjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBAKq1xmDfGy1t6noK4hCJw+n+wEuLh/02NTNjLFa3WZRd7Zuv0ouV0ilG
|
||||||
|
VxMNIw31P/tlhvna6PGwQT3DwqD8TLdBiTW6qqZfw2iZhSqk4PGD05sLktHcDw5r
|
||||||
|
DCQve3fGzMGBK55bHrlFKo9DaERxzenSVqe6YgtCnnYOdpUtbGoxjBVtD9cHbE5F
|
||||||
|
t/S+gvCL+v9uDv3FzF/BjJ+A+sy7GNqCNqu43RMQK3DPLupZ+6Jz1IbTqfht5PYV
|
||||||
|
P2DMrlrAJVikIAlalhpYogFnjwyMOIg6/ZtT9xgNHOaDklH8EjgqXhM7/DDRgYzc
|
||||||
|
X+WjfHN5so/fvO+FdepEtb1Fa9/yh7kCAwEAAaNCMEAwHQYDVR0OBBYEFESpI/r4
|
||||||
|
5uM04EpbCMXduTnZBVyqMB8GA1UdIwQYMBaAFD56a0oPcVXFV5aMEzZzm8ycqbjy
|
||||||
|
MA0GCSqGSIb3DQEBCwUAA4IBAQBm0AqU1uNVObyVnXN6O0MSQFpTcnXZ5rEx0N9v
|
||||||
|
WZ+oHTyKIlnTtgXdtkC7ZN8P6AJUacWifttiPMA1MU9jCkNtFiVQICjeDI18XQn+
|
||||||
|
X+tv/50FdlTcRrRU34lxdGRW/UxlCs6OWG0UAimokTETr2a9L1oIee+LoPl99iDp
|
||||||
|
FwJCuNBOA9bLbayl8u7EgtGVfZaqk7AV4apAsits8jdjrT9UFj1SFsNHMHSk9ZRw
|
||||||
|
HcYzbQM3+yvJWtNoHP9fX2a7JlZfqdHdhO4nmYbWYXuv+5Z7zZkgcgqehsptcoUe
|
||||||
|
/uud4LZjAYFkRIiYZ9jEdBSWUr3wQjS/Ed0d56NeXEQVbjDO
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC8DCCAdigAwIBAgIUObylipFiamGeDkUl3B2x9Qoe0X8wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEDEOMAwGA1UEAwwFQ2VydEIwHhcNMjQxMjEyMTUyNzI3WhcNMzQxMjEwMTUy
|
||||||
|
NzI3WjAQMQ4wDAYDVQQDDAVDZXJ0QzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBAJmRdT01g1lJjQXkwybJWbYfLRCHxW2zkNkejD5uHGv/hhrhOIuU3Ihy
|
||||||
|
u39t1vDMHQkxOHREXZ9zki4ArXNKe2+NjVEMcZS3d03wEM+Mx8iusgn75xEKZ/C4
|
||||||
|
wU8MHQwC5tOm4h76H2l5NV5IbLENYZNV6YmgsE24te5qQltXpVAVkUxqgejFsFWh
|
||||||
|
q8UxCeDtSyAq4pTFre88G29XFyRqhLZk+x4YuX/IqgFQwm2w+WqgY1yYyLIDYcuG
|
||||||
|
n01izKYAUj+1+iWT9TQWUo6a3oz5rtuUm7BQM1AsLk3XqOX8no9znHwBTiidpUhd
|
||||||
|
1oyjkTLg+uCJIcNB165PNbPkP+ZUda0CAwEAAaNCMEAwHQYDVR0OBBYEFHazPU3y
|
||||||
|
Y+LBXuhI31moiPRY9W+VMB8GA1UdIwQYMBaAFESpI/r45uM04EpbCMXduTnZBVyq
|
||||||
|
MA0GCSqGSIb3DQEBCwUAA4IBAQBh5F1uw/VQ43hF5n+gMk4kIBmo80NYe36NeW8m
|
||||||
|
KcKFlrDhdAKVs9wd0Wql1yQJaOiY2KLoJCk9Yu4WDP8c+C0BMYsn0XcBzHQYqIvW
|
||||||
|
qkU0+YHl29xApHGc5uGmwUIYORaxth/Ts8nK55JIiXoNXMvYiFGDJu65yFLAf1iM
|
||||||
|
cK/sJRV5QmdOTZLIrcfo6IB7vfbpL2S1GW2kHCOPYqTyVcrmzos3akMkdHeSkcoW
|
||||||
|
ayrzuUuW4livtAApNOm6Sp+F12wWyve9iy/Lcm5zmITGJSJ7kHufF5GdrQSUkzDi
|
||||||
|
a/0n8+rWY3K1jiSju1p3dob6Hn31F6JVXnYX7IwA0BUQ5Sis
|
||||||
|
-----END CERTIFICATE-----
|
57
tests/data/untrusted-root-ca/fullchain.pem
Normal file
57
tests/data/untrusted-root-ca/fullchain.pem
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDFDCCAfygAwIBAgIUKffY79zMWn0EhgRSqWiXbnR1IvMwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwIzEhMB8GA1UEAwwYSW50ZXJtZWRpYXRlIENlcnRpZmljYXRlMB4XDTI0MTIx
|
||||||
|
MjEzNDUwN1oXDTI1MTIxMjEzNDUwN1owITEfMB0GA1UEAwwWRW5kIEVudGl0eSBD
|
||||||
|
ZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIzUM7O
|
||||||
|
LnPT8qZ/Rc3w2cb6sQsatHoGkdxRq4yI1gSXT14Tuv8c93WXhCGnASGELFWW511u
|
||||||
|
B+XCn3BeJ5dMhsGAGT7/NDVZpxKXmev9T70QIW0FToEbqC2Yp9+brZckYJIl9JyX
|
||||||
|
AsD2LpKOCZ8pVSMVmRbAU+U29s3ZaGwfGJbfgmXLS/tXYB7OPg8mSF4Vvyfsj33s
|
||||||
|
0wKQQyGI2PtmKljGqtDCmhR6gEs+oKTSaXJF9Q7ggNyXuUwkosM/rUkouKzEb6rj
|
||||||
|
6yyiy4RRPeXT3UTPLJQcPwxJi48W3w08ivX2E9NbruHotl8kRQ/+qqPd04V4l1uk
|
||||||
|
AjOOmNLWwblOLVUCAwEAAaNCMEAwHQYDVR0OBBYEFGLjBzPXCO20+kCKJxJkUBzz
|
||||||
|
l9REMB8GA1UdIwQYMBaAFE6WzIs4ON0xLZS7m2x1whhFf0YhMA0GCSqGSIb3DQEB
|
||||||
|
CwUAA4IBAQC28JJK+nIR0M7FnHEu9iqCS8MQd9CzAKqyk+f9cTtvoTcIq3ACZyXB
|
||||||
|
LvxP7KFgfyTJUiPiR52dhnbDZt+GaaFghUE7RuSNkRhyAAlc49I22i8zborR0MFj
|
||||||
|
9/L7VjrogALhV0vQ8PyYiYBYz1M2QaVTyVfrmLqqOkbd9xX1PAu25vhTBWMOVAP+
|
||||||
|
1+1XDBIkRPCGIxPofJapLGtVeajX//mkKzvpTrubY82FYwZFnLxJ+37pb4D5Hrla
|
||||||
|
876ghFgzK3cSqdfRJNYgkTaikrUnvtWJsiDgRRq5A4CgXPgrSHBVNOg1O8wzHJKJ
|
||||||
|
09V4kiRQtbPBN/rezPpqbMy5Po0uhbGT
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDDjCCAfagAwIBAgIUdnEBQPjg1OUGZ2Zt2DMTkk8EI2EwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwGzEZMBcGA1UEAwwQUm9vdCBDZXJ0aWZpY2F0ZTAeFw0yNDEyMTIxMzQ0Mzha
|
||||||
|
Fw0yOTEyMTExMzQ0MzhaMCMxITAfBgNVBAMMGEludGVybWVkaWF0ZSBDZXJ0aWZp
|
||||||
|
Y2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALppGVRHNi2olRop
|
||||||
|
KIIuRcKHuy3GXLO0ds9LDaTIKEksQyHwx9WwoZ0gIIdtQAg6NTxfJlGtJ08yE5+R
|
||||||
|
79nO6sQhRBakEveVhr3COc0MBTdo9SQrYLeMMUH6qd5tTh/EUXYpguq24ZCMbpuL
|
||||||
|
+DCRuKH77/Js1a/8A4Cx7welUi+JgEQoH0/F5o1pBC4NzLygI71tcxSdZsbqBTtu
|
||||||
|
eQdXTBGUezhnU1wxzZGgz36vV7rPQEJTK/TAABfPsFQDo+LzEki4gY0VOJ7P3RGk
|
||||||
|
roWZc6LepqN/1+C2FSfiwAZQYFdB5Y+vGe0z7Q8C7U1zrmEdxHv+Q4N1KbnKKnCn
|
||||||
|
+WSlgzECAwEAAaNCMEAwHQYDVR0OBBYEFE6WzIs4ON0xLZS7m2x1whhFf0YhMB8G
|
||||||
|
A1UdIwQYMBaAFHbSd9I5sg9dIYYomK8vAZOwMtC1MA0GCSqGSIb3DQEBCwUAA4IB
|
||||||
|
AQAfiRihMxaAphKlRIozma0/5KK9eXXp8pf49IWqeQQWO6yoBGq4N6dIhZ5cocqy
|
||||||
|
AkdrMFFRCIttt8oDuXZb93047i5W6t2vDloo+OomF0DN5RCR938OD6pyzvnkWXTP
|
||||||
|
YqhT4JH9rCaQvhUmykaIjWzAmHe/HLhGsqY+6fMmCdTbjn4SJmfo2qNNfoVA22Q1
|
||||||
|
QdboRHanu2pcg/NBDePjwWdIXvjCAND5q14pCrPn755PVoOoS7uezqcINnlP7gZl
|
||||||
|
PMDVIioZ857QTbG0iQMqZEZbwAtkUUX/HQY88k/rkGm4y7qs0fM33rxlTtIucZkY
|
||||||
|
uc3Sfz4zOk8YUzQOTwF6T2es
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDFzCCAf+gAwIBAgIUOseMixTFbS0J7ZrqkZnXUziOqhkwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwGzEZMBcGA1UEAwwQUm9vdCBDZXJ0aWZpY2F0ZTAeFw0yNDEyMTIxMzQ0MTha
|
||||||
|
Fw0zNDEyMTAxMzQ0MThaMBsxGTAXBgNVBAMMEFJvb3QgQ2VydGlmaWNhdGUwggEi
|
||||||
|
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv37pAd4vWkaKqpKZLi+8nZI6k
|
||||||
|
l5qPbnVUKQ5aBIdDooF5wVwoSd0ObuvJ4WZtJFxDc3U8wDJ04dmeha6k3OH+7BHn
|
||||||
|
/G7HzRb3ijbuCZT14CQZ9BrC3oSPXwKq+Ud4BvY3HAPm+2NoHjHwnddfqX/3kR6E
|
||||||
|
xAmwjKu4H9kKQwRAeetdV6AX+NPhlrIpgD0ASVzPzEDln00PVMau14Gr/L9m7m6X
|
||||||
|
uhpJXIdUn7/VLrakG2/A3fAaCtzzfDBRqES78Kq36F9lP/0EcLmkKbvph4Yu84lY
|
||||||
|
RJNFweuBfuh7vElyQDLjfwNEqX7P/GTe1VuSDIjHAxxAUgi871AE+s4R9mwJAgMB
|
||||||
|
AAGjUzBRMB0GA1UdDgQWBBR20nfSObIPXSGGKJivLwGTsDLQtTAfBgNVHSMEGDAW
|
||||||
|
gBR20nfSObIPXSGGKJivLwGTsDLQtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
||||||
|
DQEBCwUAA4IBAQBnZdVKZdJ3W/7sCWDoA0qjSlbq4Ry0Gz2OPcNdmlykUBBDO+c9
|
||||||
|
rK1QZkyoIBMTOAvs0M+SPvPD2EDQSTzSHMUYEEUZlEahEMrhNNhQWp47K0OENE+p
|
||||||
|
0J+saj7HvpZPmd9pAphvUa1Bvh8BxFkT5qZrEPMSQntfUd8xWSqavmOD3vcmw4YC
|
||||||
|
Njb7y4Jly5P06oi0OYkWwff7Jw7QWXF+/SvWWVq9kb/sXPsCKoOBo5/ii3UnQbWP
|
||||||
|
y8+M+ndcrgLQvbnGXt98m9/0PzmxIcHdRyg7bkNO14tE4kmFE1LjDSrcZjfV1vix
|
||||||
|
wN+oDCby9xuuBiqPPztII8ixq+6bCu0i5BcB
|
||||||
|
-----END CERTIFICATE-----
|
542
tests/utils/test_x509.py
Normal file
542
tests/utils/test_x509.py
Normal file
|
@ -0,0 +1,542 @@
|
||||||
|
import pytest
|
||||||
|
from cryptography.x509 import Certificate
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from app.util import x509
|
||||||
|
from app.util.x509 import TLSValidationError
|
||||||
|
from tests.api.test_onion import generate_self_signed_tls_certificate
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import shutil
|
||||||
|
from itertools import permutations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def self_signed_rsa_cert(tmp_path):
|
||||||
|
directory = (
|
||||||
|
tmp_path / "cert"
|
||||||
|
) # tmp_path is a pytest fixture implemented as a pathlib.Path object
|
||||||
|
if directory.exists():
|
||||||
|
shutil.rmtree(directory)
|
||||||
|
directory.mkdir()
|
||||||
|
onion_address = "test.onion"
|
||||||
|
valid_from = datetime.now()
|
||||||
|
valid_to = valid_from + timedelta(days=30)
|
||||||
|
dns_names = ["test.onion", "www.test.onion"]
|
||||||
|
|
||||||
|
# Generate certificate
|
||||||
|
private_key_path, cert_path = generate_self_signed_tls_certificate(
|
||||||
|
str(directory), onion_address, valid_from, valid_to, dns_names=dns_names
|
||||||
|
)
|
||||||
|
return private_key_path, cert_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def expired_cert(tmp_path):
|
||||||
|
# Fixture for generating a certificate that is expired
|
||||||
|
directory = tmp_path / "cert_expired"
|
||||||
|
if directory.exists():
|
||||||
|
shutil.rmtree(directory)
|
||||||
|
directory.mkdir()
|
||||||
|
onion_address = "test.onion"
|
||||||
|
valid_from = datetime.now() - timedelta(days=60)
|
||||||
|
valid_to = datetime.now() - timedelta(days=30)
|
||||||
|
dns_names = ["test.onion", "www.test.onion"]
|
||||||
|
|
||||||
|
private_key_path, cert_path = generate_self_signed_tls_certificate(
|
||||||
|
str(directory), onion_address, valid_from, valid_to, dns_names=dns_names
|
||||||
|
)
|
||||||
|
return private_key_path, cert_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def not_yet_valid_cert(tmp_path):
|
||||||
|
# Fixture for generating a certificate that is not yet valid
|
||||||
|
directory = tmp_path / "cert_not_yet_valid"
|
||||||
|
if directory.exists():
|
||||||
|
shutil.rmtree(directory)
|
||||||
|
directory.mkdir()
|
||||||
|
onion_address = "test.onion"
|
||||||
|
valid_from = datetime.now() + timedelta(days=30)
|
||||||
|
valid_to = valid_from + timedelta(days=30)
|
||||||
|
dns_names = ["test.onion", "www.test.onion"]
|
||||||
|
private_key_path, cert_path = generate_self_signed_tls_certificate(
|
||||||
|
str(directory), onion_address, valid_from, valid_to, dns_names=dns_names
|
||||||
|
)
|
||||||
|
return private_key_path, cert_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def letsencrypt_cert():
|
||||||
|
cert_path = "tests/data/letsencrypt-issued/fullchain.pem"
|
||||||
|
private_key_path = "tests/data/letsencrypt-issued/privkey.pem"
|
||||||
|
return private_key_path, cert_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def invalid_algorithm_key():
|
||||||
|
key_path = "tests/data/invalid-algorithm/dsa_private_key.pem"
|
||||||
|
return key_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def letsencrypt_valid_chain():
|
||||||
|
cert_path = "tests/data/letsencrypt-issued/fullchain.pem"
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
certificates = x509.load_certificates_from_pem(pem_cert)
|
||||||
|
return certificates
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def invalid_algorithm_chain():
|
||||||
|
cert_path = "tests/data/invalid-algorithm/fullchain.pem"
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
certificates = x509.load_certificates_from_pem(pem_cert)
|
||||||
|
return certificates
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def untrusted_root_ca_chain():
|
||||||
|
cert_path = "tests/data/untrusted-root-ca/fullchain.pem"
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
certificates = x509.load_certificates_from_pem(pem_cert)
|
||||||
|
return certificates
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def circular_chain():
|
||||||
|
cert_path = "tests/data/no-end-entity-chain/fullchain.pem"
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
certificates = x509.load_certificates_from_pem(pem_cert)
|
||||||
|
return certificates
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_single_certificate(self_signed_rsa_cert):
|
||||||
|
"""Test loading a single certificate from PEM data."""
|
||||||
|
_, cert_path = self_signed_rsa_cert
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
|
||||||
|
certificates = x509.load_certificates_from_pem(pem_cert)
|
||||||
|
assert len(certificates) == 1
|
||||||
|
assert isinstance(certificates[0], Certificate)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_multiple_certificates(self_signed_rsa_cert):
|
||||||
|
"""Test loading multiple certificates from PEM data."""
|
||||||
|
_, cert_path = self_signed_rsa_cert
|
||||||
|
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
pem_data = pem_cert + b"\n" + pem_cert
|
||||||
|
certificates = x509.load_certificates_from_pem(pem_data)
|
||||||
|
assert len(certificates) == 2
|
||||||
|
for loaded_cert in certificates:
|
||||||
|
assert isinstance(loaded_cert, Certificate)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_invalid_pem_data():
|
||||||
|
"""Test handling of invalid PEM data."""
|
||||||
|
invalid_pem = b"-----BEGIN CERTIFICATE-----\nInvalidCertificateData\n-----END CERTIFICATE-----"
|
||||||
|
with pytest.raises(TLSValidationError, match="TLS certificate parsing error"):
|
||||||
|
x509.load_certificates_from_pem(invalid_pem)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_key_with_valid_RSA_key(self_signed_rsa_cert):
|
||||||
|
"""Test that a valid RSA private key is accepted."""
|
||||||
|
private_key_path, _ = self_signed_rsa_cert
|
||||||
|
with open(private_key_path, "r") as private_key_file:
|
||||||
|
private_key_pem = private_key_file.read()
|
||||||
|
assert x509.validate_key(private_key_pem) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_key_with_valid_EC_key(letsencrypt_cert):
|
||||||
|
"""Test that a valid EC private key is accepted."""
|
||||||
|
private_key_path, _ = letsencrypt_cert
|
||||||
|
with open(private_key_path, "r") as private_key_file:
|
||||||
|
private_key_pem = private_key_file.read()
|
||||||
|
assert x509.validate_key(private_key_pem) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_key_with_valid_DSA_key(invalid_algorithm_key):
|
||||||
|
"""Test that a valid DSA private key is not accepted."""
|
||||||
|
with open(invalid_algorithm_key, "r") as private_key_file:
|
||||||
|
private_key_pem = private_key_file.read()
|
||||||
|
assert x509.validate_key(private_key_pem) is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scenario, key_data, expected_result, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"invalid_key_format",
|
||||||
|
"-----BEGIN INVALID KEY-----\nInvalidData\n-----END INVALID KEY-----",
|
||||||
|
None,
|
||||||
|
x509.TLSInvalidPrivateKeyError,
|
||||||
|
),
|
||||||
|
("none_key", None, False, None),
|
||||||
|
("empty_key", "", False, None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validate_key_with_invalid_key(
|
||||||
|
scenario, key_data, expected_result, expected_exception
|
||||||
|
):
|
||||||
|
"""Test that various invalid private keys are not recognized."""
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(expected_exception):
|
||||||
|
x509.validate_key(key_data)
|
||||||
|
else:
|
||||||
|
assert x509.validate_key(key_data) is expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scenario, certificate_fixture, expected_result",
|
||||||
|
[
|
||||||
|
("expired_certificate", "expired_cert", True),
|
||||||
|
("valid_certificate", "self_signed_rsa_cert", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validate_end_entity_expired(
|
||||||
|
scenario, certificate_fixture, expected_result, request
|
||||||
|
):
|
||||||
|
"""Test with both expired and valid certificates"""
|
||||||
|
_, cert_path = request.getfixturevalue(
|
||||||
|
certificate_fixture
|
||||||
|
) # Access cert_path from the fixture
|
||||||
|
with open(cert_path, "r") as cert_file:
|
||||||
|
tls_certificate_pem = cert_file.read().encode("utf-8")
|
||||||
|
end_entity_cert = x509.load_certificates_from_pem(tls_certificate_pem)[0]
|
||||||
|
assert x509.validate_end_entity_expired(end_entity_cert) is expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scenario, certificate_fixture, expected_result",
|
||||||
|
[
|
||||||
|
("not_yet_valid_certificate", "not_yet_valid_cert", True),
|
||||||
|
("valid_certificate", "self_signed_rsa_cert", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validate_end_entity_not_yet_valid(
|
||||||
|
scenario, certificate_fixture, expected_result, request
|
||||||
|
):
|
||||||
|
# Test with a certificate that is not yet valid
|
||||||
|
_, cert_path = request.getfixturevalue(
|
||||||
|
certificate_fixture
|
||||||
|
) # Access cert_path from the fixture
|
||||||
|
with open(cert_path, "r") as cert_file:
|
||||||
|
tls_certificate_pem = cert_file.read().encode("utf-8")
|
||||||
|
end_entity_cert = x509.load_certificates_from_pem(tls_certificate_pem)[0]
|
||||||
|
assert x509.validate_end_entity_not_yet_valid(end_entity_cert) is expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scenario, cert_fixture, key_fixture, expected_result",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"matching_keys_rsa",
|
||||||
|
"self_signed_rsa_cert",
|
||||||
|
"self_signed_rsa_cert",
|
||||||
|
True,
|
||||||
|
), # Matching certificate and key
|
||||||
|
(
|
||||||
|
"matching_keys_ec",
|
||||||
|
"letsencrypt_cert",
|
||||||
|
"letsencrypt_cert",
|
||||||
|
True,
|
||||||
|
), # Matching certificate and key
|
||||||
|
(
|
||||||
|
"mismatched_keys",
|
||||||
|
"self_signed_rsa_cert",
|
||||||
|
"expired_cert",
|
||||||
|
False,
|
||||||
|
), # Mismatched certificate and key
|
||||||
|
("missing_key", "self_signed_rsa_cert", None, False), # Missing private key
|
||||||
|
("missing_cert", None, "self_signed_rsa_cert", False), # Missing certificate
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validate_key_matches_cert(
|
||||||
|
scenario, cert_fixture, key_fixture, expected_result, request
|
||||||
|
):
|
||||||
|
"""Test the validate_key_matches_cert function with different scenarios"""
|
||||||
|
cert_path = None
|
||||||
|
key_path = None
|
||||||
|
|
||||||
|
if cert_fixture:
|
||||||
|
_, cert_path = request.getfixturevalue(cert_fixture)
|
||||||
|
if key_fixture:
|
||||||
|
key_path, _ = request.getfixturevalue(key_fixture)
|
||||||
|
|
||||||
|
if cert_path is None or key_path is None:
|
||||||
|
assert x509.validate_key_matches_cert(key_path, cert_path) is expected_result
|
||||||
|
return
|
||||||
|
with open(cert_path, "r") as cert_file:
|
||||||
|
tls_certificate_pem = cert_file.read().encode("utf-8")
|
||||||
|
end_entity_cert = x509.load_certificates_from_pem(tls_certificate_pem)[0]
|
||||||
|
with open(key_path, "r") as key_file:
|
||||||
|
tls_private_key_pem = key_file.read()
|
||||||
|
assert (
|
||||||
|
x509.validate_key_matches_cert(tls_private_key_pem, end_entity_cert)
|
||||||
|
is expected_result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_sans(letsencrypt_cert):
|
||||||
|
_, cert_path = letsencrypt_cert
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
certs = x509.load_certificates_from_pem(pem_cert)
|
||||||
|
sans = x509.extract_sans(certs[0])
|
||||||
|
assert isinstance(sans, list)
|
||||||
|
expected_sans = ["xahm6aech1mie5queyo8.censorship.guide"]
|
||||||
|
assert sans == expected_sans
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scenario, certificate_input, expected_result",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"invalid_certificate_format",
|
||||||
|
"-----BEGIN CERTIFICATE-----\nINVALID_CERTIFICATE\n-----END CERTIFICATE-----",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
("certificate_is_none", None, False),
|
||||||
|
("certificate_is_empty_string", "", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_extract_sans_invalid(scenario, certificate_input, expected_result):
|
||||||
|
sans = x509.extract_sans(certificate_input)
|
||||||
|
assert isinstance(sans, list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_certificate_chain_invalid_algorithm(invalid_algorithm_chain):
|
||||||
|
with pytest.raises(
|
||||||
|
TLSValidationError, match="Certificate using unsupported algorithm"
|
||||||
|
):
|
||||||
|
x509.validate_certificate_chain(invalid_algorithm_chain)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_certificate_chain_invalid_root_ca(untrusted_root_ca_chain):
|
||||||
|
with pytest.raises(
|
||||||
|
TLSValidationError,
|
||||||
|
match="Certificate chain does not terminate at a trusted root CA.",
|
||||||
|
):
|
||||||
|
x509.validate_certificate_chain(untrusted_root_ca_chain)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_certificate_chain_valid(letsencrypt_valid_chain):
|
||||||
|
assert x509.validate_certificate_chain(letsencrypt_valid_chain) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_single_certificate(self_signed_rsa_cert):
|
||||||
|
_, cert_path = self_signed_rsa_cert
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
pem_cert = cert_file.read()
|
||||||
|
certs = x509.load_certificates_from_pem(pem_cert)
|
||||||
|
assert x509.build_certificate_chain(certs) == certs
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_valid_certificate_letsencrypt(letsencrypt_valid_chain):
|
||||||
|
assert (
|
||||||
|
x509.build_certificate_chain(letsencrypt_valid_chain) == letsencrypt_valid_chain
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_certificates_in_order(untrusted_root_ca_chain):
|
||||||
|
assert (
|
||||||
|
x509.build_certificate_chain(untrusted_root_ca_chain) == untrusted_root_ca_chain
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_certificates_in_random_order(untrusted_root_ca_chain):
|
||||||
|
for perm in permutations(untrusted_root_ca_chain):
|
||||||
|
assert x509.build_certificate_chain(perm) == untrusted_root_ca_chain
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_empty_list():
|
||||||
|
with pytest.raises(
|
||||||
|
TLSValidationError, match="Cannot identify the end-entity certificate."
|
||||||
|
):
|
||||||
|
x509.build_certificate_chain([]) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_invalid_chain_no_end_entity(circular_chain):
|
||||||
|
with pytest.raises(
|
||||||
|
TLSValidationError, match="Cannot identify the end-entity certificate."
|
||||||
|
):
|
||||||
|
x509.build_certificate_chain(circular_chain)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_certificates_missing_issuers(untrusted_root_ca_chain):
|
||||||
|
untrusted_root_ca_chain.pop(1)
|
||||||
|
with pytest.raises(
|
||||||
|
TLSValidationError, match="Certificates do not form a valid chain."
|
||||||
|
):
|
||||||
|
x509.build_certificate_chain(untrusted_root_ca_chain)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_duplicate_certificates(untrusted_root_ca_chain):
|
||||||
|
duplicate_untrusted_root_ca_chain = untrusted_root_ca_chain * 2
|
||||||
|
assert (
|
||||||
|
x509.build_certificate_chain(duplicate_untrusted_root_ca_chain)
|
||||||
|
== untrusted_root_ca_chain
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_chain_non_chain_certificates(
|
||||||
|
untrusted_root_ca_chain, letsencrypt_valid_chain
|
||||||
|
):
|
||||||
|
non_chain = untrusted_root_ca_chain + letsencrypt_valid_chain
|
||||||
|
with pytest.raises(
|
||||||
|
TLSValidationError, match="Certificates do not form a valid chain."
|
||||||
|
):
|
||||||
|
x509.build_certificate_chain(non_chain)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scenario, cert_fixture, key_fixture, hostname, expected_result, expected_errors",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"empty_cert",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "no_valid_certificates",
|
||||||
|
"message": "No valid certificates supplied.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"trusted_cert_wrong_hostname",
|
||||||
|
"letsencrypt_cert",
|
||||||
|
"letsencrypt_cert",
|
||||||
|
"wrong.example.com",
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "hostname_not_in_san",
|
||||||
|
"message": "wrong.example.com not found in SANs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "hostname_not_in_san",
|
||||||
|
"message": "*.wrong.example.com not found in SANs.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"expired_self_signed_cert",
|
||||||
|
"expired_cert",
|
||||||
|
"expired_cert",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
{"key": "public_key_expired", "message": "TLS public key is expired."},
|
||||||
|
{
|
||||||
|
"key": "untrusted_root_ca",
|
||||||
|
"message": "Certificate chain does not terminate at a trusted root CA.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"not_yet_valid_self_signed_cert",
|
||||||
|
"not_yet_valid_cert",
|
||||||
|
"not_yet_valid_cert",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "public_key_future",
|
||||||
|
"message": "TLS public key is not yet valid.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "untrusted_root_ca",
|
||||||
|
"message": "Certificate chain does not terminate at a trusted root CA.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mismatched_key_cert",
|
||||||
|
"letsencrypt_cert",
|
||||||
|
"self_signed_rsa_cert",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "key_mismatch",
|
||||||
|
"message": "Private key does not match certificate.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_validate_tls_keys(
|
||||||
|
scenario,
|
||||||
|
cert_fixture,
|
||||||
|
key_fixture,
|
||||||
|
hostname,
|
||||||
|
expected_result,
|
||||||
|
expected_errors,
|
||||||
|
request,
|
||||||
|
):
|
||||||
|
# Get the certificate and key paths from the fixture
|
||||||
|
if cert_fixture and key_fixture:
|
||||||
|
_, cert_path = request.getfixturevalue(cert_fixture)
|
||||||
|
key_path, _ = request.getfixturevalue(key_fixture)
|
||||||
|
|
||||||
|
# Read the certificate and key files
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
cert_pem = cert_file.read().decode("utf-8")
|
||||||
|
with open(key_path, "rb") as key_file:
|
||||||
|
key_pem = key_file.read().decode("utf-8")
|
||||||
|
else:
|
||||||
|
cert_pem = None
|
||||||
|
key_pem = None
|
||||||
|
|
||||||
|
# Call the validate_tls_keys function
|
||||||
|
skip_name_verification = False
|
||||||
|
if not hostname:
|
||||||
|
skip_name_verification = True
|
||||||
|
chain, san_list, errors = x509.validate_tls_keys(
|
||||||
|
tls_private_key_pem=key_pem,
|
||||||
|
tls_certificate_pem=cert_pem,
|
||||||
|
hostname=hostname,
|
||||||
|
skip_name_verification=skip_name_verification,
|
||||||
|
skip_chain_verification=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that the errors match the expected errors
|
||||||
|
assert errors == expected_errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tls_keys_invalid_algorithn(
|
||||||
|
invalid_algorithm_key, self_signed_rsa_cert
|
||||||
|
):
|
||||||
|
with open(invalid_algorithm_key, "rb") as key_file:
|
||||||
|
key_pem = key_file.read().decode("utf-8")
|
||||||
|
_, cert_path = self_signed_rsa_cert
|
||||||
|
with open(cert_path, "rb") as cert_file:
|
||||||
|
cert_pem = cert_file.read().decode("utf-8")
|
||||||
|
|
||||||
|
chain, san_list, errors = x509.validate_tls_keys(
|
||||||
|
tls_private_key_pem=key_pem,
|
||||||
|
tls_certificate_pem=cert_pem,
|
||||||
|
hostname=None,
|
||||||
|
skip_name_verification=True,
|
||||||
|
skip_chain_verification=True,
|
||||||
|
)
|
||||||
|
expected_errors = [
|
||||||
|
{
|
||||||
|
"key": "private_key_not_rsa_or_ec",
|
||||||
|
"message": "Private key must be RSA or Elliptic-Curve.",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert errors == expected_errors
|
Loading…
Add table
Add a link
Reference in a new issue