Add a GenericTokenChallenge. Remove boilerplate code.

pull/17/merge
Richard Körber 2015-12-24 16:29:35 +01:00
parent ade0207d6d
commit 9b458fb2b6
6 changed files with 120 additions and 124 deletions

View File

@ -18,15 +18,13 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import org.jose4j.base64url.Base64Url; import org.jose4j.base64url.Base64Url;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.util.ClaimBuilder;
/** /**
* Implements the {@code dns-01} challenge. * Implements the {@code dns-01} challenge.
* *
* @author Richard "Shred" Körber * @author Richard "Shred" Körber
*/ */
public class DnsChallenge extends GenericChallenge { public class DnsChallenge extends GenericTokenChallenge {
private static final long serialVersionUID = 6964687027713533075L; private static final long serialVersionUID = 6964687027713533075L;
/** /**
@ -34,27 +32,13 @@ public class DnsChallenge extends GenericChallenge {
*/ */
public static final String TYPE = "dns-01"; 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 * Returns the digest string to be set in the domain's {@code _acme-challenge} TXT
* record. * record.
*/ */
public String getDigest() { public String getDigest() {
assertIsAuthorized();
try { try {
MessageDigest md = MessageDigest.getInstance("SHA-256"); MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(getAuthorization().getBytes("UTF-8")); md.update(getAuthorization().getBytes("UTF-8"));
@ -66,32 +50,9 @@ public class DnsChallenge extends GenericChallenge {
} }
} }
@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 @Override
protected boolean acceptable(String type) { protected boolean acceptable(String type) {
return TYPE.equals(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;
}
} }

View File

@ -50,8 +50,6 @@ public class GenericChallenge implements Challenge {
protected static final String KEY_STATUS = "status"; protected static final String KEY_STATUS = "status";
protected static final String KEY_URI = "uri"; protected static final String KEY_URI = "uri";
protected static final String KEY_VALIDATED = "validated"; 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<String, Object> data = new HashMap<>(); private transient Map<String, Object> data = new HashMap<>();

View File

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

View File

@ -13,16 +13,13 @@
*/ */
package org.shredzone.acme4j.challenge; 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. * Implements the {@code http-01} challenge.
* *
* @author Richard "Shred" Körber * @author Richard "Shred" Körber
*/ */
public class HttpChallenge extends GenericChallenge { public class HttpChallenge extends GenericTokenChallenge {
private static final long serialVersionUID = 3322211185872544605L; private static final long serialVersionUID = 3322211185872544605L;
/** /**
@ -30,27 +27,12 @@ public class HttpChallenge extends GenericChallenge {
*/ */
public static final String TYPE = "http-01"; 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. * Returns the token to be used for this challenge.
*/ */
@Override
public String getToken() { public String getToken() {
return get(KEY_TOKEN); return super.getToken();
} }
/** /**
@ -60,22 +42,9 @@ public class HttpChallenge extends GenericChallenge {
* or ASCII encoded). There must not be any other leading or trailing characters * or ASCII encoded). There must not be any other leading or trailing characters
* (like white-spaces or line breaks). Otherwise the challenge will fail. * (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 @Override
public void respond(ClaimBuilder cb) { public String getAuthorization() {
if (authorization == null) { return super.getAuthorization();
throw new IllegalStateException("Challenge is not authorized yet");
}
super.respond(cb);
cb.put(KEY_TOKEN, getToken());
cb.put(KEY_KEY_AUTHORIZATION, getAuthorization());
} }
@Override @Override

View File

@ -40,6 +40,9 @@ import org.shredzone.acme4j.util.ValidationBuilder;
public class ProofOfPossessionChallenge extends GenericChallenge { public class ProofOfPossessionChallenge extends GenericChallenge {
private static final long serialVersionUID = 6212440828380185335L; private static final long serialVersionUID = 6212440828380185335L;
protected static final String KEY_CERTS = "certs";
protected static final String KEY_AUTHORIZATION = "authorization";
/** /**
* Challenge type name: {@value} * Challenge type name: {@value}
*/ */
@ -96,7 +99,7 @@ public class ProofOfPossessionChallenge extends GenericChallenge {
public void unmarshall(Map<String, Object> map) { public void unmarshall(Map<String, Object> map) {
super.unmarshall(map); super.unmarshall(map);
List<String> certData = get("certs"); List<String> certData = get(KEY_CERTS);
if (certData != null) { if (certData != null) {
try { try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
@ -123,7 +126,7 @@ public class ProofOfPossessionChallenge extends GenericChallenge {
super.respond(cb); super.respond(cb);
try { try {
cb.put("authorization", JsonUtil.parseJson(validation)); cb.put(KEY_AUTHORIZATION, JsonUtil.parseJson(validation));
} catch (JoseException ex) { } catch (JoseException ex) {
// should not happen, as the JSON is prevalidated in the setter // should not happen, as the JSON is prevalidated in the setter
throw new IllegalStateException("validation: invalid JSON", ex); throw new IllegalStateException("validation: invalid JSON", ex);

View File

@ -17,16 +17,14 @@ import java.io.UnsupportedEncodingException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import org.jose4j.base64url.Base64Url;
import org.shredzone.acme4j.Account; import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.util.ClaimBuilder;
/** /**
* Implements the {@code tls-sni-01} challenge. * Implements the {@code tls-sni-01} challenge.
* *
* @author Richard "Shred" Körber * @author Richard "Shred" Körber
*/ */
public class TlsSniChallenge extends GenericChallenge { public class TlsSniChallenge extends GenericTokenChallenge {
private static final long serialVersionUID = 7370329525205430573L; private static final long serialVersionUID = 7370329525205430573L;
private static final char[] HEX = "0123456789abcdef".toCharArray(); private static final char[] HEX = "0123456789abcdef".toCharArray();
@ -35,42 +33,21 @@ public class TlsSniChallenge extends GenericChallenge {
*/ */
public static final String TYPE = "tls-sni-01"; public static final String TYPE = "tls-sni-01";
private String authorization = null; private String subject;
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";
}
/** /**
* Return the subject to generate a self-signed certificate for. * Return the subject to generate a self-signed certificate for.
*/ */
public String getSubject() { public String getSubject() {
if (authorization == null) { assertIsAuthorized();
throw new IllegalStateException("Challenge is not authorized yet");
}
return subject; return subject;
} }
@Override @Override
public void respond(ClaimBuilder cb) { public void authorize(Account account) {
super.respond(cb); super.authorize(account);
cb.put(KEY_TOKEN, getToken()); String hash = computeHash(getAuthorization());
cb.put(KEY_KEY_AUTHORIZATION, getAuthorization()); subject = hash.substring(0, 32) + '.' + hash.substring(32) + ".acme.invalid";
} }
@Override @Override
@ -103,16 +80,4 @@ public class TlsSniChallenge extends GenericChallenge {
} }
} }
private String getToken() {
return get(KEY_TOKEN);
}
private String getAuthorization() {
if (authorization == null) {
throw new IllegalStateException("Challenge is not authorized yet");
}
return authorization;
}
} }