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
378
app/util/x509.py
378
app/util/x509.py
|
@ -6,19 +6,77 @@ from cryptography import x509
|
|||
from cryptography.hazmat._oid import ExtensionOID
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
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.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]:
|
||||
certificates = []
|
||||
for pem_block in pem_data.split(b"-----END CERTIFICATE-----"):
|
||||
pem_block = pem_block.strip()
|
||||
if pem_block:
|
||||
pem_block += b"-----END CERTIFICATE-----"
|
||||
certificate = x509.load_pem_x509_certificate(pem_block, default_backend())
|
||||
certificates.append(certificate)
|
||||
try:
|
||||
for pem_block in pem_data.split(b"-----END CERTIFICATE-----"):
|
||||
pem_block = pem_block.strip()
|
||||
if pem_block:
|
||||
pem_block += b"-----END CERTIFICATE-----"
|
||||
certificate = x509.load_pem_x509_certificate(
|
||||
pem_block, default_backend()
|
||||
)
|
||||
certificates.append(certificate)
|
||||
except ValueError:
|
||||
raise TLSCertificateParsingError()
|
||||
return certificates
|
||||
|
||||
|
||||
|
@ -33,17 +91,22 @@ def build_certificate_chain(
|
|||
(
|
||||
cert
|
||||
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,
|
||||
)
|
||||
if not end_entity:
|
||||
raise ValueError("Cannot identify the end-entity certificate.")
|
||||
raise TLSNoEndEntityError()
|
||||
chain.append(end_entity)
|
||||
current_cert = end_entity
|
||||
while current_cert.issuer.rfc4514_string() in cert_map:
|
||||
next_cert = cert_map[current_cert.issuer.rfc4514_string()]
|
||||
# when there is 1 item left in cert_map, that will be the Root CA
|
||||
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)
|
||||
cert_map.pop(current_cert.subject.rfc4514_string())
|
||||
current_cert = next_cert
|
||||
return chain
|
||||
|
||||
|
@ -56,28 +119,142 @@ def validate_certificate_chain(chain: list[x509.Certificate]) -> bool:
|
|||
|
||||
for i in range(len(chain) - 1):
|
||||
next_public_key = chain[i + 1].public_key()
|
||||
if not (isinstance(next_public_key, RSAPublicKey)):
|
||||
raise ValueError(
|
||||
f"Certificate using unsupported algorithm: {type(next_public_key)}"
|
||||
)
|
||||
if not isinstance(next_public_key, RSAPublicKey) and not isinstance(
|
||||
next_public_key, EllipticCurvePublicKey
|
||||
):
|
||||
raise TLSUnsupportedAlgorithmError()
|
||||
hash_algorithm = chain[i].signature_hash_algorithm
|
||||
if hash_algorithm is None:
|
||||
raise ValueError("Certificate missing hash algorithm")
|
||||
next_public_key.verify(
|
||||
chain[i].signature,
|
||||
chain[i].tbs_certificate_bytes,
|
||||
PKCS1v15(),
|
||||
hash_algorithm,
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
if hash_algorithm is None:
|
||||
raise TLSMissingHashError()
|
||||
if isinstance(next_public_key, RSAPublicKey):
|
||||
next_public_key.verify(
|
||||
chain[i].signature,
|
||||
chain[i].tbs_certificate_bytes,
|
||||
PKCS1v15(),
|
||||
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]
|
||||
if not any(
|
||||
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
|
||||
|
||||
|
||||
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(
|
||||
tls_private_key_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_name_verification = skip_name_verification or False
|
||||
|
||||
certificates = []
|
||||
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:
|
||||
certificates = list(
|
||||
load_certificates_from_pem(tls_certificate_pem.encode("utf-8"))
|
||||
certificates = load_certificates_from_pem(
|
||||
tls_certificate_pem.encode("utf-8")
|
||||
)
|
||||
if not certificates:
|
||||
except TLSValidationError as e:
|
||||
errors.append(e.as_dict())
|
||||
if len(certificates) > 0:
|
||||
try:
|
||||
chain = build_certificate_chain(certificates)
|
||||
end_entity_cert = chain[0]
|
||||
# validate expiry
|
||||
if validate_end_entity_expired(end_entity_cert):
|
||||
errors.append(
|
||||
{
|
||||
"Error": "tls_certificate_invalid",
|
||||
"Message": "No valid certificate found.",
|
||||
"key": "public_key_expired",
|
||||
"message": "TLS public key is expired.",
|
||||
}
|
||||
)
|
||||
else:
|
||||
chain = build_certificate_chain(certificates)
|
||||
end_entity_cert = chain[0]
|
||||
|
||||
if end_entity_cert.not_valid_after_utc < datetime.now(timezone.utc):
|
||||
errors.append(
|
||||
{
|
||||
"Error": "tls_public_key_expired",
|
||||
"Message": "TLS public key is expired.",
|
||||
}
|
||||
)
|
||||
|
||||
if end_entity_cert.not_valid_before_utc > datetime.now(timezone.utc):
|
||||
errors.append(
|
||||
{
|
||||
"Error": "tls_public_key_future",
|
||||
"Message": "TLS public key is not yet valid.",
|
||||
}
|
||||
)
|
||||
|
||||
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:
|
||||
# validate beginning
|
||||
if validate_end_entity_not_yet_valid(end_entity_cert):
|
||||
errors.append(
|
||||
{
|
||||
"key": "public_key_future",
|
||||
"message": "TLS public key is not yet valid.",
|
||||
}
|
||||
)
|
||||
# chain verification
|
||||
if not skip_chain_verification:
|
||||
validate_certificate_chain(chain)
|
||||
# name verification
|
||||
if not skip_name_verification:
|
||||
san_list = extract_sans(end_entity_cert)
|
||||
for expected_hostname in [hostname, f"*.{hostname}"]:
|
||||
if expected_hostname not in san_list:
|
||||
errors.append(
|
||||
{
|
||||
"Error": "tls_key_mismatch",
|
||||
"Message": "Private key does not match certificate.",
|
||||
"key": "hostname_not_in_san",
|
||||
"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.",
|
||||
}
|
||||
)
|
||||
|
||||
if not skip_chain_verification:
|
||||
try:
|
||||
validate_certificate_chain(chain)
|
||||
except ValueError as e:
|
||||
errors.append(
|
||||
{"Error": "certificate_chain_invalid", "Message": str(e)}
|
||||
)
|
||||
except TLSValidationError as e:
|
||||
errors.append(e.as_dict())
|
||||
|
||||
if not skip_name_verification:
|
||||
san_list = extract_sans(end_entity_cert)
|
||||
for expected_hostname in [hostname, f"*.{hostname}"]:
|
||||
if expected_hostname not in san_list:
|
||||
errors.append(
|
||||
{
|
||||
"Error": "hostname_not_in_san",
|
||||
"Message": f"{expected_hostname} not found in SANs.",
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
errors.append({"Error": "tls_validation_error", "Message": str(e)})
|
||||
else:
|
||||
errors.append(
|
||||
{
|
||||
"key": "no_valid_certificates",
|
||||
"message": "No valid certificates supplied.",
|
||||
}
|
||||
)
|
||||
|
||||
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