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