mirror of https://github.com/shred/acme4j
Implement new order finalization
Replaces the "CSR first" new-order flow, see ietf-wg-acme/acme#342pull/55/head
parent
a6ec6d04d2
commit
e0673c93bd
|
@ -22,6 +22,7 @@ import java.security.KeyPair;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -141,21 +142,36 @@ public class Account extends AcmeResource {
|
||||||
/**
|
/**
|
||||||
* Orders a certificate. The certificate will be associated with this account.
|
* Orders a certificate. The certificate will be associated with this account.
|
||||||
*
|
*
|
||||||
* @param csr
|
* @param domains
|
||||||
* CSR containing the parameters for the certificate being requested
|
* Domains of the certificate to be ordered. May contain wildcard domains.
|
||||||
|
* IDN names are accepted and will be ACE encoded automatically.
|
||||||
* @param notBefore
|
* @param notBefore
|
||||||
* Requested "notBefore" date in the certificate, or {@code null}
|
* Requested "notBefore" date in the certificate, or {@code null}
|
||||||
* @param notAfter
|
* @param notAfter
|
||||||
* Requested "notAfter" date in the certificate, or {@code null}
|
* Requested "notAfter" date in the certificate, or {@code null}
|
||||||
* @return {@link Order} object for this domain
|
* @return {@link Order} object for this domain
|
||||||
*/
|
*/
|
||||||
public Order orderCertificate(byte[] csr, Instant notBefore, Instant notAfter) throws AcmeException {
|
public Order orderCertificate(Collection<String> domains, Instant notBefore, Instant notAfter)
|
||||||
Objects.requireNonNull(csr, "csr");
|
throws AcmeException {
|
||||||
|
Objects.requireNonNull(domains, "domains");
|
||||||
|
if (domains.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Cannot order an empty collection of domains");
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[] identifiers = new Object[domains.size()];
|
||||||
|
Iterator<String> di = domains.iterator();
|
||||||
|
for (int ix = 0; ix < identifiers.length; ix++) {
|
||||||
|
identifiers[ix] = new JSONBuilder()
|
||||||
|
.put("type", "dns")
|
||||||
|
.put("value", toAce(di.next()))
|
||||||
|
.toMap();
|
||||||
|
}
|
||||||
|
|
||||||
LOG.debug("orderCertificate");
|
LOG.debug("orderCertificate");
|
||||||
try (Connection conn = getSession().provider().connect()) {
|
try (Connection conn = getSession().provider().connect()) {
|
||||||
JSONBuilder claims = new JSONBuilder();
|
JSONBuilder claims = new JSONBuilder();
|
||||||
claims.putBase64("csr", csr);
|
claims.array("identifiers", identifiers);
|
||||||
|
|
||||||
if (notBefore != null) {
|
if (notBefore != null) {
|
||||||
claims.put("notBefore", notBefore);
|
claims.put("notBefore", notBefore);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.shredzone.acme4j.connector.Connection;
|
||||||
import org.shredzone.acme4j.exception.AcmeException;
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
|
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
|
||||||
import org.shredzone.acme4j.toolbox.JSON;
|
import org.shredzone.acme4j.toolbox.JSON;
|
||||||
|
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -36,11 +37,12 @@ public class Order extends AcmeResource {
|
||||||
|
|
||||||
private Status status;
|
private Status status;
|
||||||
private Instant expires;
|
private Instant expires;
|
||||||
private byte[] csr;
|
private List<String> identifiers;
|
||||||
private Instant notBefore;
|
private Instant notBefore;
|
||||||
private Instant notAfter;
|
private Instant notAfter;
|
||||||
private Problem error;
|
private Problem error;
|
||||||
private List<URL> authorizations;
|
private List<URL> authorizations;
|
||||||
|
private URL finalizeUrl;
|
||||||
private Certificate certificate;
|
private Certificate certificate;
|
||||||
private boolean loaded = false;
|
private boolean loaded = false;
|
||||||
|
|
||||||
|
@ -87,11 +89,10 @@ public class Order extends AcmeResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the CSR that was used for the order.
|
* Gets the list of domain names to be ordered.
|
||||||
*/
|
*/
|
||||||
public byte[] getCsr() {
|
public List<String> getDomains() {
|
||||||
load();
|
return identifiers;
|
||||||
return csr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -121,6 +122,42 @@ public class Order extends AcmeResource {
|
||||||
.collect(toList());
|
.collect(toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the location {@link URL} of where to send the finalization call to.
|
||||||
|
* <p>
|
||||||
|
* For internal purposes. Use {@link #execute(byte[])} to finalize an order.
|
||||||
|
*/
|
||||||
|
public URL getFinalizeLocation() {
|
||||||
|
load();
|
||||||
|
return finalizeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalizes the order, by providing a CSR.
|
||||||
|
* <p>
|
||||||
|
* After a successful finalization, the certificate is available at
|
||||||
|
* {@link #getCertificate()}.
|
||||||
|
* <p>
|
||||||
|
* Even though the ACME protocol uses the term "finalize an order", this method is
|
||||||
|
* called {@link #execute(byte[])} to avoid confusion with the general
|
||||||
|
* {@link Object#finalize()} method.
|
||||||
|
*
|
||||||
|
* @param csr
|
||||||
|
* CSR containing the parameters for the certificate being requested, in
|
||||||
|
* DER format
|
||||||
|
*/
|
||||||
|
public void execute(byte[] csr) throws AcmeException {
|
||||||
|
LOG.debug("finalize");
|
||||||
|
try (Connection conn = getSession().provider().connect()) {
|
||||||
|
JSONBuilder claims = new JSONBuilder();
|
||||||
|
claims.putBase64("csr", csr);
|
||||||
|
|
||||||
|
conn.sendSignedRequest(getFinalizeLocation(), claims, getSession());
|
||||||
|
conn.accept(HttpURLConnection.HTTP_OK);
|
||||||
|
}
|
||||||
|
loaded = false; // invalidate this object
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the {@link Certificate} if it is available. {@code null} otherwise.
|
* Gets the {@link Certificate} if it is available. {@code null} otherwise.
|
||||||
*/
|
*/
|
||||||
|
@ -165,15 +202,20 @@ public class Order extends AcmeResource {
|
||||||
public void unmarshal(JSON json) {
|
public void unmarshal(JSON json) {
|
||||||
this.status = json.get("status").asStatusOrElse(Status.UNKNOWN);
|
this.status = json.get("status").asStatusOrElse(Status.UNKNOWN);
|
||||||
this.expires = json.get("expires").asInstant();
|
this.expires = json.get("expires").asInstant();
|
||||||
this.csr = json.get("csr").asBinary();
|
|
||||||
this.notBefore = json.get("notBefore").asInstant();
|
this.notBefore = json.get("notBefore").asInstant();
|
||||||
this.notAfter = json.get("notAfter").asInstant();
|
this.notAfter = json.get("notAfter").asInstant();
|
||||||
|
this.finalizeUrl = json.get("finalizeURL").asURL();
|
||||||
|
|
||||||
URL certUrl = json.get("certificate").asURL();
|
URL certUrl = json.get("certificate").asURL();
|
||||||
certificate = certUrl != null ? Certificate.bind(getSession(), certUrl) : null;
|
certificate = certUrl != null ? Certificate.bind(getSession(), certUrl) : null;
|
||||||
|
|
||||||
this.error = json.get("error").asProblem(getLocation());
|
this.error = json.get("error").asProblem(getLocation());
|
||||||
|
|
||||||
|
this.identifiers = json.get("identifiers").asArray().stream()
|
||||||
|
.map(JSON.Value::asObject)
|
||||||
|
.map(it -> it.get("value").asString())
|
||||||
|
.collect(toList());
|
||||||
|
|
||||||
this.authorizations = json.get("authorizations").asArray().stream()
|
this.authorizations = json.get("authorizations").asArray().stream()
|
||||||
.map(JSON.Value::asURL)
|
.map(JSON.Value::asURL)
|
||||||
.collect(toList());
|
.collect(toList());
|
||||||
|
|
|
@ -190,7 +190,6 @@ public class AccountTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testOrderCertificate() throws Exception {
|
public void testOrderCertificate() throws Exception {
|
||||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
|
||||||
Instant notBefore = parseTimestamp("2016-01-01T00:00:00Z");
|
Instant notBefore = parseTimestamp("2016-01-01T00:00:00Z");
|
||||||
Instant notAfter = parseTimestamp("2016-01-08T00:00:00Z");
|
Instant notAfter = parseTimestamp("2016-01-08T00:00:00Z");
|
||||||
|
|
||||||
|
@ -224,9 +223,11 @@ public class AccountTest {
|
||||||
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||||
|
|
||||||
Account account = new Account(session, locationUrl);
|
Account account = new Account(session, locationUrl);
|
||||||
Order order = account.orderCertificate(csr, notBefore, notAfter);
|
Order order = account.orderCertificate(
|
||||||
|
Arrays.asList("example.com", "www.example.com"),
|
||||||
|
notBefore, notAfter);
|
||||||
|
|
||||||
assertThat(order.getCsr(), is(csr));
|
assertThat(order.getDomains(), containsInAnyOrder("example.com", "www.example.com"));
|
||||||
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:10:00Z")));
|
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:10:00Z")));
|
||||||
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:10:00Z")));
|
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:10:00Z")));
|
||||||
assertThat(order.getExpires(), is(parseTimestamp("2016-01-10T00:00:00Z")));
|
assertThat(order.getExpires(), is(parseTimestamp("2016-01-10T00:00:00Z")));
|
||||||
|
|
|
@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
|
import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
|
||||||
import static org.shredzone.acme4j.toolbox.TestUtils.*;
|
import static org.shredzone.acme4j.toolbox.TestUtils.*;
|
||||||
|
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -28,6 +29,7 @@ import org.junit.Test;
|
||||||
import org.shredzone.acme4j.exception.AcmeException;
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||||
import org.shredzone.acme4j.toolbox.JSON;
|
import org.shredzone.acme4j.toolbox.JSON;
|
||||||
|
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||||
import org.shredzone.acme4j.toolbox.TestUtils;
|
import org.shredzone.acme4j.toolbox.TestUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,14 +38,13 @@ import org.shredzone.acme4j.toolbox.TestUtils;
|
||||||
public class OrderTest {
|
public class OrderTest {
|
||||||
|
|
||||||
private URL locationUrl = url("http://example.com/acme/order/1234");
|
private URL locationUrl = url("http://example.com/acme/order/1234");
|
||||||
|
private URL finalizeUrl = url("https://example.com/acme/acct/1/order/1/finalize");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that order is properly updated.
|
* Test that order is properly updated.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testUpdate() throws Exception {
|
public void testUpdate() throws Exception {
|
||||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
|
||||||
|
|
||||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||||
@Override
|
@Override
|
||||||
public void sendRequest(URL url, Session session) {
|
public void sendRequest(URL url, Session session) {
|
||||||
|
@ -71,10 +72,11 @@ public class OrderTest {
|
||||||
assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z")));
|
assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z")));
|
||||||
assertThat(order.getLocation(), is(locationUrl));
|
assertThat(order.getLocation(), is(locationUrl));
|
||||||
|
|
||||||
|
assertThat(order.getDomains(), containsInAnyOrder("example.com", "www.example.com"));
|
||||||
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:00:00Z")));
|
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:00:00Z")));
|
||||||
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:00:00Z")));
|
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:00:00Z")));
|
||||||
assertThat(order.getCertificate().getLocation(), is(url("https://example.com/acme/cert/1234")));
|
assertThat(order.getCertificate().getLocation(), is(url("https://example.com/acme/cert/1234")));
|
||||||
assertThat(order.getCsr(), is(csr));
|
assertThat(order.getFinalizeLocation(), is(finalizeUrl));
|
||||||
|
|
||||||
assertThat(order.getError(), is(notNullValue()));
|
assertThat(order.getError(), is(notNullValue()));
|
||||||
assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
|
assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
|
||||||
|
@ -135,4 +137,64 @@ public class OrderTest {
|
||||||
provider.close();
|
provider.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that order is properly finalized.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testFinalize() throws Exception {
|
||||||
|
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
||||||
|
|
||||||
|
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||||
|
private boolean isFinalized = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendRequest(URL url, Session session) {
|
||||||
|
assertThat(url, is(locationUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendSignedRequest(URL url, JSONBuilder claims, Session session) {
|
||||||
|
assertThat(url, is(finalizeUrl));
|
||||||
|
assertThat(claims.toString(), sameJSONAs(getJSON("finalizeRequest").toString()));
|
||||||
|
assertThat(session, is(notNullValue()));
|
||||||
|
isFinalized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int accept(int... httpStatus) throws AcmeException {
|
||||||
|
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK));
|
||||||
|
return HttpURLConnection.HTTP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSON readJsonResponse() {
|
||||||
|
return getJSON(isFinalized ? "finalizeResponse" : "updateOrderResponse");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Session session = provider.createSession();
|
||||||
|
|
||||||
|
Order order = new Order(session, locationUrl);
|
||||||
|
order.execute(csr);
|
||||||
|
|
||||||
|
assertThat(order.getStatus(), is(Status.VALID));
|
||||||
|
assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z")));
|
||||||
|
assertThat(order.getLocation(), is(locationUrl));
|
||||||
|
|
||||||
|
assertThat(order.getDomains(), containsInAnyOrder("example.com", "www.example.com"));
|
||||||
|
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:00:00Z")));
|
||||||
|
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:00:00Z")));
|
||||||
|
assertThat(order.getCertificate().getLocation(), is(url("https://example.com/acme/cert/1234")));
|
||||||
|
assertThat(order.getFinalizeLocation(), is(finalizeUrl));
|
||||||
|
|
||||||
|
List<Authorization> auths = order.getAuthorizations();
|
||||||
|
assertThat(auths.size(), is(2));
|
||||||
|
assertThat(auths.stream().map(Authorization::getLocation)::iterator,
|
||||||
|
containsInAnyOrder(
|
||||||
|
url("https://example.com/acme/authz/1234"),
|
||||||
|
url("https://example.com/acme/authz/2345")));
|
||||||
|
|
||||||
|
provider.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"status": "valid",
|
||||||
|
"expires": "2015-03-01T14:09:00Z",
|
||||||
|
"identifiers": [
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "www.example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notBefore": "2016-01-01T00:00:00Z",
|
||||||
|
"notAfter": "2016-01-08T00:00:00Z",
|
||||||
|
"authorizations": [
|
||||||
|
"https://example.com/acme/authz/1234",
|
||||||
|
"https://example.com/acme/authz/2345"
|
||||||
|
],
|
||||||
|
"finalizeURL": "https://example.com/acme/acct/1/order/1/finalize",
|
||||||
|
"certificate": "https://example.com/acme/cert/1234"
|
||||||
|
}
|
|
@ -1,5 +1,14 @@
|
||||||
{
|
{
|
||||||
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",
|
"identifiers": [
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "www.example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
"notBefore": "2016-01-01T00:00:00Z",
|
"notBefore": "2016-01-01T00:00:00Z",
|
||||||
"notAfter": "2016-01-08T00:00:00Z"
|
"notAfter": "2016-01-08T00:00:00Z"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
{
|
{
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"expires": "2016-01-10T00:00:00Z",
|
"expires": "2016-01-10T00:00:00Z",
|
||||||
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",
|
"identifiers": [
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "www.example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
"notBefore": "2016-01-01T00:10:00Z",
|
"notBefore": "2016-01-01T00:10:00Z",
|
||||||
"notAfter": "2016-01-08T00:10:00Z",
|
"notAfter": "2016-01-08T00:10:00Z",
|
||||||
"authorizations": [
|
"authorizations": [
|
||||||
"https://example.com/acme/authz/1234",
|
"https://example.com/acme/authz/1234",
|
||||||
"https://example.com/acme/authz/2345"
|
"https://example.com/acme/authz/2345"
|
||||||
]
|
],
|
||||||
|
"finalizeURL": "https://example.com/acme/acct/1/order/1/finalize"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
{
|
{
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"expires": "2015-03-01T14:09:00Z",
|
"expires": "2015-03-01T14:09:00Z",
|
||||||
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",
|
"identifiers": [
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"value": "www.example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
"notBefore": "2016-01-01T00:00:00Z",
|
"notBefore": "2016-01-01T00:00:00Z",
|
||||||
"notAfter": "2016-01-08T00:00:00Z",
|
"notAfter": "2016-01-08T00:00:00Z",
|
||||||
"authorizations": [
|
"authorizations": [
|
||||||
"https://example.com/acme/authz/1234",
|
"https://example.com/acme/authz/1234",
|
||||||
"https://example.com/acme/authz/2345"
|
"https://example.com/acme/authz/2345"
|
||||||
],
|
],
|
||||||
|
"finalizeURL": "https://example.com/acme/acct/1/order/1/finalize",
|
||||||
"certificate": "https://example.com/acme/cert/1234",
|
"certificate": "https://example.com/acme/cert/1234",
|
||||||
"error": {
|
"error": {
|
||||||
"type": "urn:ietf:params:acme:error:connection",
|
"type": "urn:ietf:params:acme:error:connection",
|
||||||
|
|
|
@ -90,6 +90,14 @@ public class ClientTest {
|
||||||
// Load or create a key pair for the domains. This should not be the userKeyPair!
|
// Load or create a key pair for the domains. This should not be the userKeyPair!
|
||||||
KeyPair domainKeyPair = loadOrCreateDomainKeyPair();
|
KeyPair domainKeyPair = loadOrCreateDomainKeyPair();
|
||||||
|
|
||||||
|
// Order the certificate
|
||||||
|
Order order = acct.orderCertificate(domains, null, null);
|
||||||
|
|
||||||
|
// Perform all required authorizations
|
||||||
|
for (Authorization auth : order.getAuthorizations()) {
|
||||||
|
authorize(auth);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a CSR for all of the domains, and sign it with the domain key pair.
|
// Generate a CSR for all of the domains, and sign it with the domain key pair.
|
||||||
CSRBuilder csrb = new CSRBuilder();
|
CSRBuilder csrb = new CSRBuilder();
|
||||||
csrb.addDomains(domains);
|
csrb.addDomains(domains);
|
||||||
|
@ -100,16 +108,8 @@ public class ClientTest {
|
||||||
csrb.write(out);
|
csrb.write(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Order the certificate
|
|
||||||
Order order = acct.orderCertificate(csrb.getEncoded(), null, null);
|
|
||||||
|
|
||||||
// Perform all required authorizations
|
|
||||||
for (Authorization auth : order.getAuthorizations()) {
|
|
||||||
authorize(auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the certificate
|
// Get the certificate
|
||||||
order.update();
|
order.execute(csrb.getEncoded());
|
||||||
Certificate certificate = order.getCertificate();
|
Certificate certificate = order.getCertificate();
|
||||||
|
|
||||||
LOG.info("Success! The certificate for domains " + domains + " has been generated!");
|
LOG.info("Success! The certificate for domains " + domains + " has been generated!");
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.security.cert.X509Certificate;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.shredzone.acme4j.Account;
|
import org.shredzone.acme4j.Account;
|
||||||
|
@ -134,16 +135,10 @@ public class OrderIT extends PebbleITBase {
|
||||||
|
|
||||||
KeyPair domainKeyPair = createKeyPair();
|
KeyPair domainKeyPair = createKeyPair();
|
||||||
|
|
||||||
CSRBuilder csr = new CSRBuilder();
|
|
||||||
csr.addDomain(domain);
|
|
||||||
csr.sign(domainKeyPair);
|
|
||||||
byte[] encodedCsr = csr.getEncoded();
|
|
||||||
|
|
||||||
Instant notBefore = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
Instant notBefore = Instant.now().truncatedTo(ChronoUnit.MILLIS);
|
||||||
Instant notAfter = notBefore.plus(Duration.ofDays(20L));
|
Instant notAfter = notBefore.plus(Duration.ofDays(20L));
|
||||||
|
|
||||||
Order order = account.orderCertificate(encodedCsr, notBefore, notAfter);
|
Order order = account.orderCertificate(Arrays.asList(domain), notBefore, notAfter);
|
||||||
assertThat(order.getCsr(), is(encodedCsr));
|
|
||||||
assertThat(order.getNotBefore(), is(notBefore));
|
assertThat(order.getNotBefore(), is(notBefore));
|
||||||
assertThat(order.getNotAfter(), is(notAfter));
|
assertThat(order.getNotAfter(), is(notAfter));
|
||||||
assertThat(order.getStatus(), is(Status.PENDING));
|
assertThat(order.getStatus(), is(Status.PENDING));
|
||||||
|
@ -166,7 +161,12 @@ public class OrderIT extends PebbleITBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
order.update();
|
CSRBuilder csr = new CSRBuilder();
|
||||||
|
csr.addDomain(domain);
|
||||||
|
csr.sign(domainKeyPair);
|
||||||
|
byte[] encodedCsr = csr.getEncoded();
|
||||||
|
|
||||||
|
order.execute(encodedCsr);
|
||||||
|
|
||||||
Certificate certificate = order.getCertificate();
|
Certificate certificate = order.getCertificate();
|
||||||
X509Certificate cert = certificate.getCertificate();
|
X509Certificate cert = certificate.getCertificate();
|
||||||
|
|
|
@ -2,45 +2,14 @@
|
||||||
|
|
||||||
Once you have your account set up, you are ready to order certificates.
|
Once you have your account set up, you are ready to order certificates.
|
||||||
|
|
||||||
## Create a Certificate Signing Request (CSR)
|
Use your `Account` object to order the certificate, by using the `orderCertificate()` method. It requires a collection of domain names to be ordered. You can optionally give your desired `notBefore` and `notAfter` dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values.
|
||||||
|
|
||||||
To do so, prepare a PKCS#10 CSR file. A single domain may be set as _Common Name_. Multiple domains must be provided as _Subject Alternative Name_. Other properties (_Organization_, _Organization Unit_ etc.) depend on the CA. Some may require these properties to be set, while others may ignore them when generating the certificate.
|
|
||||||
|
|
||||||
CSR files can be generated with command line tools like `openssl`. Unfortunately the standard Java does not offer classes for that, so you'd have to resort to [Bouncy Castle](http://www.bouncycastle.org/java.html) if you want to create a CSR programmatically. In the `acme4j-utils` module, there is a [`CSRBuilder`](../apidocs/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience. You can also use [`KeyPairUtils`](../apidocs/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain.
|
|
||||||
|
|
||||||
> __Important:__ Do not just use your account key pair as domain key pair, but always generate separate key pairs!
|
|
||||||
|
|
||||||
```java
|
|
||||||
KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption
|
|
||||||
|
|
||||||
CSRBuilder csrb = new CSRBuilder();
|
|
||||||
csrb.addDomain("example.org");
|
|
||||||
csrb.addDomain("www.example.org");
|
|
||||||
csrb.addDomain("m.example.org");
|
|
||||||
csrb.setOrganization("The Example Organization")
|
|
||||||
csrb.sign(domainKeyPair);
|
|
||||||
byte[] csr = csrb.getEncoded();
|
|
||||||
```
|
|
||||||
|
|
||||||
It is a good idea to store the generated CSR somewhere, as you will need it again for renewal:
|
|
||||||
|
|
||||||
```java
|
|
||||||
csrb.write(new FileWriter("example.csr"));
|
|
||||||
```
|
|
||||||
|
|
||||||
The generated certificate will be valid for all of the domains stored in the CSR.
|
|
||||||
|
|
||||||
> __Note:__ The number of domains per certificate may be limited. See your CA's documentation for the limits.
|
|
||||||
|
|
||||||
## Order a Certificate
|
|
||||||
|
|
||||||
The next step is to use your `Account` object to order the certificate, by using the `orderCertificate()` method. You can optionally give your desired `notBefore` and `notAfter` dates for the generated certificate, but it is at the discretion of the CA to use (or ignore) these values.
|
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Account account = ... // your Account object
|
Account account = ... // your Account object
|
||||||
byte[] csr = ... // your CSR (see above)
|
|
||||||
|
|
||||||
Order order = account.orderCertificate(csr, null, null);
|
Order order = account.orderCertificate(
|
||||||
|
Arrays.of("example.org", "www.example.org", "m.example.org"),
|
||||||
|
null, null);
|
||||||
```
|
```
|
||||||
|
|
||||||
The `Order` resource contains a collection of `Authorization`s that can be read from the `getAuthorizations()` method. You must process _all of them_ in order to get the certificate.
|
The `Order` resource contains a collection of `Authorization`s that can be read from the `getAuthorizations()` method. You must process _all of them_ in order to get the certificate.
|
||||||
|
@ -84,6 +53,40 @@ The CA server may start the validation immediately after `trigger()` is invoked,
|
||||||
|
|
||||||
When the challenge status is `VALID`, you have successfully authorized your domain.
|
When the challenge status is `VALID`, you have successfully authorized your domain.
|
||||||
|
|
||||||
|
## Finalize the Order
|
||||||
|
|
||||||
|
After successfully completing all authorizations, the order needs to be finalized by a PKCS#10 CSR file. A single domain may be set as _Common Name_. Multiple domains must be provided as _Subject Alternative Name_. You must provide exactly the domains that you had passed to the `order()` method above, otherwise the finalization will fail. It depends on the CA if other CSR properties (_Organization_, _Organization Unit_ etc.) are accepted. Some may require these properties to be set, while others may ignore them when generating the certificate.
|
||||||
|
|
||||||
|
CSR files can be generated with command line tools like `openssl`. Unfortunately the standard Java does not offer classes for that, so you'd have to resort to [Bouncy Castle](http://www.bouncycastle.org/java.html) if you want to create a CSR programmatically. In the `acme4j-utils` module, there is a [`CSRBuilder`](../apidocs/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience. You can also use [`KeyPairUtils`](../apidocs/org/shredzone/acme4j/util/KeyPairUtils.html) for generating a new key pair for your domain.
|
||||||
|
|
||||||
|
> __Important:__ Do not just use your account key pair as domain key pair, but always generate separate key pairs!
|
||||||
|
|
||||||
|
```java
|
||||||
|
KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption
|
||||||
|
|
||||||
|
CSRBuilder csrb = new CSRBuilder();
|
||||||
|
csrb.addDomain("example.org");
|
||||||
|
csrb.addDomain("www.example.org");
|
||||||
|
csrb.addDomain("m.example.org");
|
||||||
|
csrb.setOrganization("The Example Organization")
|
||||||
|
csrb.sign(domainKeyPair);
|
||||||
|
byte[] csr = csrb.getEncoded();
|
||||||
|
```
|
||||||
|
|
||||||
|
It is a good idea to store the generated CSR somewhere, as you will need it again for renewal:
|
||||||
|
|
||||||
|
```java
|
||||||
|
csrb.write(new FileWriter("example.csr"));
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, finalize the order:
|
||||||
|
|
||||||
|
```java
|
||||||
|
order.execute(csr);
|
||||||
|
```
|
||||||
|
|
||||||
|
> __Note:__ The number of domains per certificate may be limited. See your CA's documentation for the limits.
|
||||||
|
|
||||||
## Wildcard Certificates
|
## Wildcard Certificates
|
||||||
|
|
||||||
You can also generate a wildcard certificate that is valid for all subdomains of a domain, by prefixing the domain name with `*.` (e.g. `*.example.org`). The domain itself is not covered by the wildcard certificate, and also needs to be added to the CSR if necessary.
|
You can also generate a wildcard certificate that is valid for all subdomains of a domain, by prefixing the domain name with `*.` (e.g. `*.example.org`). The domain itself is not covered by the wildcard certificate, and also needs to be added to the CSR if necessary.
|
||||||
|
@ -104,7 +107,7 @@ byte[] csr = csrb.getEncoded();
|
||||||
|
|
||||||
In the subsequent authorization process, you would have to prove ownership of the `example.org` domain.
|
In the subsequent authorization process, you would have to prove ownership of the `example.org` domain.
|
||||||
|
|
||||||
> __Note:__ Some CAs may reject wildcard certificate orders, or may involve `Challenge`s that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy.
|
> __Note:__ Some CAs may reject wildcard certificate orders, may only offer a limited set of `Challenge`s, or may involve `Challenge`s that are not documented here. Refer to your CA's documentation to find out about the wildcard certificate policy.
|
||||||
|
|
||||||
> __Note:__ _acme4j_ accepts all kind of wildcard notations (e.g. `www.*.example.org`, `*.*.example.org`.). However, those notations are not specified and may be rejected by your CA.
|
> __Note:__ _acme4j_ accepts all kind of wildcard notations (e.g. `www.*.example.org`, `*.*.example.org`.). However, those notations are not specified and may be rejected by your CA.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue