Use an OrderBuilder for collecting order parameters

pull/55/head
Richard Körber 2017-11-10 00:14:05 +01:00
parent e0673c93bd
commit 827e1277ef
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
9 changed files with 310 additions and 113 deletions

View File

@ -19,10 +19,8 @@ import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
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;
@ -140,54 +138,12 @@ public class Account extends AcmeResource {
}
/**
* Orders a certificate. The certificate will be associated with this account.
* Creates a builder for a new {@link Order}.
*
* @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
* @return {@link OrderBuilder} object
*/
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.array("identifiers", identifiers);
if (notBefore != null) {
claims.put("notBefore", notBefore);
}
if (notAfter != null) {
claims.put("notAfter", notAfter);
}
conn.sendSignedRequest(getSession().resourceUrl(Resource.NEW_ORDER), claims, getSession());
conn.accept(HttpURLConnection.HTTP_CREATED);
JSON json = conn.readJsonResponse();
Order order = new Order(getSession(), conn.getLocation());
order.unmarshal(json);
return order;
}
public OrderBuilder newOrder() throws AcmeException {
return new OrderBuilder(getSession());
}
/**

View File

@ -0,0 +1,162 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2017 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;
import static java.util.Objects.requireNonNull;
import static org.shredzone.acme4j.toolbox.AcmeUtils.toAce;
import java.net.HttpURLConnection;
import java.time.Instant;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A builder for a new {@link Order} object.
*/
public class OrderBuilder {
private static final Logger LOG = LoggerFactory.getLogger(OrderBuilder.class);
private final Session session;
private final Set<String> domainSet = new LinkedHashSet<>();
private Instant notBefore;
private Instant notAfter;
/**
* Create a new {@link OrderBuilder}.
*
* @param session {@link Session} to bind with
*/
protected OrderBuilder(Session session) {
this.session = session;
}
/**
* Adds a domain name to the order.
*
* @param domain
* Name of a domain to be ordered. May be a wildcard domain if supported by
* the CA. IDN names are accepted and will be ACE encoded automatically.
* @return itself
*/
public OrderBuilder domain(String domain) {
domainSet.add(toAce(requireNonNull(domain, "domain")));
return this;
}
/**
* Adds domain names to the order.
*
* @param domains
* Collection of domain names to be ordered. May be wildcard domains if
* supported by the CA. IDN names are accepted and will be ACE encoded
* automatically.
* @return itself
*/
public OrderBuilder domains(String... domains) {
for (String domain : requireNonNull(domains, "domains")) {
domain(domain);
}
return this;
}
/**
* Adds a collection of domain names to the order.
*
* @param domains
* Collection of domain names to be ordered. May be wildcard domains if
* supported by the CA. IDN names are accepted and will be ACE encoded
* automatically.
* @return itself
*/
public OrderBuilder domains(Collection<String> domains) {
requireNonNull(domains, "domains").forEach(this::domain);
return this;
}
/**
* Sets a "not before" date in the certificate. May be ignored by the CA.
*
* @param notBefore "not before" date
* @return itself
*/
public OrderBuilder notBefore(Instant notBefore) {
this.notBefore = requireNonNull(notBefore, "notBefore");
return this;
}
/**
* Sets a "not after" date in the certificate. May be ignored by the CA.
*
* @param notAfter "not after" date
* @return itself
*/
public OrderBuilder notAfter(Instant notAfter) {
this.notAfter = requireNonNull(notAfter, "notAfter");
return this;
}
/**
* Sends a new order to the server, and returns an {@link Order} object.
*
* @return {@link Order} that was created
*/
public Order create() throws AcmeException {
if (domainSet.isEmpty()) {
throw new IllegalArgumentException("At least one domain is required");
}
Object[] identifiers = new Object[domainSet.size()];
Iterator<String> di = domainSet.iterator();
for (int ix = 0; ix < identifiers.length; ix++) {
identifiers[ix] = new JSONBuilder()
.put("type", "dns")
.put("value", di.next())
.toMap();
}
LOG.debug("create");
try (Connection conn = session.provider().connect()) {
JSONBuilder claims = new JSONBuilder();
claims.array("identifiers", identifiers);
if (notBefore != null) {
claims.put("notBefore", notBefore);
}
if (notAfter != null) {
claims.put("notAfter", notAfter);
}
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, session);
conn.accept(HttpURLConnection.HTTP_CREATED);
JSON json = conn.readJsonResponse();
Order order = new Order(session, conn.getLocation());
order.unmarshal(json);
return order;
}
}
}

View File

@ -15,7 +15,6 @@ package org.shredzone.acme4j;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
import static org.shredzone.acme4j.toolbox.TestUtils.*;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
@ -25,7 +24,6 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyPair;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@ -185,60 +183,6 @@ public class AccountTest {
provider.close();
}
/**
* Test that a new {@link Authorization} can be created.
*/
@Test
public void testOrderCertificate() throws Exception {
Instant notBefore = parseTimestamp("2016-01-01T00:00:00Z");
Instant notAfter = parseTimestamp("2016-01-08T00:00:00Z");
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendSignedRequest(URL url, JSONBuilder claims, Session session) {
assertThat(url, is(resourceUrl));
assertThat(claims.toString(), sameJSONAs(getJSON("requestOrderRequest").toString()));
assertThat(session, is(notNullValue()));
}
@Override
public int accept(int... httpStatus) throws AcmeException {
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_CREATED));
return HttpURLConnection.HTTP_CREATED;
}
@Override
public JSON readJsonResponse() {
return getJSON("requestOrderResponse");
}
@Override
public URL getLocation() {
return locationUrl;
}
};
Session session = provider.createSession();
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
Account account = new Account(session, locationUrl);
Order order = account.orderCertificate(
Arrays.asList("example.com", "www.example.com"),
notBefore, notAfter);
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")));
assertThat(order.getStatus(), is(Status.PENDING));
assertThat(order.getLocation(), is(locationUrl));
assertThat(order.getAuthorizations(), is(notNullValue()));
assertThat(order.getAuthorizations().size(), is(2));
provider.close();
}
/**
* Test that a domain can be pre-authorized.
*/

View File

@ -0,0 +1,103 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2017 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;
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.URL;
import java.time.Instant;
import java.util.Arrays;
import org.junit.Test;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
/**
* Unit tests for {@link OrderBuilder}.
*/
public class OrderBuilderTest {
private URL resourceUrl = url("http://example.com/acme/resource");
private URL locationUrl = url("http://example.com/acme/account");
/**
* Test that a new {@link Order} can be created.
*/
@Test
public void testOrderCertificate() throws Exception {
Instant notBefore = parseTimestamp("2016-01-01T00:00:00Z");
Instant notAfter = parseTimestamp("2016-01-08T00:00:00Z");
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendSignedRequest(URL url, JSONBuilder claims, Session session) {
assertThat(url, is(resourceUrl));
assertThat(claims.toString(), sameJSONAs(getJSON("requestOrderRequest").toString()));
assertThat(session, is(notNullValue()));
}
@Override
public int accept(int... httpStatus) throws AcmeException {
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_CREATED));
return HttpURLConnection.HTTP_CREATED;
}
@Override
public JSON readJsonResponse() {
return getJSON("requestOrderResponse");
}
@Override
public URL getLocation() {
return locationUrl;
}
};
Session session = provider.createSession();
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
Account account = new Account(session, locationUrl);
Order order = account.newOrder()
.domains("example.com", "www.example.com")
.domain("example.org")
.domains(Arrays.asList("m.example.com", "m.example.org"))
.notBefore(notBefore)
.notAfter(notAfter)
.create();
assertThat(order.getDomains(), containsInAnyOrder(
"example.com", "www.example.com",
"example.org",
"m.example.com", "m.example.org"));
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")));
assertThat(order.getStatus(), is(Status.PENDING));
assertThat(order.getLocation(), is(locationUrl));
assertThat(order.getAuthorizations(), is(notNullValue()));
assertThat(order.getAuthorizations().size(), is(2));
provider.close();
}
}

View File

@ -7,6 +7,18 @@
{
"type": "dns",
"value": "www.example.com"
},
{
"type": "dns",
"value": "example.org"
},
{
"type": "dns",
"value": "m.example.com"
},
{
"type": "dns",
"value": "m.example.org"
}
],
"notBefore": "2016-01-01T00:00:00Z",

View File

@ -9,6 +9,18 @@
{
"type": "dns",
"value": "www.example.com"
},
{
"type": "dns",
"value": "example.org"
},
{
"type": "dns",
"value": "m.example.com"
},
{
"type": "dns",
"value": "m.example.org"
}
],
"notBefore": "2016-01-01T00:10:00Z",

View File

@ -91,7 +91,7 @@ public class ClientTest {
KeyPair domainKeyPair = loadOrCreateDomainKeyPair();
// Order the certificate
Order order = acct.orderCertificate(domains, null, null);
Order order = acct.newOrder().domains(domains).create();
// Perform all required authorizations
for (Authorization auth : order.getAuthorizations()) {

View File

@ -23,7 +23,6 @@ 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;
@ -138,7 +137,11 @@ public class OrderIT extends PebbleITBase {
Instant notBefore = Instant.now().truncatedTo(ChronoUnit.MILLIS);
Instant notAfter = notBefore.plus(Duration.ofDays(20L));
Order order = account.orderCertificate(Arrays.asList(domain), notBefore, notAfter);
Order order = account.newOrder()
.domain(domain)
.notBefore(notBefore)
.notAfter(notAfter)
.create();
assertThat(order.getNotBefore(), is(notBefore));
assertThat(order.getNotAfter(), is(notAfter));
assertThat(order.getStatus(), is(Status.PENDING));

View File

@ -2,14 +2,15 @@
Once you have your account set up, you are ready to order certificates.
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.
Use your `Account` object to order the certificate, by using the `newOrder()` method. It returns an OrderBuilder object that helps you to collect the parameters of the order. You can give one or more domain names. Optionally you can also 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
Order order = account.orderCertificate(
Arrays.of("example.org", "www.example.org", "m.example.org"),
null, null);
Order order = account.newOrder()
.domains("example.org", "www.example.org", "m.example.org")
.notAfter(Instant.now().plus(Duration.ofDays(20L)))
.create();
```
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.
@ -89,13 +90,17 @@ order.execute(csr);
## 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 order if necessary.
You must be able to prove ownership of the domain that you want to order a wildcard certificate for. The corresponding `Authorization` resource only refers to that domain, and does not contain the wildcard notation.
The following example creates a CSR for `example.org` and `*.example.org`:
The following example creates an `Order` and a CSR for `example.org` and `*.example.org`:
```java
Order order = account.newOrder()
.domains("example.org", "*.example.org")
.create();
KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption
CSRBuilder csrb = new CSRBuilder();