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.
pull/168/head
Richard Körber 2024-02-26 18:22:07 +01:00
parent 081e53f137
commit 908e11b152
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
4 changed files with 48 additions and 5 deletions

View File

@ -16,10 +16,14 @@ package org.shredzone.acme4j.provider.sslcom;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URL; 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.exception.AcmeProtocolException;
import org.shredzone.acme4j.provider.AbstractAcmeProvider; import org.shredzone.acme4j.provider.AbstractAcmeProvider;
import org.shredzone.acme4j.provider.AcmeProvider; import org.shredzone.acme4j.provider.AcmeProvider;
import org.shredzone.acme4j.toolbox.JSON;
/** /**
* An {@link AcmeProvider} for <em>SSL.com</em>. * An {@link AcmeProvider} for <em>SSL.com</em>.
@ -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<String, Object>) meta);
metaMap.remove("externalAccountRequired");
metaMap.put("externalAccountRequired", true);
}
return JSON.fromMap(directory);
}
} }

View File

@ -118,6 +118,21 @@ public final class JSON implements Serializable {
} }
} }
/**
* Creates a JSON object from a map.
* <p>
* 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<String, Object> data) {
return JSON.parse(JsonUtil.toJson(data));
}
/** /**
* Returns a {@link JSON} of an empty document. * Returns a {@link JSON} of an empty document.
* *

View File

@ -74,26 +74,33 @@ public class ProviderIT {
var sessionEcc = new Session("acme://ssl.com/ecc"); var sessionEcc = new Session("acme://ssl.com/ecc");
assertThat(sessionEcc.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThat(sessionEcc.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com"));
assertThatNoException().isThrownBy(() -> sessionEcc.resourceUrl(Resource.NEW_ACCOUNT)); assertThatNoException().isThrownBy(() -> sessionEcc.resourceUrl(Resource.NEW_ACCOUNT));
assertThat(sessionEcc.getMetadata().isExternalAccountRequired()).isFalse(); assertThat(sessionEcc.getMetadata().isExternalAccountRequired()).isTrue();
assertThat(sessionEcc.getMetadata().isAutoRenewalEnabled()).isFalse(); assertThat(sessionEcc.getMetadata().isAutoRenewalEnabled()).isFalse();
var sessionRsa = new Session("acme://ssl.com/rsa"); var sessionRsa = new Session("acme://ssl.com/rsa");
assertThat(sessionRsa.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThat(sessionRsa.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com"));
assertThatNoException().isThrownBy(() -> sessionRsa.resourceUrl(Resource.NEW_ACCOUNT)); assertThatNoException().isThrownBy(() -> sessionRsa.resourceUrl(Resource.NEW_ACCOUNT));
assertThat(sessionRsa.getMetadata().isExternalAccountRequired()).isFalse(); assertThat(sessionRsa.getMetadata().isExternalAccountRequired()).isTrue();
assertThat(sessionRsa.getMetadata().isAutoRenewalEnabled()).isFalse(); assertThat(sessionRsa.getMetadata().isAutoRenewalEnabled()).isFalse();
var sessionEccStage = new Session("acme://ssl.com/staging/ecc"); var sessionEccStage = new Session("acme://ssl.com/staging/ecc");
assertThat(sessionEccStage.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThat(sessionEccStage.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com"));
assertThatNoException().isThrownBy(() -> sessionEccStage.resourceUrl(Resource.NEW_ACCOUNT)); assertThatNoException().isThrownBy(() -> sessionEccStage.resourceUrl(Resource.NEW_ACCOUNT));
assertThat(sessionEccStage.getMetadata().isExternalAccountRequired()).isFalse(); assertThat(sessionEccStage.getMetadata().isExternalAccountRequired()).isTrue();
assertThat(sessionEccStage.getMetadata().isAutoRenewalEnabled()).isFalse(); assertThat(sessionEccStage.getMetadata().isAutoRenewalEnabled()).isFalse();
var sessionRsaStage = new Session("acme://ssl.com/staging/rsa"); var sessionRsaStage = new Session("acme://ssl.com/staging/rsa");
assertThat(sessionRsaStage.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com")); assertThat(sessionRsaStage.getMetadata().getWebsite()).hasValue(new URL("https://www.ssl.com"));
assertThatNoException().isThrownBy(() -> sessionRsaStage.resourceUrl(Resource.NEW_ACCOUNT)); assertThatNoException().isThrownBy(() -> sessionRsaStage.resourceUrl(Resource.NEW_ACCOUNT));
assertThat(sessionRsaStage.getMetadata().isExternalAccountRequired()).isFalse(); assertThat(sessionRsaStage.getMetadata().isExternalAccountRequired()).isTrue();
assertThat(sessionRsaStage.getMetadata().isAutoRenewalEnabled()).isFalse(); 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();
} }
/** /**

View File

@ -13,7 +13,7 @@ Available since acme4j 3.2.0
## Note ## 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 ## Disclaimer