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(); + +}