Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions moto/acm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,29 @@ def __init__(
self._cert = self.validate_certificate()
# Extracting some common fields for ease of use
# Have to search through cert.subject for OIDs
self.common_name: Any = self._cert.subject.get_attributes_for_oid(
OID_COMMON_NAME
)[0].value

# Parse SANs once here so they can be reused in describe() without re-parsing
try:
san_obj: Any = self._cert.extensions.get_extension_for_oid(
cryptography.x509.OID_SUBJECT_ALTERNATIVE_NAME
)
self.sans: list[str] = [str(item.value) for item in san_obj.value]
except cryptography.x509.ExtensionNotFound:
self.sans = []

# CN is optional per CAB Forum baseline requirements; fall back to first SAN
# (matching real AWS ACM DomainName behaviour) or empty string if no SANs either
cn_attrs = self._cert.subject.get_attributes_for_oid(OID_COMMON_NAME)
self.common_name: Any = (
cn_attrs[0].value if cn_attrs else (self.sans[0] if self.sans else "")
)

# Parse issuer CN, also optional
issuer_cn_attrs = self._cert.issuer.get_attributes_for_oid(OID_COMMON_NAME)
self.issuer_common_name: str = str(
issuer_cn_attrs[0].value if issuer_cn_attrs else ""
)

if chain is not None:
self.validate_chain()

Expand Down Expand Up @@ -361,25 +381,12 @@ def describe(self) -> dict[str, Any]:
# Handle RSA keys
key_algo = f"RSA_{self._key.key_size}"

# Look for SANs
try:
san_obj: Any = self._cert.extensions.get_extension_for_oid(
cryptography.x509.OID_SUBJECT_ALTERNATIVE_NAME
)
except cryptography.x509.ExtensionNotFound:
san_obj = None
sans = []
if san_obj is not None:
sans = [str(item.value) for item in san_obj.value]

result: dict[str, Any] = {
"Certificate": {
"CertificateArn": self.arn,
"DomainName": self.common_name,
"InUseBy": self.in_use_by,
"Issuer": self._cert.issuer.get_attributes_for_oid(OID_COMMON_NAME)[
0
].value,
"Issuer": self.issuer_common_name,
"KeyAlgorithm": key_algo,
"NotAfter": datetime_to_epoch(self._not_valid_after(self._cert)),
"NotBefore": datetime_to_epoch(self._not_valid_before(self._cert)),
Expand All @@ -389,7 +396,7 @@ def describe(self) -> dict[str, Any]:
),
"Status": self.status, # One of PENDING_VALIDATION, ISSUED, INACTIVE, EXPIRED, VALIDATION_TIMED_OUT, REVOKED, FAILED.
"Subject": f"CN={self.common_name}",
"SubjectAlternativeNames": sans,
"SubjectAlternativeNames": self.sans,
"Type": self.type, # One of IMPORTED, AMAZON_ISSUED,
"ExtendedKeyUsages": [],
"RenewalEligibility": "INELIGIBLE",
Expand All @@ -400,7 +407,7 @@ def describe(self) -> dict[str, Any]:
if self.cert_authority_arn is not None:
result["Certificate"]["CertificateAuthorityArn"] = self.cert_authority_arn

domain_names = set(sans + [self.common_name])
domain_names = set(self.sans + ([self.common_name] if self.common_name else []))
validation_options = []

domain_name_status = "SUCCESS" if self.status == "ISSUED" else self.status
Expand Down
51 changes: 51 additions & 0 deletions tests/test_acm/test_acm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
import boto3
import pytest
from botocore.exceptions import ClientError
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509 import (
DNSName,
IPAddress,
NameOID,
SubjectAlternativeName,
load_pem_x509_certificate,
)
Expand Down Expand Up @@ -75,6 +81,51 @@
assert "CertificateChain" in resp


@mock_aws
def test_import_certificate_without_cn():
"""CN is optional per CAB Forum baseline requirements since 2017.
import_certificate should succeed and DomainName should fall back to the first SAN."""
# Generate a cert with SANs but no CN in the subject
key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
subject = x509.Name([]) # empty subject — no CN
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Test Issuer")])
)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())

Check warning on line 101 in tests/test_acm/test_acm.py

View workflow job for this annotation

GitHub Actions / test / test (3.14)

datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).

Check warning on line 101 in tests/test_acm/test_acm.py

View workflow job for this annotation

GitHub Actions / test / test (3.13)

datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).

Check warning on line 101 in tests/test_acm/test_acm.py

View workflow job for this annotation

GitHub Actions / test / test (3.12)

datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))

Check warning on line 102 in tests/test_acm/test_acm.py

View workflow job for this annotation

GitHub Actions / test / test (3.14)

datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).

Check warning on line 102 in tests/test_acm/test_acm.py

View workflow job for this annotation

GitHub Actions / test / test (3.13)

datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).

Check warning on line 102 in tests/test_acm/test_acm.py

View workflow job for this annotation

GitHub Actions / test / test (3.12)

datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
.add_extension(
x509.SubjectAlternativeName(
[DNSName("app.test.example.com"), DNSName("app2.test.example.com")]
),
critical=False,
)
.sign(key, hashes.SHA256(), default_backend())
)
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
key_pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)

client = boto3.client("acm", region_name="us-east-1")
resp = client.import_certificate(Certificate=cert_pem, PrivateKey=key_pem)
arn = resp["CertificateArn"]

desc = client.describe_certificate(CertificateArn=arn)["Certificate"]
# DomainName should fall back to the first SAN when CN is absent
assert desc["DomainName"] == "app.test.example.com"
assert "app.test.example.com" in desc["SubjectAlternativeNames"]
assert "app2.test.example.com" in desc["SubjectAlternativeNames"]


@mock_aws
def test_import_certificate_with_tags():
client = boto3.client("acm", region_name="eu-central-1")
Expand Down
Loading