diff --git a/acme4j-client/src/main/java/module-info.java b/acme4j-client/src/main/java/module-info.java
index 3dfe4708..761b249f 100644
--- a/acme4j-client/src/main/java/module-info.java
+++ b/acme4j-client/src/main/java/module-info.java
@@ -25,6 +25,7 @@ module org.shredzone.acme4j {
exports org.shredzone.acme4j.toolbox;
uses org.shredzone.acme4j.provider.AcmeProvider;
+ uses org.shredzone.acme4j.provider.ChallengeProvider;
provides org.shredzone.acme4j.provider.AcmeProvider
with org.shredzone.acme4j.provider.GenericAcmeProvider,
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java
index 0dd74092..d94a64bb 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java
@@ -56,14 +56,30 @@ public class TokenChallenge extends Challenge {
}
/**
- * Returns the authorization string.
+ * Computes the key authorization for the given token.
*
* The default is {@code token + '.' + base64url(jwkThumbprint)}. Subclasses may
* override this method if a different algorithm is used.
+ *
+ * @param token
+ * Token to be used
+ * @return Key Authorization string for that token
+ * @since 2.12
+ */
+ protected String keyAuthorizationFor(String token) {
+ PublicKey pk = getLogin().getKeyPair().getPublic();
+ return token + '.' + base64UrlEncode(JoseUtils.thumbprint(pk));
+ }
+
+ /**
+ * Returns the authorization string.
+ *
+ * The default uses {@link #keyAuthorizationFor(String)} to compute the key
+ * authorization of {@link #getToken()}. Subclasses may override this method if a
+ * different algorithm is used.
*/
public String getAuthorization() {
- PublicKey pk = getLogin().getKeyPair().getPublic();
- return getToken() + '.' + base64UrlEncode(JoseUtils.thumbprint(pk));
+ return keyAuthorizationFor(getToken());
}
}
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java
index 0ee2053c..3721b2dc 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java
@@ -20,7 +20,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
-import java.util.function.BiFunction;
+import java.util.ServiceLoader;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.Session;
@@ -44,7 +44,7 @@ import org.shredzone.acme4j.toolbox.JSON;
*/
public abstract class AbstractAcmeProvider implements AcmeProvider {
- private static final Map> CHALLENGES = challengeMap();
+ private static final Map CHALLENGES = challengeMap();
@Override
public Connection connect(URI serverUri) {
@@ -81,13 +81,35 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
}
}
- private static Map> challengeMap() {
- Map> map = new HashMap<>();
+ private static Map challengeMap() {
+ Map map = new HashMap<>();
map.put(Dns01Challenge.TYPE, Dns01Challenge::new);
map.put(Http01Challenge.TYPE, Http01Challenge::new);
map.put(TlsAlpn01Challenge.TYPE, TlsAlpn01Challenge::new);
+ for (ChallengeProvider provider : ServiceLoader.load(ChallengeProvider.class)) {
+ ChallengeType typeAnno = provider.getClass().getAnnotation(ChallengeType.class);
+ if (typeAnno == null) {
+ throw new IllegalStateException("ChallengeProvider "
+ + provider.getClass().getName()
+ + " has no @ChallengeType annotation");
+ }
+ String type = typeAnno.value();
+ if (type == null || type.trim().isEmpty()) {
+ throw new IllegalStateException("ChallengeProvider "
+ + provider.getClass().getName()
+ + ": type must not be null or empty");
+ }
+ if (map.containsKey(type)) {
+ throw new IllegalStateException("ChallengeProvider "
+ + provider.getClass().getName()
+ + ": there is already a provider for challenge type "
+ + type);
+ }
+ map.put(type, provider);
+ }
+
return Collections.unmodifiableMap(map);
}
@@ -107,9 +129,9 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
String type = data.get("type").asString();
- BiFunction constructor = CHALLENGES.get(type);
+ ChallengeProvider constructor = CHALLENGES.get(type);
if (constructor != null) {
- return constructor.apply(login, data);
+ return constructor.create(login, data);
}
if (data.contains("token")) {
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeProvider.java
new file mode 100644
index 00000000..5769f349
--- /dev/null
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeProvider.java
@@ -0,0 +1,39 @@
+/*
+ * acme4j - Java ACME client
+ *
+ * Copyright (C) 2021 Richard "Shred" Körber
+ * http://acme4j.shredzone.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ */
+package org.shredzone.acme4j.provider;
+
+import org.shredzone.acme4j.Login;
+import org.shredzone.acme4j.challenge.Challenge;
+import org.shredzone.acme4j.toolbox.JSON;
+
+/**
+ * A provider that creates a Challenge from a matching JSON.
+ *
+ * @since 2.12
+ */
+@FunctionalInterface
+public interface ChallengeProvider {
+
+ /**
+ * Creates a Challenge.
+ *
+ * @param login
+ * {@link Login} of the user's account
+ * @param data
+ * {@link JSON} of the challenge as sent by the CA
+ * @return Created and initialized {@link Challenge}. It must match the JSON type.
+ */
+ Challenge create(Login login, JSON data);
+
+}
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeType.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeType.java
new file mode 100644
index 00000000..7f8b69c1
--- /dev/null
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/ChallengeType.java
@@ -0,0 +1,37 @@
+/*
+ * acme4j - Java ACME client
+ *
+ * Copyright (C) 2021 Richard "Shred" Körber
+ * http://acme4j.shredzone.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ */
+package org.shredzone.acme4j.provider;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates the challenge type that is generated by the {@link ChallengeProvider}.
+ *
+ * @since 2.12
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ChallengeType {
+
+ /**
+ * Challenge type.
+ */
+ String value();
+
+}