Repository: floci-io/floci (2026-04-06T20:07:01Z)

Issue:
[BUG] ACM rejects import of valid certs
## Service

ACM

## AWS API Action

ImportCertificate

## Expected behavior

Successful import

## Actual behavior

HTTP 400 error

ERROR: An error occurred (ValidationException) when calling the ImportCertificate operation: Invalid certificate: Certificate parsing failed: Invalid certificate PEM format

## Reproduction

```bash
# Generate a self-signed cert
openssl req -x509 -newkey rsa:2048 -keyout /tmp/key.pem -out /tmp/cert.pem \
  -days 365 -nodes -subj "/CN=test.example.com" 2>/dev/null

# This works — direct HTTP with PEM as plain string:
curl -s -X POST http://localhost:8081 \
  -H "X-Amz-Target: CertificateManager.ImportCertificate" \
  -H "Content-Type: application/x-amz-json-1.1" \
  -d "{\"Certificate\":\"$(sed ':a;N;$!ba;s/\n/\\n/g' /tmp/cert.pem)\",\"PrivateKey\":\"$(sed ':a;N;$!ba;s/\n/\\n/g' /tmp/key.pem)\"}"

# This fails — AWS CLI base64-encodes blobs per JSON 1.1 spec:
aws acm import-certificate \
  --endpoint-url http://localhost:8081 \
  --certificate fileb:///tmp/cert.pem \
  --private-key fileb:///tmp/key.pem
```

## Environment

- Floci version / image tag: 1.3.0
- Java SDK version (if applicable): 25
- How you're running Floci: `mvn quarkus:dev`
[BUG] ACM refuses to export an imported cert
## Service

ACM

## AWS API Action

ExportCertificate

## Expected behavior

After importing a user-provided certificate, export certificate should be allowed.

## Actual behavior

operation error ACM: ExportCertificate, https response error StatusCode: 400, RequestID: 4dddfc97-4919-4464-ba80-0a6a90385603, api error ValidationException: Certificate is not PRIVATE type and Export is not ENABLED

## Reproduction

```bash
openssl req -x509 -newkey rsa:2048 -keyout /tmp/key.pem -out /tmp/cert.pem \
  -days 365 -nodes -subj "/CN=test.example.com" 2>/dev/null

aws acm import-certificate \
  --endpoint-url http://localhost:8081 \
  --certificate fileb:///tmp/cert.pem \
  --private-key fileb:///tmp/key.pem
For the export bug:

# Import via direct HTTP (workaround for the import bug)
ARN=$(curl -s -X POST http://localhost:8081 \
  -H "X-Amz-Target: CertificateManager.ImportCertificate" \
  -H "Content-Type: application/x-amz-json-1.1" \
  -d "{\"Certificate\":\"$(sed ':a;N;$!ba;s/\n/\\n/g' /tmp/cert.pem)\",\"PrivateKey\":\"$(sed ':a;N;$!ba;s/\n/\\n/g' /tmp/key.pem)\"}" \
  | jq -r .CertificateArn)

aws acm export-certificate \
  --endpoint-url http://localhost:8081 \
  --certificate-arn "$ARN" \
  --passphrase "test-passphrase"
```

## Environment

- Floci version / image tag: 1.3.0
- Java SDK version (if applicable): 25
- How you're running Floci: `mvn quarkus:dev`


Discussion:
:tada: This issue has been resolved in version 1.4.0 :tada:

The release is available on [GitHub release](https://github.com/floci-io/floci/releases/tag/1.4.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:

:tada: This issue has been resolved in version 1.4.0 :tada:

The release is available on [GitHub release](https://github.com/floci-io/floci/releases/tag/1.4.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:



Patch:
diff --git a/AGENT.md b/AGENT.md
index 3f03e8e4..4014ed83 100644
--- a/AGENT.md
+++ b/AGENT.md
@@ -185,28 +185,16 @@ Critical areas:
     ./mvnw test -Dtest=SsmIntegrationTest
     ./mvnw test -Dtest=SsmIntegrationTest#putParameter
 
-### Manual testing
-
-    ./test-services.sh
-
 ---
 
 ## Compatibility Project
 
-Compatibility test suite:
-`./compatibility-tests/`
+Compatibility test suite: `./compatibility-tests/`
 
 Guidelines:
 
 - Prefer AWS SDK clients over raw HTTP for management-plane validation
 - Use this suite when changes may affect real SDK behavior
-- If the suite is unavailable locally, state that limitation explicitly
-
-Default module:
-
-- `sdk-test-java`
-
-Use `docker-compose-test.yml` for container-based testing.
 
 ---
 
diff --git a/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java b/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java
index 71966cbc..e4d0fb43 100644
--- a/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java
+++ b/src/main/java/io/github/hectorvent/floci/services/acm/AcmJsonHandler.java
@@ -135,9 +135,12 @@ private Response handleDeleteCertificate(JsonNode request, String region) {
     }
 
     private Response handleImportCertificate(JsonNode request, String region) {
-        String certificate = request.path("Certificate").asText();
-        String privateKey = request.path("PrivateKey").asText();
+        String certificate = decodeBlob(request.path("Certificate").asText());
+        String privateKey = decodeBlob(request.path("PrivateKey").asText());
         String chain = request.path("CertificateChain").asText(null);
+        if (chain != null) {
+            chain = decodeBlob(chain);
+        }
         String existingArn = request.path("CertificateArn").asText(null);
         Map<String, String> tags = parseTags(request.path("Tags"));
 
@@ -432,6 +435,24 @@ private List<CertificateStatus> parseCertificateStatuses(JsonNode node) {
         return list.isEmpty() ? null : list;
     }
 
+    /**
+     * Decodes a base64-encoded blob field from the AWS JSON 1.1 wire protocol.
+     * AWS SDKs send binary fields (Certificate, PrivateKey, Passphrase, etc.)
+     * as base64-encoded strings. If the value is already in PEM format (e.g. from
+     * direct HTTP calls), it is returned as-is.
+     */
+    private String decodeBlob(String value) {
+        if (value == null || value.startsWith("-----")) {
+            return value;
+        }
+        try {
+            return new String(Base64.getDecoder().decode(value));
+        } catch (IllegalArgumentException e) {
+            // Not valid base64 — return as-is
+            return value;
+        }
+    }
+
     private List<KeyAlgorithm> parseKeyTypes(JsonNode node) {
         if (!node.isArray()) return null;
         List<KeyAlgorithm> list = new ArrayList<>();
diff --git a/src/main/java/io/github/hectorvent/floci/services/acm/model/Certificate.java b/src/main/java/io/github/hectorvent/floci/services/acm/model/Certificate.java
index cfb86113..081c03f6 100644
--- a/src/main/java/io/github/hectorvent/floci/services/acm/model/Certificate.java
+++ b/src/main/java/io/github/hectorvent/floci/services/acm/model/Certificate.java
@@ -256,6 +256,7 @@ public boolean isExpired() {
 
     public boolean canExport() {
         return type == CertificateType.PRIVATE ||
+               type == CertificateType.IMPORTED ||
                (certOptions != null && "ENABLED".equals(certOptions.export()));
     }
 


Test patch:
diff --git a/compatibility-tests/README.md b/compatibility-tests/README.md
index 9f2bd831..1cb12ff2 100644
--- a/compatibility-tests/README.md
+++ b/compatibility-tests/README.md
@@ -14,6 +14,9 @@ Verifies that standard AWS tooling (SDKs, CDK, OpenTofu/Terraform) works correct
 # Copy and configure environment
 cp env.example .env
 
+# Install dependencies
+just setup
+
 # Run all tests
 just test-all
 
@@ -99,21 +102,6 @@ Bats-based suites keep their normal console output and also write JUnit XML repo
 - `compat-terraform/test-results/junit.xml`
 - `compat-opentofu/test-results/junit.xml`
 
-### TAP Output
-
-All test frameworks support TAP (Test Anything Protocol) output:
-
-```bash
-# Python
-cd sdk-test-python && pytest --tap-stream
-
-# TypeScript
-cd sdk-test-node && npm run test:tap
-
-# AWS CLI
-cd sdk-test-awscli && ./lib/bats-core/bin/bats --tap test/
-```
-
 ## Configuration
 
 All modules read from environment variables (see `.env.example`):
@@ -125,77 +113,6 @@ AWS_SECRET_ACCESS_KEY=test
 AWS_DEFAULT_REGION=us-east-1
 ```
 
-## Services Covered
-
-### Python SDK (`sdk-test-python`)
-
-16 test files covering:
-
-- SSM Parameter Store
-- SQS Queues
-- SNS Topics
-- S3 Buckets & Objects
-- S3 CORS
-- S3 Notifications
-- DynamoDB Tables
-- Lambda Functions
-- IAM Users, Roles, Policies
-- STS Identity
-- Secrets Manager
-- KMS Keys & Encryption
-- Kinesis Streams
-- CloudWatch Metrics & Alarms
-- CloudFormation
-- Cognito User Pools
-
-### TypeScript SDK (`sdk-test-node`)
-
-17 test files covering:
-
-- SSM Parameter Store
-- SQS Queues
-- SNS Topics
-- S3 Buckets & Objects
-- S3 CORS
-- S3 Notifications
-- DynamoDB Tables
-- Lambda Functions
-- IAM Roles & Policies
-- STS Identity
-- Secrets Manager
-- KMS Keys & Encryption
-- Kinesis Streams
-- CloudWatch Metrics & Alarms
-- CloudFormation Naming
-- Cognito User Pools
-- Cognito OAuth
-
-### AWS CLI Tests (`sdk-test-awscli`)
-
-12 test files covering:
-
-- SSM Parameter Store
-- SQS Queues
-- SNS Topics
-- S3 Buckets & Objects
-- S3 Notifications
-- DynamoDB Tables
-- IAM Roles & Policies
-- STS Identity
-- SES Identities & Sending
-- Secrets Manager
-- KMS Keys & Encryption
-- Cognito User Pools
-
-## CI/CD
-
-GitHub Actions workflow runs tests on pull requests and pushes:
-
-```yaml
-# .github/workflows/sdk-tests.yml
-# Matrix strategy runs python, typescript, bash in parallel
-```
-
 ## Running with Docker
 
 Each module includes a `Dockerfile` for isolated execution:
diff --git a/compatibility-tests/sdk-test-awscli/Dockerfile b/compatibility-tests/sdk-test-awscli/Dockerfile
index 89317c18..706f8044 100644
--- a/compatibility-tests/sdk-test-awscli/Dockerfile
+++ b/compatibility-tests/sdk-test-awscli/Dockerfile
@@ -1,6 +1,6 @@
 FROM amazon/aws-cli:latest
 
-RUN yum install -y git jq bc
+RUN yum install -y git jq bc openssl
 
 WORKDIR /app
 
diff --git a/compatibility-tests/sdk-test-awscli/test/acm.bats b/compatibility-tests/sdk-test-awscli/test/acm.bats
new file mode 100644
index 00000000..f107a66e
--- /dev/null
+++ b/compatibility-tests/sdk-test-awscli/test/acm.bats
@@ -0,0 +1,238 @@
+#!/usr/bin/env bats
+# ACM integration tests
+
+setup() {
+    load 'test_helper/common-setup'
+    CERT_ARN=""
+    IMPORTED_CERT_ARN=""
+}
+
+teardown() {
+    if [ -n "$CERT_ARN" ]; then
+        aws_cmd acm delete-certificate --certificate-arn "$CERT_ARN" >/dev/null 2>&1 || true
+    fi
+    if [ -n "$IMPORTED_CERT_ARN" ]; then
+        aws_cmd acm delete-certificate --certificate-arn "$IMPORTED_CERT_ARN" >/dev/null 2>&1 || true
+    fi
+}
+
+# ============================================
+# US1: Certificate Lifecycle
+# ============================================
+
+@test "ACM: request certificate" {
+    run aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS
+    assert_success
+    CERT_ARN=$(json_get "$output" '.CertificateArn')
+    [ -n "$CERT_ARN" ]
+    [[ "$CERT_ARN" =~ ^arn:aws:acm: ]]
+}
+
+@test "ACM: describe certificate" {
+    out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
+    CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm describe-certificate --certificate-arn "$CERT_ARN"
+    assert_success
+    domain=$(json_get "$output" '.Certificate.DomainName')
+    status=$(json_get "$output" '.Certificate.Status')
+    [ "$domain" = "cli-test.example.com" ]
+    [ "$status" = "ISSUED" ]
+}
+
+@test "ACM: get certificate" {
+    out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
+    CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm get-certificate --certificate-arn "$CERT_ARN"
+    assert_success
+    cert=$(json_get "$output" '.Certificate')
+    [ -n "$cert" ]
+    [[ "$cert" =~ "BEGIN CERTIFICATE" ]]
+}
+
+@test "ACM: list certificates" {
+    out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
+    CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm list-certificates
+    assert_success
+    found=$(echo "$output" | jq --arg arn "$CERT_ARN" '.CertificateSummaryList | any(.CertificateArn == $arn)')
+    [ "$found" = "true" ]
+}
+
+@test "ACM: delete certificate" {
+    out=$(aws_cmd acm request-certificate --domain-name "cli-test.example.com" --validation-method DNS)
+    CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm delete-certificate --certificate-arn "$CERT_ARN"
+    assert_success
+    CERT_ARN=""
+
+    # Verify it's gone
+    run aws_cmd acm describe-certificate --certificate-arn "$(json_get "$out" '.CertificateArn')"
+    assert_failure
+}
+
+# ============================================
+# US2: Import and Export
+# ============================================
+
+@test "ACM: import certificate" {
+    # Generate self-signed certificate with openssl
+    local key_file cert_file
+    key_file=$(mktemp)
+    cert_file=$(mktemp)
+    openssl req -x509 -newkey rsa:2048 -keyout "$key_file" -out "$cert_file" \
+        -days 365 -nodes -subj "/CN=cli-import.example.com" 2>/dev/null
+
+    run aws_cmd acm import-certificate \
+        --certificate "fileb://$cert_file" \
+        --private-key "fileb://$key_file"
+    rm -f "$key_file" "$cert_file"
+    assert_success
+    IMPORTED_CERT_ARN=$(json_get "$output" '.CertificateArn')
+    [ -n "$IMPORTED_CERT_ARN" ]
+}
+
+@test "ACM: get imported certificate" {
+    local key_file cert_file
+    key_file=$(mktemp)
+    cert_file=$(mktemp)
+    openssl req -x509 -newkey rsa:2048 -keyout "$key_file" -out "$cert_file" \
+        -days 365 -nodes -subj "/CN=cli-import.example.com" 2>/dev/null
+
+    out=$(aws_cmd acm import-certificate \
+        --certificate "fileb://$cert_file" \
+        --private-key "fileb://$key_file")
+    rm -f "$key_file" "$cert_file"
+    IMPORTED_CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm get-certificate --certificate-arn "$IMPORTED_CERT_ARN"
+    assert_success
+    cert=$(json_get "$output" '.Certificate')
+    [[ "$cert" =~ "BEGIN CERTIFICATE" ]]
+}
+
+@test "ACM: export certificate" {
+    local key_file cert_file
+    key_file=$(mktemp)
+    cert_file=$(mktemp)
+    openssl req -x509 -newkey rsa:2048 -keyout "$key_file" -out "$cert_file" \
+        -days 365 -nodes -subj "/CN=cli-export.example.com" 2>/dev/null
+
+    out=$(aws_cmd acm import-certificate \
+        --certificate "fileb://$cert_file" \
+        --private-key "fileb://$key_file")
+    rm -f "$key_file" "$cert_file"
+    IMPORTED_CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm export-certificate \
+        --certificate-arn "$IMPORTED_CERT_ARN" \
+        --passphrase "dGVzdC1wYXNzcGhyYXNl"
+    assert_success
+    cert=$(json_get "$output" '.Certificate')
+    [ -n "$cert" ]
+    pk=$(json_get "$output" '.PrivateKey')
+    [ -n "$pk" ]
+}
+
+@test "ACM: export requested certificate fails" {
+    out=$(aws_cmd acm request-certificate --domain-name "cli-noexport.example.com" --validation-method DNS)
+    CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm export-certificate \
+        --certificate-arn "$CERT_ARN" \
+        --passphrase "dGVzdC1wYXNzcGhyYXNl"
+    assert_failure
+}
+
+# ============================================
+# US3: Tagging
+# ============================================
+
+@test "ACM: add and list tags" {
+    out=$(aws_cmd acm request-certificate --domain-name "cli-tag.example.com" --validation-method DNS)
+    CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    run aws_cmd acm add-tags-to-certificate \
+        --certificate-arn "$CERT_ARN" \
+        --tags Key=Env,Value=test Key=Project,Value=floci
+    assert_success
+
+    run aws_cmd acm list-tags-for-certificate --certificate-arn "$CERT_ARN"
+    assert_success
+    env_tag=$(echo "$output" | jq '.Tags[] | select(.Key == "Env") | .Value' -r)
+    [ "$env_tag" = "test" ]
+    project_tag=$(echo "$output" | jq '.Tags[] | select(.Key == "Project") | .Value' -r)
+    [ "$project_tag" = "floci" ]
+}
+
+@test "ACM: remove tags" {
+    out=$(aws_cmd acm request-certificate --domain-name "cli-untag.example.com" --validation-method DNS)
+    CERT_ARN=$(json_get "$out" '.CertificateArn')
+
+    aws_cmd acm add-tags-to-certificate \
+        --certificate-arn "$CERT_ARN" \
+        --tags Key=Env,Value=test Key=Project,Value=floci
+
+    run aws_cmd acm remove-tags-from-certificate \
+        --certificate-arn "$CERT_ARN" \
+        --tags Key=Env
+    assert_success
+
+    run aws_cmd acm list-tags-for-certificate --certificate-arn "$CERT_ARN"
+    assert_success
+    env_gone=$(echo "$output" | jq '.Tags[] | select(.Key == "Env")' 2>/dev/null)
+    [ -z "$env_gone" ]
+    project_tag=$(echo "$output" | jq '.Tags[] | select(.Key == "Project") | .Value' -r)
+    [ "$project_tag" = "floci" ]
+}
+
+# ============================================
+# US4: Account Configuration
+# ============================================
+
+@test "ACM: put and get account configuration" {
+    run aws_cmd acm put-account-configuration \
+        --expiry-events DaysBeforeExpiry=45 \
+        --idempotency-token "cli-test-$(date +%s)"
+    assert_success
+
+    run aws_cmd acm get-account-configuration
+    assert_success
+    days=$(json_get "$output" '.ExpiryEvents.DaysBeforeExpiry')
+    [ "$days" = "45" ]
+}
+
+# ============================================
+# US5: Error Handling
+# ============================================
+
+@test "ACM: describe non-existent certificate" {
+    run aws_cmd acm describe-certificate \
+        --certificate-arn "arn:aws:acm:us-east-1:000000000000:certificate/00000000-0000-0000-0000-000000000000"
+    assert_failure
+}
+
+@test "ACM: request certificate with SANs" {
+    run aws_cmd acm request-certificate \
+        --domain-name "cli-san.example.com" \
+        --validation-method DNS \
+        --subject-alternative-names "alt1.example.com" "alt2.example.com"
+    assert_success
+    CERT_ARN=$(json_get "$output" '.CertificateArn')
+
+    run aws_cmd acm describe-certificate --certificate-arn "$CERT_ARN"
+    assert_success
+    sans=$(json_get "$output" '.Certificate.SubjectAlternativeNames')
+    [[ "$sans" =~ "alt1.example.com" ]]
+    [[ "$sans" =~ "alt2.example.com" ]]
+}
+
+@test "ACM: import invalid PEM" {
+    run aws_cmd acm import-certificate \
+        --certificate "not-valid-pem-data" \
+        --private-key "also-not-valid-pem"
+    assert_failure
+}
diff --git a/compatibility-tests/sdk-test-go/go.mod b/compatibility-tests/sdk-test-go/go.mod
index 30c4f66b..98ae6e7b 100644
--- a/compatibility-tests/sdk-test-go/go.mod
+++ b/compatibility-tests/sdk-test-go/go.mod
@@ -3,9 +3,10 @@ module floci-sdk-test-go
 go 1.24
 
 require (
-	github.com/aws/aws-sdk-go-v2 v1.41.4
+	github.com/aws/aws-sdk-go-v2 v1.41.5
 	github.com/aws/aws-sdk-go-v2/config v1.32.12
 	github.com/aws/aws-sdk-go-v2/credentials v1.19.12
+	github.com/aws/aws-sdk-go-v2/service/acm v1.38.1
 	github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.2
 	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.0
 	github.com/aws/aws-sdk-go-v2/service/iam v1.53.6
@@ -24,8 +25,8 @@ require (
 require (
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
diff --git a/compatibility-tests/sdk-test-go/internal/testutil/fixtures.go b/compatibility-tests/sdk-test-go/internal/testutil/fixtures.go
index 2806b72b..221afa8f 100644
--- a/compatibility-tests/sdk-test-go/internal/testutil/fixtures.go
+++ b/compatibility-tests/sdk-test-go/internal/testutil/fixtures.go
@@ -10,6 +10,7 @@ import (
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/config"
 	"github.com/aws/aws-sdk-go-v2/credentials"
+	"github.com/aws/aws-sdk-go-v2/service/acm"
 	"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
 	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
 	"github.com/aws/aws-sdk-go-v2/service/iam"
@@ -108,6 +109,11 @@ func CloudWatchClient() *cloudwatch.Client {
 	return cloudwatch.NewFromConfig(Config())
 }
 
+// ACMClient returns a new ACM client.
+func ACMClient() *acm.Client {
+	return acm.NewFromConfig(Config())
+}
+
 // MinimalZip returns a minimal Lambda deployment zip with a Node.js handler.
 func MinimalZip() []byte {
 	code := `exports.handler = async (event) => {
diff --git a/compatibility-tests/sdk-test-go/tests/acm_test.go b/compatibility-tests/sdk-test-go/tests/acm_test.go
new file mode 100644
index 00000000..fe4f9e8f
--- /dev/null
+++ b/compatibility-tests/sdk-test-go/tests/acm_test.go
@@ -0,0 +1,311 @@
+package tests
+
+import (
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"math/big"
+	"strings"
+	"testing"
+	"time"
+
+	"floci-sdk-test-go/internal/testutil"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/acm"
+	acmtypes "github.com/aws/aws-sdk-go-v2/service/acm/types"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// generateSelfSignedCert creates a self-signed certificate and private key in PEM format.
+func generateSelfSignedCert() (certPEM, keyPEM []byte) {
+	key, _ := rsa.GenerateKey(rand.Reader, 2048)
+	tmpl := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		Subject:      pkix.Name{CommonName: "test.example.com"},
+		NotBefore:    time.Now(),
+		NotAfter:     time.Now().Add(365 * 24 * time.Hour),
+	}
+	certDER, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+	certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
+	keyPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
+	return
+}
+
+func TestACM(t *testing.T) {
+	ctx := context.Background()
+	svc := testutil.ACMClient()
+	var certARN string
+
+	t.Run("RequestCertificate", func(t *testing.T) {
+		r, err := svc.RequestCertificate(ctx, &acm.RequestCertificateInput{
+			DomainName: aws.String("go-test.example.com"),
+		})
+		require.NoError(t, err)
+		certARN = aws.ToString(r.CertificateArn)
+		assert.NotEmpty(t, certARN)
+		assert.True(t, strings.HasPrefix(certARN, "arn:aws:acm:"), "ARN should match arn:aws:acm pattern, got: %s", certARN)
+	})
+
+	t.Run("DescribeCertificate", func(t *testing.T) {
+		if certARN == "" {
+			t.Skip("No certificate ARN from RequestCertificate")
+		}
+		r, err := svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
+			CertificateArn: aws.String(certARN),
+		})
+		require.NoError(t, err)
+		assert.Equal(t, "go-test.example.com", aws.ToString(r.Certificate.DomainName))
+		assert.Equal(t, acmtypes.CertificateStatusIssued, r.Certificate.Status)
+	})
+
+	t.Run("GetCertificate", func(t *testing.T) {
+		if certARN == "" {
+			t.Skip("No certificate ARN from RequestCertificate")
+		}
+		r, err := svc.GetCertificate(ctx, &acm.GetCertificateInput{
+			CertificateArn: aws.String(certARN),
+		})
+		require.NoError(t, err)
+		assert.NotEmpty(t, aws.ToString(r.Certificate))
+	})
+
+	t.Run("ListCertificates", func(t *testing.T) {
+		if certARN == "" {
+			t.Skip("No certificate ARN from RequestCertificate")
+		}
+		r, err := svc.ListCertificates(ctx, &acm.ListCertificatesInput{})
+		require.NoError(t, err)
+
+		found := false
+		for _, c := range r.CertificateSummaryList {
+			if aws.ToString(c.CertificateArn) == certARN {
+				found = true
+				break
+			}
+		}
+		assert.True(t, found, "Certificate %s should appear in list", certARN)
+	})
+
+	t.Run("AddTagsToCertificate", func(t *testing.T) {
+		if certARN == "" {
+			t.Skip("No certificate ARN from RequestCertificate")
+		}
+		_, err := svc.AddTagsToCertificate(ctx, &acm.AddTagsToCertificateInput{
+			CertificateArn: aws.String(certARN),
+			Tags: []acmtypes.Tag{
+				{Key: aws.String("Env"), Value: aws.String("test")},
+			},
+		})
+		require.NoError(t, err)
+
+		r, err := svc.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{
+			CertificateArn: aws.String(certARN),
+		})
+		require.NoError(t, err)
+
+		found := false
+		for _, tag := range r.Tags {
+			if aws.ToString(tag.Key) == "Env" && aws.ToString(tag.Value) == "test" {
+				found = true
+				break
+			}
+		}
+		assert.True(t, found, "Tag Env=test should be present")
+	})
+
+	t.Run("ListTagsForCertificate", func(t *testing.T) {
+		if certARN == "" {
+			t.Skip("No certificate ARN from RequestCertificate")
+		}
+		r, err := svc.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{
+			CertificateArn: aws.String(certARN),
+		})
+		require.NoError(t, err)
+
+		found := false
+		for _, tag := range r.Tags {
+			if aws.ToString(tag.Key) == "Env" && aws.ToString(tag.Value) == "test" {
+				found = true
+				break
+			}
+		}
+		assert.True(t, found, "Tag Env=test should still be present")
+	})
+
+	t.Run("RemoveTagsFromCertificate", func(t *testing.T) {
+		if certARN == "" {
+			t.Skip("No certificate ARN from RequestCertificate")
+		}
+		_, err := svc.RemoveTagsFromCertificate(ctx, &acm.RemoveTagsFromCertificateInput{
+			CertificateArn: aws.String(certARN),
+			Tags: []acmtypes.Tag{
+				{Key: aws.String("Env"), Value: aws.String("test")},
+			},
+		})
+		require.NoError(t, err)
+
+		r, err := svc.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{
+			CertificateArn: aws.String(certARN),
+		})
+		require.NoError(t, err)
+
+		for _, tag := range r.Tags {
+			assert.NotEqual(t, "Env", aws.ToString(tag.Key), "Tag Env should have been removed")
+		}
+	})
+
+	t.Run("DeleteCertificate", func(t *testing.T) {
+		if certARN == "" {
+			t.Skip("No certificate ARN from RequestCertificate")
+		}
+		_, err := svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
+			CertificateArn: aws.String(certARN),
+		})
+		require.NoError(t, err)
+
+		_, err = svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
+			CertificateArn: aws.String(certARN),
+		})
+		assert.Error(t, err, "DescribeCertificate should fail after deletion")
+	})
+}
+
+func TestACMImportExport(t *testing.T) {
+	ctx := context.Background()
+	svc := testutil.ACMClient()
+	certPEM, keyPEM := generateSelfSignedCert()
+	var importedARN string
+
+	t.Cleanup(func() {
+		if importedARN != "" {
+			svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
+				CertificateArn: aws.String(importedARN),
+			})
+		}
+	})
+
+	t.Run("ImportCertificate", func(t *testing.T) {
+		r, err := svc.ImportCertificate(ctx, &acm.ImportCertificateInput{
+			Certificate: certPEM,
+			PrivateKey:  keyPEM,
+		})
+		require.NoError(t, err)
+		importedARN = aws.ToString(r.CertificateArn)
+		assert.NotEmpty(t, importedARN)
+		assert.True(t, strings.HasPrefix(importedARN, "arn:aws:acm:"), "ARN should match arn:aws:acm pattern, got: %s", importedARN)
+	})
+
+	t.Run("GetImportedCertificate", func(t *testing.T) {
+		if importedARN == "" {
+			t.Skip("No imported certificate ARN")
+		}
+		r, err := svc.GetCertificate(ctx, &acm.GetCertificateInput{
+			CertificateArn: aws.String(importedARN),
+		})
+		require.NoError(t, err)
+		assert.NotEmpty(t, aws.ToString(r.Certificate))
+	})
+
+	t.Run("ExportCertificate", func(t *testing.T) {
+		if importedARN == "" {
+			t.Skip("No imported certificate ARN")
+		}
+		r, err := svc.ExportCertificate(ctx, &acm.ExportCertificateInput{
+			CertificateArn: aws.String(importedARN),
+			Passphrase:     []byte("test-passphrase"),
+		})
+		require.NoError(t, err)
+		assert.NotEmpty(t, aws.ToString(r.Certificate), "Exported certificate body should not be empty")
+		assert.NotEmpty(t, aws.ToString(r.PrivateKey), "Exported private key should not be empty")
+	})
+
+	t.Run("ExportRequestedCertificateFails", func(t *testing.T) {
+		// Request a certificate (not imported) - export should fail
+		reqResult, err := svc.RequestCertificate(ctx, &acm.RequestCertificateInput{
+			DomainName: aws.String("go-export-fail.example.com"),
+		})
+		require.NoError(t, err)
+		requestedARN := aws.ToString(reqResult.CertificateArn)
+
+		t.Cleanup(func() {
+			svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
+				CertificateArn: aws.String(requestedARN),
+			})
+		})
+
+		_, err = svc.ExportCertificate(ctx, &acm.ExportCertificateInput{
+			CertificateArn: aws.String(requestedARN),
+			Passphrase:     []byte("test-passphrase"),
+		})
+		assert.Error(t, err, "ExportCertificate should fail for a requested (non-imported) certificate")
+	})
+}
+
+func TestACMAccountConfiguration(t *testing.T) {
+	ctx := context.Background()
+	svc := testutil.ACMClient()
+
+	_, err := svc.PutAccountConfiguration(ctx, &acm.PutAccountConfigurationInput{
+		ExpiryEvents: &acmtypes.ExpiryEventsConfiguration{
+			DaysBeforeExpiry: aws.Int32(45),
+		},
+		IdempotencyToken: aws.String("go-test-idempotency-token"),
+	})
+	require.NoError(t, err)
+
+	r, err := svc.GetAccountConfiguration(ctx, &acm.GetAccountConfigurationInput{})
+	require.NoError(t, err)
+	require.NotNil(t, r.ExpiryEvents)
+	assert.Equal(t, int32(45), aws.ToInt32(r.ExpiryEvents.DaysBeforeExpiry))
+}
+
+func TestACMErrorHandling(t *testing.T) {
+	ctx := context.Background()
+	svc := testutil.ACMClient()
+
+	t.Run("DescribeNonExistent", func(t *testing.T) {
+		_, err := svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
+			CertificateArn: aws.String("arn:aws:acm:us-east-1:000000000000:certificate/00000000-0000-0000-0000-000000000000"),
+		})
+		assert.Error(t, err, "DescribeCertificate should fail for non-existent ARN")
+	})
+
+	t.Run("RequestWithSANs", func(t *testing.T) {
+		r, err := svc.RequestCertificate(ctx, &acm.RequestCertificateInput{
+			DomainName: aws.String("go-san-test.example.com"),
+			SubjectAlternativeNames: []string{
+				"go-san-test.example.com",
+				"www.go-san-test.example.com",
+			},
+		})
+		require.NoError(t, err)
+		sanARN := aws.ToString(r.CertificateArn)
+		assert.NotEmpty(t, sanARN)
+
+		t.Cleanup(func() {
+			svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
+				CertificateArn: aws.String(sanARN),
+			})
+		})
+
+		desc, err := svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
+			CertificateArn: aws.String(sanARN),
+		})
+		require.NoError(t, err)
+		assert.Contains(t, desc.Certificate.SubjectAlternativeNames, "go-san-test.example.com")
+		assert.Contains(t, desc.Certificate.SubjectAlternativeNames, "www.go-san-test.example.com")
+	})
+
+	t.Run("ImportInvalidPEM", func(t *testing.T) {
+		_, err := svc.ImportCertificate(ctx, &acm.ImportCertificateInput{
+			Certificate: []byte("not-valid-pem-data"),
+			PrivateKey:  []byte("not-valid-key-data"),
+		})
+		assert.Error(t, err, "ImportCertificate should fail with invalid PEM data")
+	})
+}
diff --git a/compatibility-tests/sdk-test-java/pom.xml b/compatibility-tests/sdk-test-java/pom.xml
index 12ed607b..31f19176 100644
--- a/compatibility-tests/sdk-test-java/pom.xml
+++ b/compatibility-tests/sdk-test-java/pom.xml
@@ -46,6 +46,10 @@
     </dependencyManagement>
 
     <dependencies>
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>acm</artifactId>
+        </dependency>
         <dependency>
             <groupId>software.amazon.awssdk</groupId>
             <artifactId>sqs</artifactId>
diff --git a/compatibility-tests/sdk-test-java/src/main/java/com/floci/test/TestFixtures.java b/compatibility-tests/sdk-test-java/src/main/java/com/floci/test/TestFixtures.java
index 6b03d9cf..372aa52e 100644
--- a/compatibility-tests/sdk-test-java/src/main/java/com/floci/test/TestFixtures.java
+++ b/compatibility-tests/sdk-test-java/src/main/java/com/floci/test/TestFixtures.java
@@ -26,6 +26,7 @@
 import software.amazon.awssdk.services.apigateway.ApiGatewayClient;
 import software.amazon.awssdk.services.apigatewayv2.ApiGatewayV2Client;
 import software.amazon.awssdk.services.elasticache.ElastiCacheClient;
+import software.amazon.awssdk.services.acm.AcmClient;
 import software.amazon.awssdk.services.ec2.Ec2Client;
 import software.amazon.awssdk.services.ecs.EcsClient;
 import software.amazon.awssdk.services.scheduler.SchedulerClient;
@@ -282,6 +283,14 @@ public static Ec2Client ec2Client() {
                 .build();
     }
 
+    public static AcmClient acmClient() {
+        return AcmClient.builder()
+                .endpointOverride(ENDPOINT)
+                .region(REGION)
+                .credentialsProvider(CREDENTIALS)
+                .build();
+    }
+
     public static EcsClient ecsClient() {
         return EcsClient.builder()
                 .endpointOverride(ENDPOINT)
diff --git a/compatibility-tests/sdk-test-java/src/test/java/com/floci/test/AcmTest.java b/compatibility-tests/sdk-test-java/src/test/java/com/floci/test/AcmTest.java
new file mode 100644
index 00000000..20e17753
--- /dev/null
+++ b/compatibility-tests/sdk-test-java/src/test/java/com/floci/test/AcmTest.java
@@ -0,0 +1,347 @@
+package com.floci.test;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import software.amazon.awssdk.core.SdkBytes;
+import software.amazon.awssdk.services.acm.AcmClient;
+import software.amazon.awssdk.services.acm.model.*;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.*;
+
+@DisplayName("ACM Certificate Manager")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class AcmTest {
+
+    private static AcmClient acm;
+    private static final List<String> arnsToCleanup = new ArrayList<>();
+
+    // Lifecycle test state
+    private static String requestedCertArn;
+
+    // Import/Export test state
+    private static String importedCertArn;
+    private static byte[] importedCertPem;
+    private static byte[] importedKeyPem;
+    private static String exportTestCertArn;
+
+    // Tagging test state
+    private static String tagTestCertArn;
+
+    @BeforeAll
+    static void setup() {
+        acm = TestFixtures.acmClient();
+    }
+
+    @AfterAll
+    static void cleanup() {
+        if (acm != null) {
+            for (String arn : arnsToCleanup) {
+                try {
+                    acm.deleteCertificate(b -> b.certificateArn(arn));
+                } catch (Exception ignored) {}
+            }
+            acm.close();
+        }
+    }
+
+    // ============================================
+    // Helper: generate a self-signed certificate using openssl
+    // ============================================
+    private static byte[][] generateSelfSignedCert() throws Exception {
+        Path keyFile = Files.createTempFile("test-key", ".pem");
+        Path certFile = Files.createTempFile("test-cert", ".pem");
+        try {
+            ProcessBuilder pb = new ProcessBuilder("openssl", "req", "-x509", "-newkey", "rsa:2048",
+                    "-keyout", keyFile.toString(), "-out", certFile.toString(),
+                    "-days", "365", "-nodes", "-subj", "/CN=test.example.com");
+            pb.redirectErrorStream(true);
+            Process p = pb.start();
+            p.getInputStream().readAllBytes(); // consume output
+            int exitCode = p.waitFor();
+            if (exitCode != 0) {
+                throw new RuntimeException("openssl failed with exit code " + exitCode);
+            }
+
+            byte[] certPem = Files.readAllBytes(certFile);
+            byte[] keyPem = Files.readAllBytes(keyFile);
+            return new byte[][] { certPem, keyPem };
+        } finally {
+            Files.deleteIfExists(keyFile);
+            Files.deleteIfExists(certFile);
+        }
+    }
+
+    // ============================================
+    // US1: Lifecycle tests
+    // ============================================
+
+    @Test
+    @Order(1)
+    @DisplayName("Request a certificate")
+    void testRequestCertificate() {
+        String domain = TestFixtures.uniqueName("java-test") + ".example.com";
+
+        RequestCertificateResponse response = acm.requestCertificate(b -> b
+                .domainName(domain));
+
+        requestedCertArn = response.certificateArn();
+        arnsToCleanup.add(requestedCertArn);
+
+        assertThat(requestedCertArn).isNotNull();
+        assertThat(requestedCertArn).matches("arn:aws:acm:.*:.*:certificate/.*");
+    }
+
+    @Test
+    @Order(2)
+    @DisplayName("Describe a certificate")
+    void testDescribeCertificate() {
+        Assumptions.assumeTrue(requestedCertArn != null, "RequestCertificate must succeed first");
+
+        DescribeCertificateResponse response = acm.describeCertificate(b -> b
+                .certificateArn(requestedCertArn));
+
+        CertificateDetail detail = response.certificate();
+        assertThat(detail.domainName()).contains("example.com");
+        assertThat(detail.statusAsString()).isNotNull();
+    }
+
+    @Test
+    @Order(3)
+    @DisplayName("Get a certificate")
+    void testGetCertificate() {
+        Assumptions.assumeTrue(requestedCertArn != null, "RequestCertificate must succeed first");
+
+        GetCertificateResponse response = acm.getCertificate(b -> b
+                .certificateArn(requestedCertArn));
+
+        assertThat(response.certificate()).isNotNull();
+        assertThat(response.certificate()).contains("BEGIN CERTIFICATE");
+    }
+
+    @Test
+    @Order(4)
+    @DisplayName("List certificates contains the requested cert")
+    void testListCertificates() {
+        Assumptions.assumeTrue(requestedCertArn != null, "RequestCertificate must succeed first");
+
+        ListCertificatesResponse response = acm.listCertificates();
+
+        assertThat(response.certificateSummaryList())
+                .anyMatch(s -> s.certificateArn().equals(requestedCertArn));
+    }
+
+    @Test
+    @Order(5)
+    @DisplayName("Delete a certificate")
+    void testDeleteCertificate() {
+        Assumptions.assumeTrue(requestedCertArn != null, "RequestCertificate must succeed first");
+
+        acm.deleteCertificate(b -> b.certificateArn(requestedCertArn));
+        arnsToCleanup.remove(requestedCertArn);
+
+        assertThatThrownBy(() -> acm.describeCertificate(b -> b
+                .certificateArn(requestedCertArn)))
+                .isInstanceOf(AcmException.class);
+    }
+
+    // ============================================
+    // US2: Import/Export tests
+    // ============================================
+
+    @Test
+    @Order(10)
+    @DisplayName("Import a self-signed certificate")
+    void testImportCertificate() throws Exception {
+        byte[][] certAndKey = generateSelfSignedCert();
+        importedCertPem = certAndKey[0];
+        importedKeyPem = certAndKey[1];
+
+        ImportCertificateResponse response = acm.importCertificate(b -> b
+                .certificate(SdkBytes.fromByteArray(importedCertPem))
+                .privateKey(SdkBytes.fromByteArray(importedKeyPem)));
+
+        importedCertArn = response.certificateArn();
+        arnsToCleanup.add(importedCertArn);
+
+        assertThat(importedCertArn).isNotNull();
+        assertThat(importedCertArn).matches("arn:aws:acm:.*:.*:certificate/.*");
+    }
+
+    @Test
+    @Order(11)
+    @DisplayName("Get an imported certificate")
+    void testGetImportedCertificate() {
+        Assumptions.assumeTrue(importedCertArn != null, "ImportCertificate must succeed first");
+
+        GetCertificateResponse response = acm.getCertificate(b -> b
+                .certificateArn(importedCertArn));
+
+        assertThat(response.certificate()).isNotNull();
+        assertThat(response.certificate()).contains("BEGIN CERTIFICATE");
+    }
+
+    @Test
+    @Order(12)
+    @DisplayName("Export an imported certificate with passphrase")
+    void testExportCertificate() {
+        Assumptions.assumeTrue(importedCertArn != null, "ImportCertificate must succeed first");
+
+        SdkBytes passphrase = SdkBytes.fromUtf8String("test-passphrase-123");
+
+        ExportCertificateResponse response = acm.exportCertificate(b -> b
+                .certificateArn(importedCertArn)
+                .passphrase(passphrase));
+
+        assertThat(response.certificate()).isNotNull();
+        assertThat(response.certificate()).contains("BEGIN CERTIFICATE");
+        assertThat(response.privateKey()).isNotNull();
+        assertThat(response.privateKey()).contains("-----BEGIN");
+    }
+
+    @Test
+    @Order(13)
+    @DisplayName("Export a requested (non-imported) certificate fails")
+    void testExportRequestedCertificateFails() {
+        // Request a new cert (not imported, so no private key to export)
+        String domain = TestFixtures.uniqueName("export-fail") + ".example.com";
+        RequestCertificateResponse reqResp = acm.requestCertificate(b -> b.domainName(domain));
+        exportTestCertArn = reqResp.certificateArn();
+        arnsToCleanup.add(exportTestCertArn);
+
+        SdkBytes passphrase = SdkBytes.fromUtf8String("test-passphrase");
+
+        assertThatThrownBy(() -> acm.exportCertificate(b -> b
+                .certificateArn(exportTestCertArn)
+                .passphrase(passphrase)))
+                .isInstanceOf(AcmException.class);
+    }
+
+    // ============================================
+    // US3: Tagging tests
+    // ============================================
+
+    @Test
+    @Order(20)
+    @DisplayName("Add and list tags on a certificate")
+    void testAddTags() {
+        String domain = TestFixtures.uniqueName("tag-test") + ".example.com";
+        RequestCertificateResponse reqResp = acm.requestCertificate(b -> b.domainName(domain));
+        tagTestCertArn = reqResp.certificateArn();
+        arnsToCleanup.add(tagTestCertArn);
+
+        acm.addTagsToCertificate(b -> b
+                .certificateArn(tagTestCertArn)
+                .tags(
+                        software.amazon.awssdk.services.acm.model.Tag.builder().key("Environment").value("test").build(),
+                        software.amazon.awssdk.services.acm.model.Tag.builder().key("Project").value("floci").build()
+                ));
+
+        ListTagsForCertificateResponse tagsResp = acm.listTagsForCertificate(b -> b
+                .certificateArn(tagTestCertArn));
+
+        assertThat(tagsResp.tags()).hasSize(2);
+        assertThat(tagsResp.tags())
+                .anyMatch(t -> t.key().equals("Environment") && t.value().equals("test"));
+        assertThat(tagsResp.tags())
+                .anyMatch(t -> t.key().equals("Project") && t.value().equals("floci"));
+    }
+
+    @Test
+    @Order(21)
+    @DisplayName("Remove a tag from a certificate")
+    void testRemoveTags() {
+        Assumptions.assumeTrue(tagTestCertArn != null, "AddTags must succeed first");
+
+        acm.removeTagsFromCertificate(b -> b
+                .certificateArn(tagTestCertArn)
+                .tags(software.amazon.awssdk.services.acm.model.Tag.builder().key("Project").value("floci").build()));
+
+        ListTagsForCertificateResponse tagsResp = acm.listTagsForCertificate(b -> b
+                .certificateArn(tagTestCertArn));
+
+        assertThat(tagsResp.tags()).hasSize(1);
+        assertThat(tagsResp.tags())
+                .anyMatch(t -> t.key().equals("Environment") && t.value().equals("test"));
+        assertThat(tagsResp.tags())
+                .noneMatch(t -> t.key().equals("Project"));
+    }
+
+    // ============================================
+    // US4: Account configuration tests
+    // ============================================
+
+    @Test
+    @Order(30)
+    @DisplayName("Put and get account configuration")
+    void testPutAndGetAccountConfiguration() {
+        acm.putAccountConfiguration(b -> b
+                .expiryEvents(e -> e.daysBeforeExpiry(45))
+                .idempotencyToken(TestFixtures.uniqueName()));
+
+        var response = acm.getAccountConfiguration(b -> b.build());
+
+        assertThat(response.expiryEvents()).isNotNull();
+        assertThat(response.expiryEvents().daysBeforeExpiry()).isEqualTo(45);
+    }
+
+    // ============================================
+    // US5: Error handling tests
+    // ============================================
+
+    @Test
+    @Order(40)
+    @DisplayName("Describe a non-existent certificate throws exception")
+    void testDescribeNonExistentCertificate() {
+        String fakeArn = "arn:aws:acm:us-east-1:000000000000:certificate/00000000-0000-0000-0000-000000000000";
+
+        assertThatThrownBy(() -> acm.describeCertificate(b -> b
+                .certificateArn(fakeArn)))
+                .isInstanceOf(AcmException.class);
+    }
+
+    @Test
+    @Order(41)
+    @DisplayName("Request certificate with SANs")
+    void testRequestWithSANs() {
+        String domain = TestFixtures.uniqueName("san-test") + ".example.com";
+        String san1 = "www." + domain;
+        String san2 = "api." + domain;
+
+        RequestCertificateResponse response = acm.requestCertificate(b -> b
+                .domainName(domain)
+                .subjectAlternativeNames(san1, san2));
+
+        String arn = response.certificateArn();
+        arnsToCleanup.add(arn);
+        assertThat(arn).isNotNull();
+
+        DescribeCertificateResponse descResp = acm.describeCertificate(b -> b
+                .certificateArn(arn));
+
+        assertThat(descResp.certificate().subjectAlternativeNames())
+                .contains(san1, san2);
+    }
+
+    @Test
+    @Order(42)
+    @DisplayName("Import invalid PEM throws exception")
+    void testImportInvalidPEM() {
+        byte[] garbage = "this is not a valid certificate".getBytes();
+
+        assertThatThrownBy(() -> acm.importCertificate(b -> b
+                .certificate(SdkBytes.fromByteArray(garbage))
+                .privateKey(SdkBytes.fromByteArray(garbage))))
+                .isInstanceOf(AcmException.class);
+    }
+}
diff --git a/compatibility-tests/sdk-test-node/package.json b/compatibility-tests/sdk-test-node/package.json
index e9f5e797..19e6cad9 100644
--- a/compatibility-tests/sdk-test-node/package.json
+++ b/compatibility-tests/sdk-test-node/package.json
@@ -7,6 +7,7 @@
     "test:watch": "vitest"
   },
   "dependencies": {
+    "@aws-sdk/client-acm": "^3.500.0",
     "@aws-sdk/client-cloudformation": "^3.500.0",
     "@aws-sdk/client-cloudwatch": "^3.500.0",
     "@aws-sdk/client-cognito-identity-provider": "^3.500.0",
@@ -20,7 +21,8 @@
     "@aws-sdk/client-sns": "^3.500.0",
     "@aws-sdk/client-sqs": "^3.500.0",
     "@aws-sdk/client-ssm": "^3.500.0",
-    "@aws-sdk/client-sts": "^3.500.0"
+    "@aws-sdk/client-sts": "^3.500.0",
+    "selfsigned": "^5.5.0"
   },
   "devDependencies": {
     "@types/node": "^20.0.0",
diff --git a/compatibility-tests/sdk-test-node/tests/acm.test.ts b/compatibility-tests/sdk-test-node/tests/acm.test.ts
new file mode 100644
index 00000000..798f6a38
--- /dev/null
+++ b/compatibility-tests/sdk-test-node/tests/acm.test.ts
@@ -0,0 +1,325 @@
+/**
+ * ACM integration tests.
+ */
+
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import {
+  ACMClient,
+  RequestCertificateCommand,
+  DescribeCertificateCommand,
+  GetCertificateCommand,
+  ListCertificatesCommand,
+  DeleteCertificateCommand,
+  ImportCertificateCommand,
+  ExportCertificateCommand,
+  AddTagsToCertificateCommand,
+  ListTagsForCertificateCommand,
+  RemoveTagsFromCertificateCommand,
+  PutAccountConfigurationCommand,
+  GetAccountConfigurationCommand,
+} from '@aws-sdk/client-acm';
+import selfsigned from 'selfsigned';
+import { makeClient, uniqueName, ACCOUNT, REGION } from './setup';
+
+async function generateSelfSignedCert(): Promise<{ cert: Buffer; key: Buffer }> {
+  const attrs = [{ name: 'commonName', value: 'test.example.com' }];
+  const pems = await selfsigned.generate(attrs);
+  return { cert: Buffer.from(pems.cert), key: Buffer.from(pems.private) };
+}
+
+describe('ACM Certificate Lifecycle', () => {
+  let acm: ACMClient;
+  let certificateArn: string;
+
+  beforeAll(() => {
+    acm = makeClient(ACMClient);
+  });
+
+  afterAll(async () => {
+    try {
+      if (certificateArn) {
+        await acm.send(new DeleteCertificateCommand({ CertificateArn: certificateArn }));
+      }
+    } catch {
+      // ignore
+    }
+  });
+
+  it('should request a certificate', async () => {
+    const domain = `${uniqueName('cert')}.example.com`;
+    const response = await acm.send(
+      new RequestCertificateCommand({ DomainName: domain })
+    );
+    certificateArn = response.CertificateArn!;
+    expect(certificateArn).toMatch(
+      new RegExp(`^arn:aws:acm:${REGION}:${ACCOUNT}:certificate/.+`)
+    );
+  });
+
+  it('should describe a certificate', async () => {
+    const response = await acm.send(
+      new DescribeCertificateCommand({ CertificateArn: certificateArn })
+    );
+    expect(response.Certificate?.DomainName).toBeTruthy();
+    expect(response.Certificate?.Status).toBe('ISSUED');
+  });
+
+  it('should get certificate', async () => {
+    const response = await acm.send(
+      new GetCertificateCommand({ CertificateArn: certificateArn })
+    );
+    expect(response.Certificate).toBeTruthy();
+  });
+
+  it('should list certificates', async () => {
+    const response = await acm.send(new ListCertificatesCommand({}));
+    expect(
+      response.CertificateSummaryList?.some((c) => c.CertificateArn === certificateArn)
+    ).toBe(true);
+  });
+
+  it('should delete a certificate', async () => {
+    await acm.send(new DeleteCertificateCommand({ CertificateArn: certificateArn }));
+
+    await expect(
+      acm.send(new DescribeCertificateCommand({ CertificateArn: certificateArn }))
+    ).rejects.toThrow();
+
+    // Already deleted; prevent afterAll from trying again
+    certificateArn = '';
+  });
+});
+
+describe('ACM Import/Export', () => {
+  let acm: ACMClient;
+  const arnsToClean: string[] = [];
+
+  beforeAll(() => {
+    acm = makeClient(ACMClient);
+  });
+
+  afterAll(async () => {
+    for (const arn of arnsToClean) {
+      try {
+        await acm.send(new DeleteCertificateCommand({ CertificateArn: arn }));
+      } catch {
+        // ignore
+      }
+    }
+  });
+
+  it('should import a certificate', async () => {
+    const { cert, key } = await generateSelfSignedCert();
+    const response = await acm.send(
+      new ImportCertificateCommand({
+        Certificate: cert,
+        PrivateKey: key,
+      })
+    );
+    const arn = response.CertificateArn!;
+    arnsToClean.push(arn);
+    expect(arn).toMatch(
+      new RegExp(`^arn:aws:acm:${REGION}:${ACCOUNT}:certificate/.+`)
+    );
+  });
+
+  it('should get imported certificate', async () => {
+    const { cert, key } = await generateSelfSignedCert();
+    const importResp = await acm.send(
+      new ImportCertificateCommand({
+        Certificate: cert,
+        PrivateKey: key,
+      })
+    );
+    const arn = importResp.CertificateArn!;
+    arnsToClean.push(arn);
+
+    const getResp = await acm.send(
+      new GetCertificateCommand({ CertificateArn: arn })
+    );
+    expect(getResp.Certificate).toBeTruthy();
+  });
+
+  it('should export imported certificate', async () => {
+    const { cert, key } = await generateSelfSignedCert();
+    const importResp = await acm.send(
+      new ImportCertificateCommand({
+        Certificate: cert,
+        PrivateKey: key,
+      })
+    );
+    const arn = importResp.CertificateArn!;
+    arnsToClean.push(arn);
+
+    const passphrase = Buffer.from('test-passphrase');
+    const exportResp = await acm.send(
+      new ExportCertificateCommand({
+        CertificateArn: arn,
+        Passphrase: passphrase,
+      })
+    );
+    expect(exportResp.Certificate).toBeTruthy();
+    expect(exportResp.PrivateKey).toBeTruthy();
+  });
+
+  it('should fail to export requested certificate', async () => {
+    const domain = `${uniqueName('exp')}.example.com`;
+    const reqResp = await acm.send(
+      new RequestCertificateCommand({ DomainName: domain })
+    );
+    const arn = reqResp.CertificateArn!;
+    arnsToClean.push(arn);
+
+    await expect(
+      acm.send(
+        new ExportCertificateCommand({
+          CertificateArn: arn,
+          Passphrase: Buffer.from('test-passphrase'),
+        })
+      )
+    ).rejects.toThrow();
+  });
+});
+
+describe('ACM Tagging', () => {
+  let acm: ACMClient;
+  let certificateArn: string;
+
+  beforeAll(async () => {
+    acm = makeClient(ACMClient);
+    const domain = `${uniqueName('tag')}.example.com`;
+    const response = await acm.send(
+      new RequestCertificateCommand({ DomainName: domain })
+    );
+    certificateArn = response.CertificateArn!;
+  });
+
+  afterAll(async () => {
+    try {
+      if (certificateArn) {
+        await acm.send(new DeleteCertificateCommand({ CertificateArn: certificateArn }));
+      }
+    } catch {
+      // ignore
+    }
+  });
+
+  it('should add and list tags', async () => {
+    await acm.send(
+      new AddTagsToCertificateCommand({
+        CertificateArn: certificateArn,
+        Tags: [
+          { Key: 'Environment', Value: 'test' },
+          { Key: 'Project', Value: 'floci' },
+        ],
+      })
+    );
+
+    const response = await acm.send(
+      new ListTagsForCertificateCommand({ CertificateArn: certificateArn })
+    );
+    const tags = response.Tags ?? [];
+    expect(tags.some((t) => t.Key === 'Environment' && t.Value === 'test')).toBe(true);
+    expect(tags.some((t) => t.Key === 'Project' && t.Value === 'floci')).toBe(true);
+  });
+
+  it('should remove tags', async () => {
+    // Ensure tags exist
+    await acm.send(
+      new AddTagsToCertificateCommand({
+        CertificateArn: certificateArn,
+        Tags: [{ Key: 'ToRemove', Value: 'yes' }],
+      })
+    );
+
+    await acm.send(
+      new RemoveTagsFromCertificateCommand({
+        CertificateArn: certificateArn,
+        Tags: [{ Key: 'ToRemove', Value: 'yes' }],
+      })
+    );
+
+    const response = await acm.send(
+      new ListTagsForCertificateCommand({ CertificateArn: certificateArn })
+    );
+    const tags = response.Tags ?? [];
+    expect(tags.some((t) => t.Key === 'ToRemove')).toBe(false);
+  });
+});
+
+describe('ACM Account Configuration', () => {
+  let acm: ACMClient;
+
+  beforeAll(() => {
+    acm = makeClient(ACMClient);
+  });
+
+  it('should put and get account configuration', async () => {
+    await acm.send(
+      new PutAccountConfigurationCommand({
+        ExpiryEvents: { DaysBeforeExpiry: 45 },
+        IdempotencyToken: uniqueName('idem'),
+      })
+    );
+
+    const response = await acm.send(new GetAccountConfigurationCommand({}));
+    expect(response.ExpiryEvents?.DaysBeforeExpiry).toBe(45);
+  });
+});
+
+describe('ACM Error Handling', () => {
+  let acm: ACMClient;
+  const arnsToClean: string[] = [];
+
+  beforeAll(() => {
+    acm = makeClient(ACMClient);
+  });
+
+  afterAll(async () => {
+    for (const arn of arnsToClean) {
+      try {
+        await acm.send(new DeleteCertificateCommand({ CertificateArn: arn }));
+      } catch {
+        // ignore
+      }
+    }
+  });
+
+  it('should throw for non-existent certificate', async () => {
+    const fakeArn = `arn:aws:acm:${REGION}:${ACCOUNT}:certificate/00000000-0000-0000-0000-000000000000`;
+    await expect(
+      acm.send(new DescribeCertificateCommand({ CertificateArn: fakeArn }))
+    ).rejects.toThrow();
+  });
+
+  it('should handle request with SANs', async () => {
+    const domain = `${uniqueName('san')}.example.com`;
+    const san = `alt.${domain}`;
+    const response = await acm.send(
+      new RequestCertificateCommand({
+        DomainName: domain,
+        SubjectAlternativeNames: [domain, san],
+      })
+    );
+    const arn = response.CertificateArn!;
+    arnsToClean.push(arn);
+    expect(arn).toBeTruthy();
+
+    const descResp = await acm.send(
+      new DescribeCertificateCommand({ CertificateArn: arn })
+    );
+    const sans = descResp.Certificate?.SubjectAlternativeNames ?? [];
+    expect(sans).toContain(san);
+  });
+
+  it('should fail to import invalid PEM', async () => {
+    await expect(
+      acm.send(
+        new ImportCertificateCommand({
+          Certificate: Buffer.from('not-a-valid-pem'),
+          PrivateKey: Buffer.from('also-not-valid'),
+        })
+      )
+    ).rejects.toThrow();
+  });
+});
diff --git a/compatibility-tests/sdk-test-python/conftest.py b/compatibility-tests/sdk-test-python/conftest.py
index 6f927e0b..4128e330 100644
--- a/compatibility-tests/sdk-test-python/conftest.py
+++ b/compatibility-tests/sdk-test-python/conftest.py
@@ -133,6 +133,12 @@ def cloudformation_client(aws_config, client_config):
     return boto3.client("cloudformation", config=client_config, **aws_config)
 
 
+@pytest.fixture
+def acm_client(aws_config, client_config):
+    """Create ACM client."""
+    return boto3.client("acm", config=client_config, **aws_config)
+
+
 # ============================================
 # Utility Fixtures
 # ============================================
diff --git a/compatibility-tests/sdk-test-python/requirements.txt b/compatibility-tests/sdk-test-python/requirements.txt
index 8e04cc96..1bc22898 100644
--- a/compatibility-tests/sdk-test-python/requirements.txt
+++ b/compatibility-tests/sdk-test-python/requirements.txt
@@ -1,4 +1,5 @@
 boto3==1.37.1
 botocore==1.37.1
+cryptography>=42.0.0
 pytest>=8.0.0
 pytest-timeout>=2.3.0
diff --git a/compatibility-tests/sdk-test-python/tests/test_acm.py b/compatibility-tests/sdk-test-python/tests/test_acm.py
new file mode 100644
index 00000000..ea167758
--- /dev/null
+++ b/compatibility-tests/sdk-test-python/tests/test_acm.py
@@ -0,0 +1,317 @@
+"""ACM integration tests."""
+
+import datetime
+import re
+
+import pytest
+from botocore.exceptions import ClientError
+from cryptography import x509
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.x509.oid import NameOID
+
+FAKE_ARN = "arn:aws:acm:us-east-1:000000000000:certificate/00000000-0000-0000-0000-000000000000"
+
+
+class TestACMCertificateLifecycle:
+    """Test ACM certificate lifecycle operations."""
+
+    def test_request_certificate(self, acm_client):
+        """Test RequestCertificate creates a certificate and returns a valid ARN."""
+        response = acm_client.request_certificate(DomainName="test.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            assert arn
+            assert re.match(r"arn:aws:acm:.*:.*:certificate/.*", arn)
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_describe_certificate(self, acm_client):
+        """Test DescribeCertificate returns certificate details."""
+        response = acm_client.request_certificate(DomainName="describe.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            response = acm_client.describe_certificate(CertificateArn=arn)
+            cert = response["Certificate"]
+            assert cert["DomainName"] == "describe.example.com"
+            assert cert["Status"] == "ISSUED"
+            assert cert["Type"] == "AMAZON_ISSUED"
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_get_certificate(self, acm_client):
+        """Test GetCertificate returns certificate body in PEM format."""
+        response = acm_client.request_certificate(DomainName="get.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            response = acm_client.get_certificate(CertificateArn=arn)
+            assert response["Certificate"]
+            assert "BEGIN CERTIFICATE" in response["Certificate"]
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_list_certificates(self, acm_client):
+        """Test ListCertificates includes created certificate."""
+        response = acm_client.request_certificate(DomainName="list.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            response = acm_client.list_certificates()
+            arns = [
+                c["CertificateArn"]
+                for c in response["CertificateSummaryList"]
+            ]
+            assert arn in arns
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_delete_certificate(self, acm_client):
+        """Test DeleteCertificate removes certificate."""
+        response = acm_client.request_certificate(DomainName="delete.example.com")
+        arn = response["CertificateArn"]
+
+        acm_client.delete_certificate(CertificateArn=arn)
+
+        with pytest.raises(ClientError) as exc_info:
+            acm_client.describe_certificate(CertificateArn=arn)
+        assert "ResourceNotFoundException" in str(exc_info.value)
+
+
+class TestACMImportExport:
+    """Test ACM import and export operations."""
+
+    @staticmethod
+    def _generate_self_signed_cert():
+        """Generate a self-signed certificate and private key.
+
+        Returns:
+            Tuple of (cert_pem_bytes, key_pem_bytes).
+        """
+        key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+        subject = issuer = x509.Name(
+            [x509.NameAttribute(NameOID.COMMON_NAME, "test.example.com")]
+        )
+        cert = (
+            x509.CertificateBuilder()
+            .subject_name(subject)
+            .issuer_name(issuer)
+            .public_key(key.public_key())
+            .serial_number(x509.random_serial_number())
+            .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
+            .not_valid_after(
+                datetime.datetime.now(datetime.timezone.utc)
+                + datetime.timedelta(days=365)
+            )
+            .sign(key, hashes.SHA256())
+        )
+        cert_pem = cert.public_bytes(serialization.Encoding.PEM)
+        key_pem = key.private_bytes(
+            serialization.Encoding.PEM,
+            serialization.PrivateFormat.TraditionalOpenSSL,
+            serialization.NoEncryption(),
+        )
+        return cert_pem, key_pem
+
+    def test_import_certificate(self, acm_client):
+        """Test ImportCertificate with self-signed cert returns ARN."""
+        cert_pem, key_pem = self._generate_self_signed_cert()
+
+        response = acm_client.import_certificate(
+            Certificate=cert_pem, PrivateKey=key_pem
+        )
+        arn = response["CertificateArn"]
+
+        try:
+            assert arn
+            assert re.match(r"arn:aws:acm:.*:.*:certificate/.*", arn)
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_import_certificate_with_chain(self, acm_client):
+        """Test ImportCertificate with certificate chain returns ARN."""
+        cert_pem, key_pem = self._generate_self_signed_cert()
+
+        response = acm_client.import_certificate(
+            Certificate=cert_pem,
+            PrivateKey=key_pem,
+            CertificateChain=cert_pem,
+        )
+        arn = response["CertificateArn"]
+
+        try:
+            assert arn
+            assert re.match(r"arn:aws:acm:.*:.*:certificate/.*", arn)
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_get_imported_certificate(self, acm_client):
+        """Test GetCertificate on imported cert returns matching body."""
+        cert_pem, key_pem = self._generate_self_signed_cert()
+
+        import_response = acm_client.import_certificate(
+            Certificate=cert_pem, PrivateKey=key_pem
+        )
+        arn = import_response["CertificateArn"]
+
+        try:
+            response = acm_client.get_certificate(CertificateArn=arn)
+            assert response["Certificate"]
+            assert "BEGIN CERTIFICATE" in response["Certificate"]
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_export_certificate(self, acm_client):
+        """Test ExportCertificate on imported cert returns cert and key."""
+        cert_pem, key_pem = self._generate_self_signed_cert()
+
+        import_response = acm_client.import_certificate(
+            Certificate=cert_pem, PrivateKey=key_pem
+        )
+        arn = import_response["CertificateArn"]
+
+        try:
+            response = acm_client.export_certificate(
+                CertificateArn=arn, Passphrase=b"test-passphrase"
+            )
+            assert response["Certificate"]
+            assert response["PrivateKey"]
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_export_requested_certificate_fails(self, acm_client):
+        """Test ExportCertificate on a requested (non-imported) cert raises error."""
+        response = acm_client.request_certificate(DomainName="export.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            with pytest.raises(ClientError):
+                acm_client.export_certificate(
+                    CertificateArn=arn, Passphrase=b"test-passphrase"
+                )
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+
+class TestACMTagging:
+    """Test ACM tagging operations."""
+
+    def test_add_tags(self, acm_client):
+        """Test AddTagsToCertificate and ListTagsForCertificate."""
+        response = acm_client.request_certificate(DomainName="tags.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            acm_client.add_tags_to_certificate(
+                CertificateArn=arn,
+                Tags=[
+                    {"Key": "Env", "Value": "test"},
+                    {"Key": "Project", "Value": "floci"},
+                ],
+            )
+
+            response = acm_client.list_tags_for_certificate(CertificateArn=arn)
+            tags = response["Tags"]
+            tag_map = {t["Key"]: t["Value"] for t in tags}
+            assert tag_map["Env"] == "test"
+            assert tag_map["Project"] == "floci"
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_remove_tags(self, acm_client):
+        """Test RemoveTagsFromCertificate removes specified tags."""
+        response = acm_client.request_certificate(DomainName="rmtags.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            acm_client.add_tags_to_certificate(
+                CertificateArn=arn,
+                Tags=[
+                    {"Key": "Env", "Value": "test"},
+                    {"Key": "Project", "Value": "floci"},
+                ],
+            )
+
+            acm_client.remove_tags_from_certificate(
+                CertificateArn=arn,
+                Tags=[{"Key": "Env", "Value": "test"}],
+            )
+
+            response = acm_client.list_tags_for_certificate(CertificateArn=arn)
+            tags = response.get("Tags", [])
+            tag_keys = [t["Key"] for t in tags]
+            assert "Env" not in tag_keys
+            assert "Project" in tag_keys
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_list_tags_empty(self, acm_client):
+        """Test ListTagsForCertificate on fresh cert returns empty tags."""
+        response = acm_client.request_certificate(DomainName="notags.example.com")
+        arn = response["CertificateArn"]
+
+        try:
+            response = acm_client.list_tags_for_certificate(CertificateArn=arn)
+            tags = response.get("Tags", [])
+            assert len(tags) == 0
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+
+class TestACMAccountConfiguration:
+    """Test ACM account configuration operations."""
+
+    def test_put_and_get_account_configuration(self, acm_client, unique_name):
+        """Test PutAccountConfiguration and GetAccountConfiguration."""
+        acm_client.put_account_configuration(
+            ExpiryEvents={"DaysBeforeExpiry": 45},
+            IdempotencyToken=unique_name,
+        )
+
+        response = acm_client.get_account_configuration()
+        assert response["ExpiryEvents"]["DaysBeforeExpiry"] == 45
+
+
+class TestACMErrorHandling:
+    """Test ACM error handling."""
+
+    def test_describe_nonexistent_certificate(self, acm_client):
+        """Test DescribeCertificate with fake ARN raises error."""
+        with pytest.raises(ClientError):
+            acm_client.describe_certificate(CertificateArn=FAKE_ARN)
+
+    def test_delete_nonexistent_certificate(self, acm_client):
+        """Test DeleteCertificate with fake ARN raises error."""
+        with pytest.raises(ClientError):
+            acm_client.delete_certificate(CertificateArn=FAKE_ARN)
+
+    def test_request_certificate_with_sans(self, acm_client):
+        """Test RequestCertificate with SubjectAlternativeNames."""
+        response = acm_client.request_certificate(
+            DomainName="sans.example.com",
+            SubjectAlternativeNames=[
+                "sans.example.com",
+                "www.sans.example.com",
+                "api.sans.example.com",
+            ],
+        )
+        arn = response["CertificateArn"]
+
+        try:
+            response = acm_client.describe_certificate(CertificateArn=arn)
+            sans = response["Certificate"]["SubjectAlternativeNames"]
+            assert "sans.example.com" in sans
+            assert "www.sans.example.com" in sans
+            assert "api.sans.example.com" in sans
+        finally:
+            acm_client.delete_certificate(CertificateArn=arn)
+
+    def test_import_invalid_pem(self, acm_client):
+        """Test ImportCertificate with invalid PEM raises error."""
+        with pytest.raises(ClientError):
+            acm_client.import_certificate(
+                Certificate=b"not-a-valid-certificate",
+                PrivateKey=b"not-a-valid-key",
+            )
diff --git a/compatibility-tests/sdk-test-rust/Cargo.lock b/compatibility-tests/sdk-test-rust/Cargo.lock
index aa4284f5..1c48f43f 100644
--- a/compatibility-tests/sdk-test-rust/Cargo.lock
+++ b/compatibility-tests/sdk-test-rust/Cargo.lock
@@ -138,6 +138,30 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "aws-sdk-acm"
+version = "1.101.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d2358730be0ebab65acbe6f752fe22024722e3eb944889111b1af8b8b0ca7f2"
+dependencies = [
+ "aws-credential-types",
+ "aws-runtime",
+ "aws-smithy-async",
+ "aws-smithy-http",
+ "aws-smithy-json",
+ "aws-smithy-observability",
+ "aws-smithy-runtime",
+ "aws-smithy-runtime-api",
+ "aws-smithy-types",
+ "aws-types",
+ "bytes",
+ "fastrand",
+ "http 0.2.12",
+ "http 1.4.0",
+ "regex-lite",
+ "tracing",
+]
+
 [[package]]
 name = "aws-sdk-cloudformation"
 version = "1.110.0"
@@ -1975,6 +1999,16 @@ dependencies = [
  "hmac",
 ]
 
+[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64",
+ "serde_core",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.2"
@@ -2057,6 +2091,19 @@ dependencies = [
  "getrandom 0.2.17",
 ]
 
+[[package]]
+name = "rcgen"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
+dependencies = [
+ "pem",
+ "ring",
+ "rustls-pki-types",
+ "time",
+ "yasna",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.5.18"
@@ -2250,6 +2297,7 @@ version = "0.1.0"
 dependencies = [
  "aws-config",
  "aws-credential-types",
+ "aws-sdk-acm",
  "aws-sdk-cloudformation",
  "aws-sdk-cloudwatch",
  "aws-sdk-cognitoidentityprovider",
@@ -2268,6 +2316,7 @@ dependencies = [
  "aws-types",
  "bytes",
  "futures",
+ "rcgen",
  "reqwest",
  "tokio",
  "tokio-test",
@@ -3014,6 +3063,15 @@ dependencies = [
  "lzma-sys",
 ]
 
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
 [[package]]
 name = "yoke"
 version = "0.8.1"
diff --git a/compatibility-tests/sdk-test-rust/Cargo.toml b/compatibility-tests/sdk-test-rust/Cargo.toml
index f8c2b748..f59fa1b7 100644
--- a/compatibility-tests/sdk-test-rust/Cargo.toml
+++ b/compatibility-tests/sdk-test-rust/Cargo.toml
@@ -10,6 +10,7 @@ path = "src/lib.rs"
 [dependencies]
 aws-config = "1"
 aws-credential-types = { version = "1", features = ["hardcoded-credentials"] }
+aws-sdk-acm = "1"
 aws-sdk-s3 = "1"
 aws-sdk-ssm = "1"
 aws-sdk-sqs = "1"
@@ -29,6 +30,7 @@ aws-types = "1"
 tokio = { version = "1", features = ["full"] }
 bytes = "1"
 reqwest = { version = "0.12", default-features = false }
+rcgen = "0.13"
 zip = "2"
 
 [dev-dependencies]
diff --git a/compatibility-tests/sdk-test-rust/src/lib.rs b/compatibility-tests/sdk-test-rust/src/lib.rs
index 59164e5e..e0628f1e 100644
--- a/compatibility-tests/sdk-test-rust/src/lib.rs
+++ b/compatibility-tests/sdk-test-rust/src/lib.rs
@@ -89,6 +89,11 @@ pub async fn cognito_client() -> aws_sdk_cognitoidentityprovider::Client {
     aws_sdk_cognitoidentityprovider::Client::new(&base_config().await)
 }
 
+/// Returns an ACM client.
+pub async fn acm_client() -> aws_sdk_acm::Client {
+    aws_sdk_acm::Client::new(&base_config().await)
+}
+
 /// Returns a CloudFormation client.
 pub async fn cloudformation_client() -> aws_sdk_cloudformation::Client {
     aws_sdk_cloudformation::Client::new(&base_config().await)
diff --git a/compatibility-tests/sdk-test-rust/tests/acm_test.rs b/compatibility-tests/sdk-test-rust/tests/acm_test.rs
new file mode 100644
index 00000000..3f65ab18
--- /dev/null
+++ b/compatibility-tests/sdk-test-rust/tests/acm_test.rs
@@ -0,0 +1,562 @@
+mod common;
+
+use aws_sdk_acm::primitives::Blob;
+use aws_sdk_acm::types::Tag;
+
+fn generate_self_signed_cert() -> (Vec<u8>, Vec<u8>) {
+    let cert = rcgen::generate_simple_self_signed(vec!["test.example.com".into()]).unwrap();
+    let cert_pem = cert.cert.pem().into_bytes();
+    let key_pem = cert.key_pair.serialize_pem().into_bytes();
+    (cert_pem, key_pem)
+}
+
+// ---------------------------------------------------------------------------
+// US1: Lifecycle tests
+// ---------------------------------------------------------------------------
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_request_certificate() {
+    let acm = common::acm_client().await;
+    let domain = "rust-test.example.com";
+
+    let result = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await;
+    assert!(result.is_ok(), "RequestCertificate failed: {:?}", result.err());
+
+    let cert_arn = result.unwrap().certificate_arn().unwrap_or("").to_string();
+    assert!(!cert_arn.is_empty(), "ARN should not be empty");
+    assert!(
+        cert_arn.contains("arn:") && cert_arn.contains("acm"),
+        "ARN should match expected pattern, got: {}",
+        cert_arn
+    );
+
+    // Cleanup
+    let _ = acm.delete_certificate().certificate_arn(&cert_arn).send().await;
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_describe_certificate() {
+    let acm = common::acm_client().await;
+    let domain = "rust-describe.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await
+        .expect("setup: request cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let result = acm
+        .describe_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await;
+    assert!(result.is_ok(), "DescribeCertificate failed: {:?}", result.err());
+
+    let detail = result.unwrap().certificate().unwrap().clone();
+    assert_eq!(
+        detail.domain_name().unwrap_or(""),
+        domain,
+        "Domain name should match"
+    );
+    // Status should be present (e.g. ISSUED or PENDING_VALIDATION)
+    assert!(detail.status().is_some(), "Status should be present");
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_get_certificate() {
+    let acm = common::acm_client().await;
+    let domain = "rust-get.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await
+        .expect("setup: request cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let result = acm
+        .get_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await;
+    assert!(result.is_ok(), "GetCertificate failed: {:?}", result.err());
+
+    let output = result.unwrap();
+    let body = output.certificate().unwrap_or("");
+    assert!(
+        body.contains("BEGIN CERTIFICATE"),
+        "Certificate body should contain PEM header, got: {}",
+        body
+    );
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_list_certificates() {
+    let acm = common::acm_client().await;
+    let domain = "rust-list.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await
+        .expect("setup: request cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let result = acm.list_certificates().send().await;
+    assert!(result.is_ok(), "ListCertificates failed: {:?}", result.err());
+
+    let output = result.unwrap();
+    let summaries = output.certificate_summary_list();
+    let found = summaries
+        .iter()
+        .any(|s| s.certificate_arn().unwrap_or("") == cert_arn);
+    assert!(found, "Created certificate should appear in list");
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_delete_certificate() {
+    let acm = common::acm_client().await;
+    let domain = "rust-delete.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await
+        .expect("setup: request cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let del_result = acm
+        .delete_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await;
+    assert!(del_result.is_ok(), "DeleteCertificate failed: {:?}", del_result.err());
+
+    // Verify describe now fails
+    let describe_result = acm
+        .describe_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await;
+    assert!(
+        describe_result.is_err(),
+        "DescribeCertificate should fail after deletion"
+    );
+}
+
+// ---------------------------------------------------------------------------
+// US2: Import / Export tests
+// ---------------------------------------------------------------------------
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_import_certificate() {
+    let acm = common::acm_client().await;
+    let (cert_pem, key_pem) = generate_self_signed_cert();
+
+    let result = acm
+        .import_certificate()
+        .certificate(Blob::new(cert_pem))
+        .private_key(Blob::new(key_pem))
+        .send()
+        .await;
+    assert!(result.is_ok(), "ImportCertificate failed: {:?}", result.err());
+
+    let cert_arn = result.unwrap().certificate_arn().unwrap_or("").to_string();
+    assert!(!cert_arn.is_empty(), "Imported cert ARN should not be empty");
+
+    // Cleanup
+    let _ = acm.delete_certificate().certificate_arn(&cert_arn).send().await;
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_get_imported_certificate() {
+    let acm = common::acm_client().await;
+    let (cert_pem, key_pem) = generate_self_signed_cert();
+
+    let cert_arn = acm
+        .import_certificate()
+        .certificate(Blob::new(cert_pem.clone()))
+        .private_key(Blob::new(key_pem))
+        .send()
+        .await
+        .expect("setup: import cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let result = acm
+        .get_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await;
+    assert!(result.is_ok(), "GetCertificate failed: {:?}", result.err());
+
+    let body = result.unwrap().certificate().unwrap_or("").to_string();
+    assert!(
+        body.contains("BEGIN CERTIFICATE"),
+        "Imported certificate body should contain PEM header"
+    );
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_export_certificate() {
+    let acm = common::acm_client().await;
+    let (cert_pem, key_pem) = generate_self_signed_cert();
+
+    let cert_arn = acm
+        .import_certificate()
+        .certificate(Blob::new(cert_pem))
+        .private_key(Blob::new(key_pem))
+        .send()
+        .await
+        .expect("setup: import cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let passphrase = Blob::new(b"test-passphrase".to_vec());
+    let result = acm
+        .export_certificate()
+        .certificate_arn(&cert_arn)
+        .passphrase(passphrase)
+        .send()
+        .await;
+    assert!(result.is_ok(), "ExportCertificate failed: {:?}", result.err());
+
+    let output = result.unwrap();
+    let exported_cert = output.certificate().unwrap_or("");
+    let exported_key = output.private_key().unwrap_or("");
+    assert!(
+        exported_cert.contains("BEGIN CERTIFICATE"),
+        "Exported certificate should contain PEM header"
+    );
+    assert!(
+        !exported_key.is_empty(),
+        "Exported private key should not be empty"
+    );
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_export_requested_fails() {
+    let acm = common::acm_client().await;
+    let domain = "rust-export-fail.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await
+        .expect("setup: request cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let passphrase = Blob::new(b"test-passphrase".to_vec());
+    let result = acm
+        .export_certificate()
+        .certificate_arn(&cert_arn)
+        .passphrase(passphrase)
+        .send()
+        .await;
+    assert!(
+        result.is_err(),
+        "ExportCertificate on a requested (non-imported) cert should fail"
+    );
+}
+
+// ---------------------------------------------------------------------------
+// US3: Tagging tests
+// ---------------------------------------------------------------------------
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_add_and_list_tags() {
+    let acm = common::acm_client().await;
+    let domain = "rust-tags.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await
+        .expect("setup: request cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let tag1 = Tag::builder().key("Env").value("test").build().unwrap();
+    let tag2 = Tag::builder().key("Team").value("platform").build().unwrap();
+
+    let add_result = acm
+        .add_tags_to_certificate()
+        .certificate_arn(&cert_arn)
+        .tags(tag1)
+        .tags(tag2)
+        .send()
+        .await;
+    assert!(add_result.is_ok(), "AddTagsToCertificate failed: {:?}", add_result.err());
+
+    let list_result = acm
+        .list_tags_for_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await;
+    assert!(list_result.is_ok(), "ListTagsForCertificate failed: {:?}", list_result.err());
+
+    let output = list_result.unwrap();
+    let tags = output.tags();
+    assert!(tags.len() >= 2, "Should have at least 2 tags, got {}", tags.len());
+
+    let has_env = tags.iter().any(|t| t.key() == "Env" && t.value() == Some("test"));
+    let has_team = tags.iter().any(|t| t.key() == "Team" && t.value() == Some("platform"));
+    assert!(has_env, "Should have Env=test tag");
+    assert!(has_team, "Should have Team=platform tag");
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_remove_tags() {
+    let acm = common::acm_client().await;
+    let domain = "rust-remove-tags.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .send()
+        .await
+        .expect("setup: request cert")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let tag_env = Tag::builder().key("Env").value("test").build().unwrap();
+    let tag_team = Tag::builder().key("Team").value("platform").build().unwrap();
+
+    acm.add_tags_to_certificate()
+        .certificate_arn(&cert_arn)
+        .tags(tag_env.clone())
+        .tags(tag_team)
+        .send()
+        .await
+        .expect("setup: add tags");
+
+    // Remove only the Env tag
+    let remove_result = acm
+        .remove_tags_from_certificate()
+        .certificate_arn(&cert_arn)
+        .tags(tag_env)
+        .send()
+        .await;
+    assert!(remove_result.is_ok(), "RemoveTagsFromCertificate failed: {:?}", remove_result.err());
+
+    let list_output = acm
+        .list_tags_for_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await
+        .expect("list tags after remove");
+
+    let tags = list_output.tags();
+    let has_env = tags.iter().any(|t| t.key() == "Env");
+    let has_team = tags.iter().any(|t| t.key() == "Team");
+    assert!(!has_env, "Env tag should have been removed");
+    assert!(has_team, "Team tag should still be present");
+}
+
+// ---------------------------------------------------------------------------
+// US4: Account Configuration tests
+// ---------------------------------------------------------------------------
+
+#[tokio::test]
+async fn test_acm_account_configuration() {
+    let acm = common::acm_client().await;
+
+    let expiry_config = aws_sdk_acm::types::ExpiryEventsConfiguration::builder()
+        .days_before_expiry(45)
+        .build();
+
+    let put_result = acm
+        .put_account_configuration()
+        .expiry_events(expiry_config)
+        .idempotency_token("rust-test-token")
+        .send()
+        .await;
+    assert!(put_result.is_ok(), "PutAccountConfiguration failed: {:?}", put_result.err());
+
+    let get_result = acm.get_account_configuration().send().await;
+    assert!(get_result.is_ok(), "GetAccountConfiguration failed: {:?}", get_result.err());
+
+    let config = get_result.unwrap().expiry_events().unwrap().clone();
+    assert_eq!(
+        config.days_before_expiry().unwrap_or(0),
+        45,
+        "DaysBeforeExpiry should be 45"
+    );
+}
+
+// ---------------------------------------------------------------------------
+// US5: Error handling tests
+// ---------------------------------------------------------------------------
+
+#[tokio::test]
+async fn test_acm_describe_nonexistent() {
+    let acm = common::acm_client().await;
+    let fake_arn = "arn:aws:acm:us-east-1:000000000000:certificate/00000000-0000-0000-0000-000000000000";
+
+    let result = acm
+        .describe_certificate()
+        .certificate_arn(fake_arn)
+        .send()
+        .await;
+    assert!(
+        result.is_err(),
+        "DescribeCertificate with fake ARN should fail"
+    );
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn test_acm_request_with_sans() {
+    let acm = common::acm_client().await;
+    let domain = "rust-sans.example.com";
+    let san1 = "alt1.rust-sans.example.com";
+    let san2 = "alt2.rust-sans.example.com";
+
+    let cert_arn = acm
+        .request_certificate()
+        .domain_name(domain)
+        .subject_alternative_names(san1)
+        .subject_alternative_names(san2)
+        .send()
+        .await
+        .expect("RequestCertificate with SANs")
+        .certificate_arn()
+        .unwrap()
+        .to_string();
+
+    let _guard = common::CleanupGuard::new({
+        let acm = acm.clone();
+        let arn = cert_arn.clone();
+        async move {
+            let _ = acm.delete_certificate().certificate_arn(&arn).send().await;
+        }
+    });
+
+    let detail = acm
+        .describe_certificate()
+        .certificate_arn(&cert_arn)
+        .send()
+        .await
+        .expect("DescribeCertificate")
+        .certificate()
+        .unwrap()
+        .clone();
+
+    let sans = detail.subject_alternative_names();
+    assert!(
+        sans.iter().any(|s| s == san1),
+        "SANs should contain {}, got: {:?}",
+        san1,
+        sans
+    );
+    assert!(
+        sans.iter().any(|s| s == san2),
+        "SANs should contain {}, got: {:?}",
+        san2,
+        sans
+    );
+}
+
+#[tokio::test]
+async fn test_acm_import_invalid_pem() {
+    let acm = common::acm_client().await;
+
+    let result = acm
+        .import_certificate()
+        .certificate(Blob::new(b"not-a-valid-certificate".to_vec()))
+        .private_key(Blob::new(b"not-a-valid-key".to_vec()))
+        .send()
+        .await;
+    assert!(
+        result.is_err(),
+        "ImportCertificate with invalid PEM should fail"
+    );
+}
diff --git a/src/test/java/io/github/hectorvent/floci/services/acm/AcmImportExportTest.java b/src/test/java/io/github/hectorvent/floci/services/acm/AcmImportExportTest.java
index 620348d4..af8af84d 100644
--- a/src/test/java/io/github/hectorvent/floci/services/acm/AcmImportExportTest.java
+++ b/src/test/java/io/github/hectorvent/floci/services/acm/AcmImportExportTest.java
@@ -268,6 +268,47 @@ void exportNonExportableCertificateFails() {
 
     @Test
     @Order(14)
+    void exportImportedCertificate() {
+        // Imported certificates should be exportable (they have a private key)
+        String certJson = validTestCertificate.replace("\n", "\\n");
+        String keyJson = validTestPrivateKey.replace("\n", "\\n");
+
+        String arn = given()
+            .header("X-Amz-Target", "CertificateManager.ImportCertificate")
+            .contentType(ACM_CONTENT_TYPE)
+            .body("""
+                {
+                    "Certificate": "%s",
+                    "PrivateKey": "%s"
+                }
+                """.formatted(certJson, keyJson))
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200)
+            .extract().jsonPath().getString("CertificateArn");
+
+        String passphrase = Base64.getEncoder().encodeToString("testpassphrase".getBytes());
+
+        given()
+            .header("X-Amz-Target", "CertificateManager.ExportCertificate")
+            .contentType(ACM_CONTENT_TYPE)
+            .body("""
+                {
+                    "CertificateArn": "%s",
+                    "Passphrase": "%s"
+                }
+                """.formatted(arn, passphrase))
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200)
+            .body("Certificate", startsWith("-----BEGIN CERTIFICATE-----"))
+            .body("PrivateKey", startsWith("-----BEGIN ENCRYPTED PRIVATE KEY-----"));
+    }
+
+    @Test
+    @Order(16)
     void exportCertificateNotFoundFails() {
         String passphrase = Base64.getEncoder().encodeToString("testpassphrase".getBytes());
 
@@ -288,7 +329,7 @@ void exportCertificateNotFoundFails() {
     }
 
     @Test
-    @Order(15)
+    @Order(17)
     void importInvalidCertificateFails() {
         given()
             .header("X-Amz-Target", "CertificateManager.ImportCertificate")