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

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