Skip to content

Commit

Permalink
Merge pull request #3 from serverless-ca/new-thinking
Browse files Browse the repository at this point in the history
Update tests for client authentication as the default
  • Loading branch information
paulschwarzenberger authored Mar 10, 2024
2 parents 7434b90 + ebc61b0 commit 900b4f3
Show file tree
Hide file tree
Showing 15 changed files with 135 additions and 41 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/requirements-dev.txt → requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ setuptools==69.0.3
assertpy==1.1
boto3==1.34.30
black==22.10.0
cryptography == 42.0.2
cryptography == 42.0.4
asn1crypto == 1.5.1
certvalidator == 0.11.1
prospector==1.10.3
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
131 changes: 109 additions & 22 deletions tests/test_tls_cert.py → tests/test_issued_certs.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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,
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand All @@ -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")
Expand All @@ -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,
Expand Down Expand Up @@ -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"
Expand All @@ -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")
Expand All @@ -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,
Expand Down Expand Up @@ -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())
Expand All @@ -256,16 +334,17 @@ 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"
locality = "New York"
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}")
Expand All @@ -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,
Expand Down Expand Up @@ -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())
Expand All @@ -322,16 +402,17 @@ 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"
locality = "New York"
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}")
Expand All @@ -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,
Expand Down Expand Up @@ -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())
Expand Down
8 changes: 5 additions & 3 deletions tests/client-cert.py → utils/client-cert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("~")
Expand All @@ -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"
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions tests/client-csr.py → utils/client-csr.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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


Expand Down
File renamed without changes.
Loading

0 comments on commit 900b4f3

Please sign in to comment.