mirror of https://github.com/shred/acme4j
Use an OrderBuilder for collecting order parameters
parent
e0673c93bd
commit
827e1277ef
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue