diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/DnsChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/DnsChallenge.java index 63aea1c2..e761e487 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/DnsChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/DnsChallenge.java @@ -18,15 +18,13 @@ import java.security.NoSuchAlgorithmException; import org.jose4j.base64url.Base64Url; -import org.shredzone.acme4j.Account; -import org.shredzone.acme4j.util.ClaimBuilder; /** * Implements the {@code dns-01} challenge. * * @author Richard "Shred" Körber */ -public class DnsChallenge extends GenericChallenge { +public class DnsChallenge extends GenericTokenChallenge { private static final long serialVersionUID = 6964687027713533075L; /** @@ -34,27 +32,13 @@ public class DnsChallenge extends GenericChallenge { */ public static final String TYPE = "dns-01"; - private String authorization = null; - - /** - * Authorizes the {@link Challenge} by signing it with an {@link Account}. - * - * @param account - * {@link Account} to sign the challenge with - */ - public void authorize(Account account) { - if (account == null) { - throw new NullPointerException("account must not be null"); - } - - authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic())); - } - /** * Returns the digest string to be set in the domain's {@code _acme-challenge} TXT * record. */ public String getDigest() { + assertIsAuthorized(); + try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(getAuthorization().getBytes("UTF-8")); @@ -66,32 +50,9 @@ public String getDigest() { } } - @Override - public void respond(ClaimBuilder cb) { - if (authorization == null) { - throw new IllegalStateException("Challenge is not authorized yet"); - } - - super.respond(cb); - cb.put(KEY_TOKEN, getToken()); - cb.put(KEY_KEY_AUTHORIZATION, getAuthorization()); - } - @Override protected boolean acceptable(String type) { return TYPE.equals(type); } - private String getToken() { - return get(KEY_TOKEN); - } - - private String getAuthorization() { - if (authorization == null) { - throw new IllegalStateException("Challenge is not authorized yet"); - } - - return authorization; - } - } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java index 575c25d0..9b1b614f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericChallenge.java @@ -50,8 +50,6 @@ public class GenericChallenge implements Challenge { protected static final String KEY_STATUS = "status"; protected static final String KEY_URI = "uri"; protected static final String KEY_VALIDATED = "validated"; - protected static final String KEY_TOKEN = "token"; - protected static final String KEY_KEY_AUTHORIZATION = "keyAuthorization"; private transient Map data = new HashMap<>(); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericTokenChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericTokenChallenge.java new file mode 100644 index 00000000..d6073cda --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/GenericTokenChallenge.java @@ -0,0 +1,100 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 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.challenge; + +import org.jose4j.base64url.Base64Url; +import org.shredzone.acme4j.Account; +import org.shredzone.acme4j.util.ClaimBuilder; + +/** + * An extension of {@link GenericChallenge} that handles challenges with a {@code token} + * and {@code keyAuthorization}. + * + * @author Richard "Shred" Körber + */ +public class GenericTokenChallenge extends GenericChallenge { + private static final long serialVersionUID = 1634133407432681800L; + + protected static final String KEY_TOKEN = "token"; + protected static final String KEY_KEY_AUTHORIZATION = "keyAuthorization"; + + private String authorization; + + /** + * Authorizes the {@link Challenge} by signing it with an {@link Account}. + * + * @param account + * {@link Account} to sign the challenge with + */ + public void authorize(Account account) { + if (account == null) { + throw new NullPointerException("account must not be null"); + } + + authorization = computeAuthorization(account); + } + + @Override + public void respond(ClaimBuilder cb) { + assertIsAuthorized(); + + super.respond(cb); + cb.put(KEY_TOKEN, getToken()); + cb.put(KEY_KEY_AUTHORIZATION, getAuthorization()); + } + + /** + * Asserts that the challenge was authorized. + * + * @throws IllegalStateException + * if {@link #authorize(Account)} was not invoked. + */ + protected void assertIsAuthorized() { + if (authorization == null) { + throw new IllegalStateException("Challenge is not authorized yet"); + } + } + + /** + * Gets the token. + */ + protected String getToken() { + return get(KEY_TOKEN); + } + + /** + * Gets the authorization after {@link #authorize(Account)} was invoked. + */ + protected String getAuthorization() { + assertIsAuthorized(); + return authorization; + } + + /** + * Computes the authorization string. + *

+ * The default is {@code token + '.' + base64url(jwkThumbprint)}. Subclasses may + * override this method if a different algorithm is used. + * + * @param account + * {@link Account} to authorize with + * @return Authorization string + */ + protected String computeAuthorization(Account account) { + return getToken() + + '.' + + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic())); + } + +} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/HttpChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/HttpChallenge.java index e4d38e48..f53f7069 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/HttpChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/HttpChallenge.java @@ -13,16 +13,13 @@ */ package org.shredzone.acme4j.challenge; -import org.jose4j.base64url.Base64Url; -import org.shredzone.acme4j.Account; -import org.shredzone.acme4j.util.ClaimBuilder; /** * Implements the {@code http-01} challenge. * * @author Richard "Shred" Körber */ -public class HttpChallenge extends GenericChallenge { +public class HttpChallenge extends GenericTokenChallenge { private static final long serialVersionUID = 3322211185872544605L; /** @@ -30,27 +27,12 @@ public class HttpChallenge extends GenericChallenge { */ public static final String TYPE = "http-01"; - private String authorization = null; - - /** - * Authorizes the {@link Challenge} by signing it with an {@link Account}. - * - * @param account - * {@link Account} to sign the challenge with - */ - public void authorize(Account account) { - if (account == null) { - throw new NullPointerException("account must not be null"); - } - - authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic())); - } - /** * Returns the token to be used for this challenge. */ + @Override public String getToken() { - return get(KEY_TOKEN); + return super.getToken(); } /** @@ -60,22 +42,9 @@ public String getToken() { * or ASCII encoded). There must not be any other leading or trailing characters * (like white-spaces or line breaks). Otherwise the challenge will fail. */ - public String getAuthorization() { - if (authorization == null) { - throw new IllegalStateException("Challenge is not authorized yet"); - } - return authorization; - } - @Override - public void respond(ClaimBuilder cb) { - if (authorization == null) { - throw new IllegalStateException("Challenge is not authorized yet"); - } - - super.respond(cb); - cb.put(KEY_TOKEN, getToken()); - cb.put(KEY_KEY_AUTHORIZATION, getAuthorization()); + public String getAuthorization() { + return super.getAuthorization(); } @Override diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/ProofOfPossessionChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/ProofOfPossessionChallenge.java index e48f90c4..060d3a60 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/ProofOfPossessionChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/ProofOfPossessionChallenge.java @@ -40,6 +40,9 @@ public class ProofOfPossessionChallenge extends GenericChallenge { private static final long serialVersionUID = 6212440828380185335L; + protected static final String KEY_CERTS = "certs"; + protected static final String KEY_AUTHORIZATION = "authorization"; + /** * Challenge type name: {@value} */ @@ -96,7 +99,7 @@ public void importValidation(String validation) { public void unmarshall(Map map) { super.unmarshall(map); - List certData = get("certs"); + List certData = get(KEY_CERTS); if (certData != null) { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); @@ -123,7 +126,7 @@ public void respond(ClaimBuilder cb) { super.respond(cb); try { - cb.put("authorization", JsonUtil.parseJson(validation)); + cb.put(KEY_AUTHORIZATION, JsonUtil.parseJson(validation)); } catch (JoseException ex) { // should not happen, as the JSON is prevalidated in the setter throw new IllegalStateException("validation: invalid JSON", ex); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSniChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSniChallenge.java index dddaa631..97a01246 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSniChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSniChallenge.java @@ -17,16 +17,14 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import org.jose4j.base64url.Base64Url; import org.shredzone.acme4j.Account; -import org.shredzone.acme4j.util.ClaimBuilder; /** * Implements the {@code tls-sni-01} challenge. * * @author Richard "Shred" Körber */ -public class TlsSniChallenge extends GenericChallenge { +public class TlsSniChallenge extends GenericTokenChallenge { private static final long serialVersionUID = 7370329525205430573L; private static final char[] HEX = "0123456789abcdef".toCharArray(); @@ -35,42 +33,21 @@ public class TlsSniChallenge extends GenericChallenge { */ public static final String TYPE = "tls-sni-01"; - private String authorization = null; - private String subject = null; - - /** - * Authorizes the {@link Challenge} by signing it with an {@link Account}. - * - * @param account - * {@link Account} to sign the challenge with - */ - public void authorize(Account account) { - if (account == null) { - throw new NullPointerException("account must not be null"); - } - - authorization = getToken() + '.' + Base64Url.encode(jwkThumbprint(account.getKeyPair().getPublic())); - - String hash = computeHash(authorization); - subject = hash.substring(0, 32) + '.' + hash.substring(32) + ".acme.invalid"; - } + private String subject; /** * Return the subject to generate a self-signed certificate for. */ public String getSubject() { - if (authorization == null) { - throw new IllegalStateException("Challenge is not authorized yet"); - } + assertIsAuthorized(); return subject; } - @Override - public void respond(ClaimBuilder cb) { - super.respond(cb); - cb.put(KEY_TOKEN, getToken()); - cb.put(KEY_KEY_AUTHORIZATION, getAuthorization()); + public void authorize(Account account) { + super.authorize(account); + String hash = computeHash(getAuthorization()); + subject = hash.substring(0, 32) + '.' + hash.substring(32) + ".acme.invalid"; } @Override @@ -103,16 +80,4 @@ private String computeHash(String z) { } } - private String getToken() { - return get(KEY_TOKEN); - } - - private String getAuthorization() { - if (authorization == null) { - throw new IllegalStateException("Challenge is not authorized yet"); - } - - return authorization; - } - }