From d8d7dd021b32eb2a7d24262c741985d7701caf63 Mon Sep 17 00:00:00 2001 From: Zakaria Date: Sun, 25 Oct 2026 12:30:11 -0400 Subject: [PATCH] fix(acm): keep the Export option on RequestCertificate RequestCertificate always stored Export as DISABLED, so an exportable public certificate could never be exported. Read Export and the transparency logging preference the way UpdateCertificateOptions does, rejecting values other than ENABLED and DISABLED. --- .../floci/services/acm/AcmJsonHandler.java | 6 +- .../acm/AcmExportableRequestTest.java | 70 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/test/java/io/github/hectorvent/floci/services/acm/AcmExportableRequestTest.java 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 8008ef7..43ebd05 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 @@ -563,8 +563,10 @@ public class AcmJsonHandler { private CertificateOptions parseOptions(JsonNode optionsNode) { if (optionsNode.isMissingNode()) return null; - String ctPref = optionsNode.path("CertificateTransparencyLoggingPreference").asText("ENABLED"); - return new CertificateOptions(ctPref, "DISABLED"); + String ctPref = parseOptionValue(optionsNode, "CertificateTransparencyLoggingPreference"); + String export = parseOptionValue(optionsNode, "Export"); + // An exportable public certificate is requested with Export ENABLED, and keeps it for life. + return new CertificateOptions(ctPref != null ? ctPref : "ENABLED", export != null ? export : "DISABLED"); } private List parseCertificateStatuses(JsonNode node) { diff --git a/src/test/java/io/github/hectorvent/floci/services/acm/AcmExportableRequestTest.java b/src/test/java/io/github/hectorvent/floci/services/acm/AcmExportableRequestTest.java new file mode 100644 index 0000000..b4b13d1 --- /dev/null +++ b/src/test/java/io/github/hectorvent/floci/services/acm/AcmExportableRequestTest.java @@ -0,0 +1,70 @@ +package io.github.hectorvent.floci.services.acm; + +import io.github.hectorvent.floci.testing.RestAssuredJsonUtils; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +@QuarkusTest +class AcmExportableRequestTest { + + private static final String ACM_CONTENT_TYPE = "application/x-amz-json-1.1"; + + @BeforeAll + static void configureRestAssured() { + RestAssuredJsonUtils.configureAwsContentTypes(); + } + + @Test + void requestCertificateKeepsTheExportOption() { + String exportable = request("{\"DomainName\":\"exportable.example.com\",\"Options\":{\"Export\":\"ENABLED\"}}"); + + call("DescribeCertificate", "{\"CertificateArn\":\"" + exportable + "\"}") + .statusCode(200) + .body("Certificate.Options.Export", equalTo("ENABLED")) + .body("Certificate.Options.CertificateTransparencyLoggingPreference", equalTo("ENABLED")); + + call("ExportCertificate", "{\"CertificateArn\":\"" + exportable + "\",\"Passphrase\":\"c2VjcmV0\"}") + .statusCode(200) + .body("PrivateKey", notNullValue()); + } + + @Test + void requestCertificateWithoutExportIsNotExportable() { + String plain = request("{\"DomainName\":\"plain.example.com\",\"Options\":{\"CertificateTransparencyLoggingPreference\":\"DISABLED\"}}"); + + call("DescribeCertificate", "{\"CertificateArn\":\"" + plain + "\"}") + .statusCode(200) + .body("Certificate.Options.Export", equalTo("DISABLED")) + .body("Certificate.Options.CertificateTransparencyLoggingPreference", equalTo("DISABLED")); + + call("ExportCertificate", "{\"CertificateArn\":\"" + plain + "\",\"Passphrase\":\"c2VjcmV0\"}") + .statusCode(400) + .body("__type", equalTo("ValidationException")); + } + + @Test + void requestCertificateRejectsAnUnknownExportValue() { + call("RequestCertificate", "{\"DomainName\":\"bad.example.com\",\"Options\":{\"Export\":\"MAYBE\"}}") + .statusCode(400) + .body("__type", equalTo("ValidationException")); + } + + private String request(String body) { + return call("RequestCertificate", body).statusCode(200).extract().jsonPath().getString("CertificateArn"); + } + + private io.restassured.response.ValidatableResponse call(String action, String body) { + return given() + .header("X-Amz-Target", "CertificateManager." + action) + .contentType(ACM_CONTENT_TYPE) + .body(body) + .when() + .post("/") + .then(); + } +}