Implement new order finalization

Replaces the "CSR first" new-order flow, see ietf-wg-acme/acme#342
pull/55/head
Richard Körber 2017-11-09 23:35:36 +01:00
parent a6ec6d04d2
commit e0673c93bd
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
12 changed files with 252 additions and 74 deletions

View File

@ -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);
}

View File

@ -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());

View File

@ -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")));

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
{
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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",

View File

@ -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!");

View File

@ -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();

View File

@ -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.