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:
Ana Custura 2024-12-14 14:26:10 +00:00 committed by irl
parent 5275a2a882
commit d5fa521fa1
10 changed files with 1091 additions and 120 deletions

View file

@ -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 []

View 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-----

View 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-----

View 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-----

View 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-----

View 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-----

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1IXBCW4hoVNlI+nb
Vmr0GL1Z7n607+GVTz9PlhkrhS2hRANCAAQ9qRr1MEI3IFrA1il9d10Mu3J+cP/v
yk07nT7k4Qo25Ie31umSk5dUJBki4vaBVFQH9aa0N/xbdYyZFKiamfQc
-----END PRIVATE KEY-----

View 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-----

View 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
View 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