From 908e11b1524fbde69bd1be4bcff7caf0acd2cdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Mon, 26 Feb 2024 18:22:07 +0100 Subject: [PATCH] Workaround for ssl.com metadata bug ssl.com requires EAB for account creation, but the metadata's "externalAccountRequired" property gives "false", indicating that no EAB is used. This fix patches the read directory's metadata if the ssl.com provider is used. --- .../provider/sslcom/SslComAcmeProvider.java | 21 +++++++++++++++++++ .../org/shredzone/acme4j/toolbox/JSON.java | 15 +++++++++++++ .../org/shredzone/acme4j/it/ProviderIT.java | 15 +++++++++---- src/doc/docs/ca/sslcom.md | 2 +- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java index 191bd23a..b7f02a61 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java @@ -16,10 +16,14 @@ package org.shredzone.acme4j.provider.sslcom; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.util.Map; +import org.shredzone.acme4j.Session; +import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.provider.AbstractAcmeProvider; import org.shredzone.acme4j.provider.AcmeProvider; +import org.shredzone.acme4j.toolbox.JSON; /** * An {@link AcmeProvider} for SSL.com. @@ -68,4 +72,21 @@ public class SslComAcmeProvider extends AbstractAcmeProvider { } } + @Override + @SuppressWarnings("unchecked") + public JSON directory(Session session, URI serverUri) throws AcmeException { + // This is a workaround for a bug at SSL.com. It requires account registration + // by EAB, but the "externalAccountRequired" flag in the directory is set to + // false. This patch reads the directory and forcefully sets the flag to true. + // The entire method can be removed once it is fixed on SSL.com side. + var directory = super.directory(session, serverUri).toMap(); + var meta = directory.get("meta"); + if (meta instanceof Map) { + var metaMap = ((Map) meta); + metaMap.remove("externalAccountRequired"); + metaMap.put("externalAccountRequired", true); + } + return JSON.fromMap(directory); + } + } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java index d7b2c2b0..6f06baba 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java @@ -118,6 +118,21 @@ public final class JSON implements Serializable { } } + /** + * Creates a JSON object from a map. + *

+ * The map's content is deeply copied. Changes to the map won't reflect in the created + * JSON structure. + * + * @param data + * Map structure + * @return {@link JSON} of the map's content. + * @since 3.2.0 + */ + public static JSON fromMap(Map data) { + return JSON.parse(JsonUtil.toJson(data)); + } + /** * Returns a {@link JSON} of an empty document. * diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java index 952c95da..5fcc024b 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java @@ -74,26 +74,33 @@ public class ProviderIT { var sessionEcc = new Session("acme://ssl.com/ecc"); assertThat(sessionEcc.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThatNoException().isThrownBy(() -> sessionEcc.resourceUrl(Resource.NEW_ACCOUNT)); - assertThat(sessionEcc.getMetadata().isExternalAccountRequired()).isFalse(); + assertThat(sessionEcc.getMetadata().isExternalAccountRequired()).isTrue(); assertThat(sessionEcc.getMetadata().isAutoRenewalEnabled()).isFalse(); var sessionRsa = new Session("acme://ssl.com/rsa"); assertThat(sessionRsa.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThatNoException().isThrownBy(() -> sessionRsa.resourceUrl(Resource.NEW_ACCOUNT)); - assertThat(sessionRsa.getMetadata().isExternalAccountRequired()).isFalse(); + assertThat(sessionRsa.getMetadata().isExternalAccountRequired()).isTrue(); assertThat(sessionRsa.getMetadata().isAutoRenewalEnabled()).isFalse(); var sessionEccStage = new Session("acme://ssl.com/staging/ecc"); assertThat(sessionEccStage.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThatNoException().isThrownBy(() -> sessionEccStage.resourceUrl(Resource.NEW_ACCOUNT)); - assertThat(sessionEccStage.getMetadata().isExternalAccountRequired()).isFalse(); + assertThat(sessionEccStage.getMetadata().isExternalAccountRequired()).isTrue(); assertThat(sessionEccStage.getMetadata().isAutoRenewalEnabled()).isFalse(); var sessionRsaStage = new Session("acme://ssl.com/staging/rsa"); assertThat(sessionRsaStage.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThatNoException().isThrownBy(() -> sessionRsaStage.resourceUrl(Resource.NEW_ACCOUNT)); - assertThat(sessionRsaStage.getMetadata().isExternalAccountRequired()).isFalse(); + assertThat(sessionRsaStage.getMetadata().isExternalAccountRequired()).isTrue(); assertThat(sessionRsaStage.getMetadata().isAutoRenewalEnabled()).isFalse(); + + // If these tests fail, the metadata have been fixed on server side. Then remove + // the patch at ZeroSSLAcmeProvider, and update the documentation. + var sessionEABCheck = new Session("https://acme.ssl.com/sslcom-dv-ecc"); + assertThat(sessionEABCheck.getMetadata().isExternalAccountRequired()).isFalse(); + var sessionEABCheckStage = new Session("https://acme-try.ssl.com/sslcom-dv-ecc"); + assertThat(sessionEABCheckStage.getMetadata().isExternalAccountRequired()).isFalse(); } /** diff --git a/src/doc/docs/ca/sslcom.md b/src/doc/docs/ca/sslcom.md index 0f49ad27..7fcd537d 100644 --- a/src/doc/docs/ca/sslcom.md +++ b/src/doc/docs/ca/sslcom.md @@ -13,7 +13,7 @@ Available since acme4j 3.2.0 ## Note -* This CA requires [External Account Binding (EAB)](../usage/account.md#external-account-binding) for account creation. However `Metadata.isExternalAccountRequired()` returns `false` due to an error in the CA's directory resource. +* This CA requires [External Account Binding (EAB)](../usage/account.md#external-account-binding) for account creation. However, the CA's directory resource returns `externalAccountRequired` as `false`, which is incorrect. If you use one of the `acme:` URIs above, _acme4j_ will patch the metadata transparently. If you directly connect to SSL.com via `https:` URI though, `Metadata.isExternalAccountRequired()` could return a wrong value. (As of February 2024) ## Disclaimer