Permit to globally register additional challenge types

pull/129/head
Richard Körber 2021-07-03 10:04:27 +02:00
parent 24af42b6b9
commit 2f2e59fd36
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
5 changed files with 124 additions and 9 deletions

View File

@ -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,

View File

@ -56,14 +56,30 @@ public class TokenChallenge extends Challenge {
}
/**
* Returns the authorization string.
* Computes the key authorization for the given token.
* <p>
* 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.
* <p>
* 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());
}
}

View File

@ -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<String, BiFunction<Login, JSON, Challenge>> CHALLENGES = challengeMap();
private static final Map<String, ChallengeProvider> CHALLENGES = challengeMap();
@Override
public Connection connect(URI serverUri) {
@ -81,13 +81,35 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
}
}
private static Map<String, BiFunction<Login, JSON, Challenge>> challengeMap() {
Map<String, BiFunction<Login, JSON, Challenge>> map = new HashMap<>();
private static Map<String, ChallengeProvider> challengeMap() {
Map<String, ChallengeProvider> 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<Login, JSON, Challenge> 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")) {

View File

@ -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);
}

View File

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