Skip to content

Commit

Permalink
Implement new order finalization
Browse files Browse the repository at this point in the history
Replaces the "CSR first" new-order flow, see ietf-wg-acme/acme#342
  • Loading branch information
shred committed Nov 9, 2017
1 parent 6ac444b commit edeb9a0
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 84 deletions.
26 changes: 21 additions & 5 deletions acme4j-client/src/main/java/org/shredzone/acme4j/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand Down Expand Up @@ -141,21 +142,36 @@ public void update() throws AcmeException {
/**
* 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);
}
Expand Down
54 changes: 48 additions & 6 deletions acme4j-client/src/main/java/org/shredzone/acme4j/Order.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;

Expand All @@ -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;

Expand Down Expand Up @@ -87,11 +89,10 @@ public Instant getExpires() {
}

/**
* 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;
}

/**
Expand Down Expand Up @@ -121,6 +122,42 @@ public List<Authorization> getAuthorizations() {
.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.
*/
Expand Down Expand Up @@ -165,15 +202,20 @@ protected void load() {
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ public Collection<URL> getLinks(String relation) {
*/
@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");

Expand Down Expand Up @@ -224,9 +223,11 @@ public URL getLocation() {
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")));
Expand Down
68 changes: 65 additions & 3 deletions acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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;
Expand All @@ -28,6 +29,7 @@
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;

/**
Expand All @@ -36,14 +38,13 @@
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) {
Expand Down Expand Up @@ -71,10 +72,11 @@ public JSON readJsonResponse() {
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")));
Expand Down Expand Up @@ -135,4 +137,64 @@ public JSON readJsonResponse() {
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();
}

}
3 changes: 3 additions & 0 deletions acme4j-client/src/test/resources/json/finalizeRequest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb"
}
22 changes: 22 additions & 0 deletions acme4j-client/src/test/resources/json/finalizeResponse.json
Original file line number Diff line number Diff line change
@@ -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"
}
11 changes: 10 additions & 1 deletion acme4j-client/src/test/resources/json/requestOrderRequest.json
Original file line number Diff line number Diff line change
@@ -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"
}
14 changes: 12 additions & 2 deletions acme4j-client/src/test/resources/json/requestOrderResponse.json
Original file line number Diff line number Diff line change
@@ -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"
}
12 changes: 11 additions & 1 deletion acme4j-client/src/test/resources/json/updateOrderResponse.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading

0 comments on commit edeb9a0

Please sign in to comment.