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.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -141,21 +142,36 @@ public class Account extends AcmeResource {
|
|||
/**
|
||||
* Orders a certificate. The certificate will be associated with this account.
|
||||
*
|
||||
* @param csr
|
||||
* CSR containing the parameters for the certificate being requested
|
||||
* @param domains
|
||||
* Domains of the certificate to be ordered. May contain wildcard domains.
|
||||
* IDN names are accepted and will be ACE encoded automatically.
|
||||
* @param notBefore
|
||||
* Requested "notBefore" date in the certificate, or {@code null}
|
||||
* @param notAfter
|
||||
* Requested "notAfter" date in the certificate, or {@code null}
|
||||
* @return {@link Order} object for this domain
|
||||
*/
|
||||
public Order orderCertificate(byte[] csr, Instant notBefore, Instant notAfter) throws AcmeException {
|
||||
Objects.requireNonNull(csr, "csr");
|
||||
public Order orderCertificate(Collection<String> domains, Instant notBefore, Instant notAfter)
|
||||
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");
|
||||
try (Connection conn = getSession().provider().connect()) {
|
||||
JSONBuilder claims = new JSONBuilder();
|
||||
claims.putBase64("csr", csr);
|
||||
claims.array("identifiers", identifiers);
|
||||
|
||||
if (notBefore != null) {
|
||||
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.AcmeLazyLoadingException;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -36,11 +37,12 @@ public class Order extends AcmeResource {
|
|||
|
||||
private Status status;
|
||||
private Instant expires;
|
||||
private byte[] csr;
|
||||
private List<String> identifiers;
|
||||
private Instant notBefore;
|
||||
private Instant notAfter;
|
||||
private Problem error;
|
||||
private List<URL> authorizations;
|
||||
private URL finalizeUrl;
|
||||
private Certificate certificate;
|
||||
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() {
|
||||
load();
|
||||
return csr;
|
||||
public List<String> getDomains() {
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,6 +122,42 @@ public class Order extends AcmeResource {
|
|||
.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.
|
||||
*/
|
||||
|
@ -165,15 +202,20 @@ public class Order extends AcmeResource {
|
|||
public void unmarshal(JSON json) {
|
||||
this.status = json.get("status").asStatusOrElse(Status.UNKNOWN);
|
||||
this.expires = json.get("expires").asInstant();
|
||||
this.csr = json.get("csr").asBinary();
|
||||
this.notBefore = json.get("notBefore").asInstant();
|
||||
this.notAfter = json.get("notAfter").asInstant();
|
||||
this.finalizeUrl = json.get("finalizeURL").asURL();
|
||||
|
||||
URL certUrl = json.get("certificate").asURL();
|
||||
certificate = certUrl != null ? Certificate.bind(getSession(), certUrl) : null;
|
||||
|
||||
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()
|
||||
.map(JSON.Value::asURL)
|
||||
.collect(toList());
|
||||
|
|
|
@ -190,7 +190,6 @@ public class AccountTest {
|
|||
*/
|
||||
@Test
|
||||
public void testOrderCertificate() throws Exception {
|
||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
||||
Instant notBefore = parseTimestamp("2016-01-01T00:00:00Z");
|
||||
Instant notAfter = parseTimestamp("2016-01-08T00:00:00Z");
|
||||
|
||||
|
@ -224,9 +223,11 @@ public class AccountTest {
|
|||
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
|
||||
|
||||
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.getNotAfter(), is(parseTimestamp("2016-01-08T00:10: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.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
|
||||
import static org.shredzone.acme4j.toolbox.TestUtils.*;
|
||||
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
|
@ -28,6 +29,7 @@ import org.junit.Test;
|
|||
import org.shredzone.acme4j.exception.AcmeException;
|
||||
import org.shredzone.acme4j.provider.TestableConnectionProvider;
|
||||
import org.shredzone.acme4j.toolbox.JSON;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
import org.shredzone.acme4j.toolbox.TestUtils;
|
||||
|
||||
/**
|
||||
|
@ -36,14 +38,13 @@ import org.shredzone.acme4j.toolbox.TestUtils;
|
|||
public class OrderTest {
|
||||
|
||||
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
|
||||
public void testUpdate() throws Exception {
|
||||
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
|
||||
|
||||
TestableConnectionProvider provider = new TestableConnectionProvider() {
|
||||
@Override
|
||||
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.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.getCsr(), is(csr));
|
||||
assertThat(order.getFinalizeLocation(), is(finalizeUrl));
|
||||
|
||||
assertThat(order.getError(), is(notNullValue()));
|
||||
assertThat(order.getError().getType(), is(URI.create("urn:ietf:params:acme:error:connection")));
|
||||
|
@ -135,4 +137,64 @@ public class OrderTest {
|
|||
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",
|
||||
"notAfter": "2016-01-08T00:00:00Z"
|
||||
}
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
{
|
||||
"status": "pending",
|
||||
"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",
|
||||
"notAfter": "2016-01-08T00:10:00Z",
|
||||
"authorizations": [
|
||||
"https://example.com/acme/authz/1234",
|
||||
"https://example.com/acme/authz/2345"
|
||||
]
|
||||
],
|
||||
"finalizeURL": "https://example.com/acme/acct/1/order/1/finalize"
|
||||
}
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
{
|
||||
"status": "pending",
|
||||
"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",
|
||||
"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",
|
||||
"error": {
|
||||
"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!
|
||||
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.
|
||||
CSRBuilder csrb = new CSRBuilder();
|
||||
csrb.addDomains(domains);
|
||||
|
@ -100,16 +108,8 @@ public class ClientTest {
|
|||
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
|
||||
order.update();
|
||||
order.execute(csrb.getEncoded());
|
||||
Certificate certificate = order.getCertificate();
|
||||
|
||||
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.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.shredzone.acme4j.Account;
|
||||
|
@ -134,16 +135,10 @@ public class OrderIT extends PebbleITBase {
|
|||
|
||||
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 notAfter = notBefore.plus(Duration.ofDays(20L));
|
||||
|
||||
Order order = account.orderCertificate(encodedCsr, notBefore, notAfter);
|
||||
assertThat(order.getCsr(), is(encodedCsr));
|
||||
Order order = account.orderCertificate(Arrays.asList(domain), notBefore, notAfter);
|
||||
assertThat(order.getNotBefore(), is(notBefore));
|
||||
assertThat(order.getNotAfter(), is(notAfter));
|
||||
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();
|
||||
X509Certificate cert = certificate.getCertificate();
|
||||
|
|
|
@ -2,45 +2,14 @@
|
|||
|
||||
Once you have your account set up, you are ready to order certificates.
|
||||
|
||||
## Create a Certificate Signing Request (CSR)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```java
|
||||
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.
|
||||
|
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
> __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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue