Repository: getmoto/moto
Pull request #8805 (2025-04-14 10:58:22)

fix(acm): Fix certificate issuance logic for private CAs and add corresponding tests
Fixes https://github.com/localstack/localstack/issues/11584 


Patch:
diff --git a/moto/acm/models.py b/moto/acm/models.py
index 2e34641ab..913b9da9c 100644
--- a/moto/acm/models.py
+++ b/moto/acm/models.py
@@ -229,7 +229,9 @@ class CertBundle(BaseModel):
             certificate=cert_armored,
             private_key=private_key,
             cert_type="PRIVATE" if cert_authority_arn is not None else "AMAZON_ISSUED",
-            cert_status="PENDING_VALIDATION",
+            cert_status="ISSUED"
+            if cert_authority_arn is not None
+            else "PENDING_VALIDATION",
             cert_authority_arn=cert_authority_arn,
             account_id=account_id,
             region=region,
@@ -317,6 +319,12 @@ class CertBundle(BaseModel):
             )
 
     def check(self) -> None:
+        # Check for certificate expiration
+        now = utcnow()
+        if self._not_valid_after(self._cert) <= now:
+            self.status = "EXPIRED"
+            return
+
         # Basically, if the certificate is pending, and then checked again after a
         # while, it will appear as if its been validated. The default wait time is 60
         # seconds but you can set an environment to change it.


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index 389a1024d..7fcf98d8f 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
+        uses: ruby/setup-ruby@e5ac7b085f6e63d49c8973eb0c6e04d876b881f1
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.12
diff --git a/tests/test_acm/test_acm.py b/tests/test_acm/test_acm.py
index ee0c9f1f3..8052d56ca 100644
--- a/tests/test_acm/test_acm.py
+++ b/tests/test_acm/test_acm.py
@@ -1,3 +1,4 @@
+import datetime
 import os
 import uuid
 from ipaddress import IPv4Address
@@ -260,7 +261,7 @@ def test_describe_certificate_with_pca_cert():
     assert resp["Certificate"]["DomainName"] == SERVER_COMMON_NAME
     assert resp["Certificate"]["Issuer"] == "Amazon"
     assert resp["Certificate"]["KeyAlgorithm"] == "RSA_2048"
-    assert resp["Certificate"]["Status"] == "PENDING_VALIDATION"
+    assert resp["Certificate"]["Status"] == "ISSUED"
     assert resp["Certificate"]["Type"] == "PRIVATE"
     assert resp["Certificate"]["RenewalEligibility"] == "INELIGIBLE"
     assert "Options" in resp["Certificate"]
@@ -860,3 +861,26 @@ def test_elbv2_acm_in_use_by():
         _cert for _cert in certificates if _cert["CertificateArn"] == certificate_arn
     ][0]
     assert cert["InUse"]
+
+
+@mock_aws
+def test_certificate_expiration_status():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Cant manipulate time in server mode")
+    client = boto3.client("acm", region_name="eu-central-1")
+    arn = _import_cert(client)
+
+    resp = client.describe_certificate(CertificateArn=arn)
+    assert resp["Certificate"]["Status"] == "ISSUED"
+
+    expiry_time = resp["Certificate"]["NotAfter"]
+
+    # Test before expiry (1 second before)
+    with freeze_time(expiry_time - datetime.timedelta(seconds=1)):
+        resp = client.describe_certificate(CertificateArn=arn)
+        assert resp["Certificate"]["Status"] == "ISSUED"
+
+    # Test after expiry
+    with freeze_time(expiry_time + datetime.timedelta(days=1)):  # 1 day after
+        resp = client.describe_certificate(CertificateArn=arn)
+        assert resp["Certificate"]["Status"] == "EXPIRED"