From 23e8e0f22544d83ef3591d05badbabdc997d8b82 Mon Sep 17 00:00:00 2001 From: Paul Schwarzenberger Date: Sun, 10 Mar 2024 20:31:02 +0000 Subject: [PATCH 1/3] Tests with client auth default --- .github/workflows/deploy.yml | 2 +- .../delete_db_table_items.py | 0 {tests/scripts => scripts}/requirements.txt | 0 .../start_ca_step_function.py | 0 tests/requirements-dev.txt | 12 -- ...{test_tls_cert.py => test_issued_certs.py} | 131 +++++++++++++++--- {tests => utils}/client-cert.py | 8 +- {tests => utils}/client-csr.py | 8 +- .../utils_tests => utils/modules}/__init__.py | 0 .../utils_tests => utils/modules}/aws/kms.py | 0 .../modules}/aws/lambdas.py | 0 .../modules}/certs/crypto.py | 7 +- .../modules}/certs/kms.py | 0 {tests => utils}/server-cert.py | 8 +- {tests => utils}/server-csr.py | 8 +- 15 files changed, 133 insertions(+), 51 deletions(-) rename {tests/scripts => scripts}/delete_db_table_items.py (100%) rename {tests/scripts => scripts}/requirements.txt (100%) rename {tests/scripts => scripts}/start_ca_step_function.py (100%) delete mode 100644 tests/requirements-dev.txt rename tests/{test_tls_cert.py => test_issued_certs.py} (72%) rename {tests => utils}/client-cert.py (92%) rename {tests => utils}/client-csr.py (85%) rename {tests/utils_tests => utils/modules}/__init__.py (100%) rename {tests/utils_tests => utils/modules}/aws/kms.py (100%) rename {tests/utils_tests => utils/modules}/aws/lambdas.py (100%) rename {tests/utils_tests => utils/modules}/certs/crypto.py (95%) rename {tests/utils_tests => utils/modules}/certs/kms.py (100%) rename {tests => utils}/server-cert.py (92%) rename {tests => utils}/server-csr.py (85%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f74440a..b74b6d4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -143,7 +143,7 @@ jobs: - name: Start CA run: | - python tests/scripts/start_ca_step_function.py + python scripts/start_ca_step_function.py integration_tests: name: Integration Tests diff --git a/tests/scripts/delete_db_table_items.py b/scripts/delete_db_table_items.py similarity index 100% rename from tests/scripts/delete_db_table_items.py rename to scripts/delete_db_table_items.py diff --git a/tests/scripts/requirements.txt b/scripts/requirements.txt similarity index 100% rename from tests/scripts/requirements.txt rename to scripts/requirements.txt diff --git a/tests/scripts/start_ca_step_function.py b/scripts/start_ca_step_function.py similarity index 100% rename from tests/scripts/start_ca_step_function.py rename to scripts/start_ca_step_function.py diff --git a/tests/requirements-dev.txt b/tests/requirements-dev.txt deleted file mode 100644 index 47c877a..0000000 --- a/tests/requirements-dev.txt +++ /dev/null @@ -1,12 +0,0 @@ -setuptools==69.0.3 -assertpy==1.1 -boto3==1.34.30 -black==22.10.0 -cryptography == 42.0.2 -asn1crypto == 1.5.1 -certvalidator == 0.11.1 -prospector==1.10.3 -bandit==1.7.5 -pytest==7.4.4 -validators==0.22.0 -requests==2.31.0 diff --git a/tests/test_tls_cert.py b/tests/test_issued_certs.py similarity index 72% rename from tests/test_tls_cert.py rename to tests/test_issued_certs.py index b42eee9..22a7844 100644 --- a/tests/test_tls_cert.py +++ b/tests/test_issued_certs.py @@ -1,24 +1,26 @@ from assertpy import assert_that import base64 +from certvalidator.errors import InvalidCertificateError from cryptography.hazmat.primitives.serialization import load_der_private_key from cryptography.hazmat.backends import default_backend from cryptography.x509 import DNSName, ExtensionOID, load_pem_x509_certificate -from tests.utils_tests.certs.crypto import ( +from utils.modules.certs.crypto import ( crypto_tls_cert_signing_request, create_csr_info, certificate_validated, convert_truststore, ) -from tests.utils_tests.certs.kms import kms_generate_key_pair -from tests.utils_tests.aws.kms import get_kms_details -from tests.utils_tests.aws.lambdas import get_lambda_name, invoke_lambda +from utils.modules.certs.kms import kms_generate_key_pair +from utils.modules.aws.kms import get_kms_details +from utils.modules.aws.lambdas import get_lambda_name, invoke_lambda -def test_tls_cert_issued_csr_no_passphrase(): +def test_cert_issued_no_passphrase(): """ - Test TLS certificate issued from a Certificate Signing Request with no passphrase + Test certificate issued from a Certificate Signing Request with no passphrase """ common_name = "pipeline-test-csr-no-passphrase.example.com" + purposes = ["server_auth"] # Get KMS details for key generation KMS key key_alias, kms_arn = get_kms_details("-tls-keygen") @@ -36,6 +38,7 @@ def test_tls_cert_issued_csr_no_passphrase(): base64_csr_data = base64.b64encode(csr).decode("utf-8") json_data = { "common_name": common_name, + "purposes": purposes, "base64_csr_data": base64_csr_data, "passphrase": False, "lifetime": 1, @@ -65,12 +68,78 @@ def test_tls_cert_issued_csr_no_passphrase(): trust_roots = convert_truststore(cert_data) # validate certificate - assert_that(certificate_validated(cert_data, trust_roots)).is_true() + assert_that(certificate_validated(cert_data, trust_roots, purposes)).is_true() + + # check client auth extension is not present in certificate + assert_that(certificate_validated).raises(InvalidCertificateError).when_called_with( + cert_data, trust_roots, ["client_auth"] + ).is_equal_to("The X.509 certificate provided is not valid for the purpose of client auth") -def test_tls_cert_issued_csr_passphrase(): +def test_client_cert_issued_only_includes_client_auth_extension(): """ - Test TLS certificate issued from a Certificate Signing Request with passphrase + Test client certificate issued only includes client authentication extension + """ + common_name = "My test client" + purposes = ["client_auth"] + + # Get KMS details for key generation KMS key + key_alias, kms_arn = get_kms_details("-tls-keygen") + print(f"Generating key pair using KMS key {key_alias}") + + # Generate key pair using KMS key to ensure randomness + private_key = load_der_private_key(kms_generate_key_pair(kms_arn)["PrivateKeyPlaintext"], None) + + csr_info = create_csr_info(common_name) + + # Generate Certificate Signing Request + csr = crypto_tls_cert_signing_request(private_key, csr_info) + + # Construct JSON data to pass to Lambda function + base64_csr_data = base64.b64encode(csr).decode("utf-8") + json_data = { + "common_name": common_name, + "purposes": purposes, + "base64_csr_data": base64_csr_data, + "passphrase": False, + "lifetime": 1, + "force_issue": True, + "cert_bundle": True, + } + + # Identify TLS certificate Lambda function + function_name = get_lambda_name("-tls") + print(f"Invoking Lambda function {function_name}") + + # Invoke TLS certificate Lambda function + response = invoke_lambda(function_name, json_data) + + # Inspect the response which includes the signed certificate + result = response["CertificateInfo"]["CommonName"] + print(f"Certificate issued for {common_name}") + + # Assert that the certificate was issued for the correct domain name + assert_that(result).is_equal_to(common_name) + + # extract certificate from response including bundled certificate chain + base64_cert_data = response["Base64Certificate"] + cert_data = base64.b64decode(base64_cert_data).decode("utf-8") + + # convert bundle to trust store format + trust_roots = convert_truststore(cert_data) + + # validate certificate including check for client auth extension + assert_that(certificate_validated(cert_data, trust_roots, purposes)).is_true() + + # check server auth extension is not present in certificate + assert_that(certificate_validated).raises(InvalidCertificateError).when_called_with( + cert_data, trust_roots, ["server_auth"] + ).is_equal_to("The X.509 certificate provided is not valid for the purpose of server auth") + + +def test_cert_issued_with_passphrase(): + """ + Test certificate issued from a Certificate Signing Request with passphrase """ common_name = "pipeline-test-csr-passphrase.example.com" @@ -116,13 +185,18 @@ def test_tls_cert_issued_csr_passphrase(): # convert bundle to trust store format trust_roots = convert_truststore(cert_data) - # validate certificate - assert_that(certificate_validated(cert_data, trust_roots)).is_true() + # validate certificate, check default setting is client auth + assert_that(certificate_validated(cert_data, trust_roots, ["client_auth"])).is_true() + # check server auth extension not present in certificate by default + assert_that(certificate_validated).raises(InvalidCertificateError).when_called_with( + cert_data, trust_roots, ["server_auth"] + ).is_equal_to("The X.509 certificate provided is not valid for the purpose of server auth") -def test_tls_cert_issued_csr_includes_specified_distinguished_name(): + +def test_issued_cert_includes_distinguished_name_specified_in_csr(): """ - Test TLS certificate issued with specified org and org unit from CSR with no passphrase + Test issued certification with no passphrase includes specified org and org unit from CSR """ common_name = "pipeline-test-dn-csr-no-passphrase.example.com" country = "GB" @@ -133,6 +207,7 @@ def test_tls_cert_issued_csr_includes_specified_distinguished_name(): expected_subject = ( "ST=England,OU=Animation Department,O=Acme Inc,L=London,C=GB,CN=pipeline-test-dn-csr-no-passphrase.example.com" ) + purposes = ["client_auth", "server_auth"] # Get KMS details for key generation KMS key key_alias, kms_arn = get_kms_details("-tls-keygen") @@ -150,6 +225,7 @@ def test_tls_cert_issued_csr_includes_specified_distinguished_name(): base64_csr_data = base64.b64encode(csr).decode("utf-8") json_data = { "common_name": common_name, + "purposes": purposes, "base64_csr_data": base64_csr_data, "lifetime": 1, "force_issue": True, @@ -186,9 +262,9 @@ def test_tls_cert_issued_csr_includes_specified_distinguished_name(): assert_that(issued_cert.subject.rfc4514_string()).is_equal_to(expected_subject) -def test_tls_cert_issued_csr_includes_correct_dns_names(): +def test_issued_cert_includes_correct_dns_names(): """ - Test TLS certificate issued contains correct DNS names in Subject Alternative Name extension + Test issued certificate contains correct DNS names in Subject Alternative Name extension """ common_name = "pipeline-test-dn-csr-no-passphrase.example.com" country = "GB" @@ -198,6 +274,7 @@ def test_tls_cert_issued_csr_includes_correct_dns_names(): state = "England" sans = ["test1.example.com", "test2.example.com", "invalid DNS name"] expected_result = ["test1.example.com", "test2.example.com"] + purposes = ["server_auth"] # Get KMS details for key generation KMS key key_alias, kms_arn = get_kms_details("-tls-keygen") @@ -215,6 +292,7 @@ def test_tls_cert_issued_csr_includes_correct_dns_names(): base64_csr_data = base64.b64encode(csr).decode("utf-8") json_data = { "common_name": common_name, + "purposes": purposes, "sans": sans, "base64_csr_data": base64_csr_data, "lifetime": 1, @@ -244,7 +322,7 @@ def test_tls_cert_issued_csr_includes_correct_dns_names(): trust_roots = convert_truststore(cert_data) # validate certificate - assert_that(certificate_validated(cert_data, trust_roots)).is_true() + assert_that(certificate_validated(cert_data, trust_roots, purposes)).is_true() # check subject of issued certificate issued_cert = load_pem_x509_certificate(cert_data.encode("utf-8"), default_backend()) @@ -256,9 +334,9 @@ def test_tls_cert_issued_csr_includes_correct_dns_names(): assert_that(sans_in_issued_cert).is_equal_to(expected_result) -def test_tls_cert_issued_csr_with_no_san_includes_correct_dns_name(): +def test_issued_cert_with_no_san_includes_correct_dns_name(): """ - Test TLS certificate with no SAN in CSR includes correct DNS name in Subject Alternative Name extension + Test issued certificate with no SAN in CSR includes correct DNS name in Subject Alternative Name extension """ common_name = "pipeline-test-common-name-in-san.example.com" country = "US" @@ -266,6 +344,7 @@ def test_tls_cert_issued_csr_with_no_san_includes_correct_dns_name(): organization = "Serverless Inc" organizational_unit = "DevOps" state = "New York" + purposes = ["server_auth"] # Get KMS details for key generation KMS key key_alias, kms_arn = get_kms_details("-tls-keygen") print(f"Generating key pair using KMS key {key_alias}") @@ -282,6 +361,7 @@ def test_tls_cert_issued_csr_with_no_san_includes_correct_dns_name(): base64_csr_data = base64.b64encode(csr).decode("utf-8") json_data = { "common_name": common_name, + "purposes": purposes, "base64_csr_data": base64_csr_data, "lifetime": 1, "force_issue": True, @@ -310,7 +390,7 @@ def test_tls_cert_issued_csr_with_no_san_includes_correct_dns_name(): trust_roots = convert_truststore(cert_data) # validate certificate - assert_that(certificate_validated(cert_data, trust_roots)).is_true() + assert_that(certificate_validated(cert_data, trust_roots, purposes)).is_true() # check subject of issued certificate issued_cert = load_pem_x509_certificate(cert_data.encode("utf-8"), default_backend()) @@ -322,9 +402,9 @@ def test_tls_cert_issued_csr_with_no_san_includes_correct_dns_name(): assert_that(sans_in_issued_cert).is_equal_to([common_name]) -def test_tls_cert_issued_without_san_if_common_name_invalid_dns(): +def test_cert_issued_without_san_if_common_name_invalid_dns(): """ - Test TLS certificate issued without SAN if common name not valid DNS and no SAN specified + Test certificate issued without SAN if common name not valid DNS and no SAN specified """ common_name = "This is not a valid DNS name" country = "US" @@ -332,6 +412,7 @@ def test_tls_cert_issued_without_san_if_common_name_invalid_dns(): organization = "Serverless Inc" organizational_unit = "DevOps" state = "New York" + purposes = ["server_auth"] # Get KMS details for key generation KMS key key_alias, kms_arn = get_kms_details("-tls-keygen") print(f"Generating key pair using KMS key {key_alias}") @@ -348,6 +429,7 @@ def test_tls_cert_issued_without_san_if_common_name_invalid_dns(): base64_csr_data = base64.b64encode(csr).decode("utf-8") json_data = { "common_name": common_name, + "purposes": purposes, "base64_csr_data": base64_csr_data, "lifetime": 1, "force_issue": True, @@ -376,7 +458,12 @@ def test_tls_cert_issued_without_san_if_common_name_invalid_dns(): trust_roots = convert_truststore(cert_data) # validate certificate - assert_that(certificate_validated(cert_data, trust_roots)).is_true() + assert_that(certificate_validated(cert_data, trust_roots, purposes)).is_true() + + # check client auth extension is not present in certificate + assert_that(certificate_validated).raises(InvalidCertificateError).when_called_with( + cert_data, trust_roots, ["client_auth"] + ).is_equal_to("The X.509 certificate provided is not valid for the purpose of client auth") # check SAN extension not present in issued certificate issued_cert = load_pem_x509_certificate(cert_data.encode("utf-8"), default_backend()) diff --git a/tests/client-cert.py b/utils/client-cert.py similarity index 92% rename from tests/client-cert.py rename to utils/client-cert.py index 5bc901c..d0f9720 100644 --- a/tests/client-cert.py +++ b/utils/client-cert.py @@ -4,9 +4,9 @@ import boto3 import os from cryptography.hazmat.primitives.serialization import load_der_private_key -from utils_tests.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request -from utils_tests.certs.kms import kms_generate_key_pair, kms_get_kms_key_id -from utils_tests.aws.lambdas import get_lambda_name +from modules.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request +from modules.certs.kms import kms_generate_key_pair, kms_get_kms_key_id +from modules.aws.lambdas import get_lambda_name # identify home directory and create certs subdirectory if needed homedir = os.path.expanduser("~") @@ -29,6 +29,7 @@ def main(): # pylint:disable=too-many-locals state = "England" organization = "Serverless Inc" organizational_unit = "Security Operations" + purposes = ["client_auth"] output_path_cert_key = f"{base_path}/client-key.pem" output_path_cert_pem = f"{base_path}/client-cert.pem" output_path_cert_crt = f"{base_path}/client-cert.crt" @@ -47,6 +48,7 @@ def main(): # pylint:disable=too-many-locals # Construct JSON data to pass to Lambda function request_payload = { "common_name": common_name, + "purposes": purposes, "lifetime": lifetime, "base64_csr_data": base64.b64encode(csr_pem).decode("utf-8"), "force_issue": True, diff --git a/tests/client-csr.py b/utils/client-csr.py similarity index 85% rename from tests/client-csr.py rename to utils/client-csr.py index a6232ae..0294771 100644 --- a/tests/client-csr.py +++ b/utils/client-csr.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import os from cryptography.hazmat.primitives.serialization import load_der_private_key -from utils_tests.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request -from utils_tests.certs.kms import kms_generate_key_pair, kms_get_kms_key_id +from modules.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request +from modules.certs.kms import kms_generate_key_pair, kms_get_kms_key_id # identify home directory and create certs subdirectory if needed @@ -15,11 +15,11 @@ def main(): # pylint:disable=too-many-locals """ - Create test Certificate Signing Request (CSR) for default Serverless CA environment + Create test client Certificate Signing Request (CSR) for default Serverless CA environment """ # set variables - common_name = "Cloud Architect" + common_name = "Cloud Engineer" country = "GB" locality = "London" state = "England" diff --git a/tests/utils_tests/__init__.py b/utils/modules/__init__.py similarity index 100% rename from tests/utils_tests/__init__.py rename to utils/modules/__init__.py diff --git a/tests/utils_tests/aws/kms.py b/utils/modules/aws/kms.py similarity index 100% rename from tests/utils_tests/aws/kms.py rename to utils/modules/aws/kms.py diff --git a/tests/utils_tests/aws/lambdas.py b/utils/modules/aws/lambdas.py similarity index 100% rename from tests/utils_tests/aws/lambdas.py rename to utils/modules/aws/lambdas.py diff --git a/tests/utils_tests/certs/crypto.py b/utils/modules/certs/crypto.py similarity index 95% rename from tests/utils_tests/certs/crypto.py rename to utils/modules/certs/crypto.py index aae4397..2946ce8 100644 --- a/tests/utils_tests/certs/crypto.py +++ b/utils/modules/certs/crypto.py @@ -18,10 +18,13 @@ def convert_truststore(cert_bundle): return trust_roots -def certificate_validated(pem_cert, trust_roots, check_crl=True): +def certificate_validated(pem_cert, trust_roots, purposes=None, check_crl=True): """ Validate certificate """ + if purposes is None: + purposes = ["server_auth", "client_auth"] + cert = pem_cert.encode(encoding="utf-8") if check_crl: cert_context = ValidationContext(allow_fetching=True, revocation_mode="hard-fail", trust_roots=trust_roots) @@ -30,7 +33,7 @@ def certificate_validated(pem_cert, trust_roots, check_crl=True): cert_context = ValidationContext(trust_roots=trust_roots) validator = CertificateValidator(cert, validation_context=cert_context) - validator.validate_usage({"digital_signature", "key_encipherment"}, {"server_auth", "client_auth"}, True) + validator.validate_usage({"digital_signature", "key_encipherment"}, set(purposes), True) return True diff --git a/tests/utils_tests/certs/kms.py b/utils/modules/certs/kms.py similarity index 100% rename from tests/utils_tests/certs/kms.py rename to utils/modules/certs/kms.py diff --git a/tests/server-cert.py b/utils/server-cert.py similarity index 92% rename from tests/server-cert.py rename to utils/server-cert.py index d93c946..e346a4b 100644 --- a/tests/server-cert.py +++ b/utils/server-cert.py @@ -4,9 +4,9 @@ import boto3 import os from cryptography.hazmat.primitives.serialization import load_der_private_key -from utils_tests.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request -from utils_tests.certs.kms import kms_generate_key_pair, kms_get_kms_key_id -from utils_tests.aws.lambdas import get_lambda_name +from modules.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request +from modules.certs.kms import kms_generate_key_pair, kms_get_kms_key_id +from modules.aws.lambdas import get_lambda_name # identify home directory and create certs subdirectory if needed homedir = os.path.expanduser("~") @@ -30,6 +30,7 @@ def main(): # pylint:disable=too-many-locals state = "England" organization = "Serverless Inc" organizational_unit = "Security Operations" + purposes = ["server_auth"] output_path_cert_key = f"{base_path}/server-key.pem" output_path_cert_pem = f"{base_path}/server-cert.pem" output_path_cert_crt = f"{base_path}/server-cert.crt" @@ -48,6 +49,7 @@ def main(): # pylint:disable=too-many-locals # Construct JSON data to pass to Lambda function request_payload = { "common_name": common_name, + "purposes": purposes, "sans": sans, "lifetime": lifetime, "base64_csr_data": base64.b64encode(csr_pem).decode("utf-8"), diff --git a/tests/server-csr.py b/utils/server-csr.py similarity index 85% rename from tests/server-csr.py rename to utils/server-csr.py index 1651010..be4e6a1 100644 --- a/tests/server-csr.py +++ b/utils/server-csr.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import os from cryptography.hazmat.primitives.serialization import load_der_private_key -from utils_tests.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request -from utils_tests.certs.kms import kms_generate_key_pair, kms_get_kms_key_id +from modules.certs.crypto import create_csr_info, crypto_encode_private_key, crypto_tls_cert_signing_request +from modules.certs.kms import kms_generate_key_pair, kms_get_kms_key_id # identify home directory and create certs subdirectory if needed @@ -15,11 +15,11 @@ def main(): # pylint:disable=too-many-locals """ - Create test Certificate Signing Request (CSR) for default Serverless CA environment + Create test server Certificate Signing Request (CSR) for default Serverless CA environment """ # set variables - common_name = "server.cloud-ca.com" + common_name = "server.example.com" country = "GB" locality = "London" state = "England" From 39f4ac8dedcb51d04bd3dc7534f2c95697a18349 Mon Sep 17 00:00:00 2001 From: Paul Schwarzenberger Date: Sun, 10 Mar 2024 20:33:39 +0000 Subject: [PATCH 2/3] add requirements-dev.txt --- requirements-dev.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..5d93c8a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,12 @@ +setuptools==69.0.3 +assertpy==1.1 +boto3==1.34.30 +black==22.10.0 +cryptography == 42.0.4 +asn1crypto == 1.5.1 +certvalidator == 0.11.1 +prospector==1.10.3 +bandit==1.7.5 +pytest==7.4.4 +validators==0.22.0 +requests==2.31.0 From ebc61b023e5b97a0678de8ae3eae6537d03019de Mon Sep 17 00:00:00 2001 From: Paul Schwarzenberger Date: Sun, 10 Mar 2024 20:37:23 +0000 Subject: [PATCH 3/3] update path to requirements file --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b74b6d4..43be6f7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -133,7 +133,7 @@ jobs: - name: Install dependencies run: | - pip install -r tests/scripts/requirements.txt + pip install -r scripts/requirements.txt - name: Configure AWS Credentials - Dev uses: aws-actions/configure-aws-credentials@v4