Add Order resource

pull/55/head
Richard Körber 2017-04-27 22:36:46 +02:00
parent 4fe4c12c62
commit 4772488896
11 changed files with 547 additions and 162 deletions

View File

@ -19,9 +19,6 @@ import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -46,7 +43,6 @@ public class Authorization extends AcmeResource {
private Status status;
private Instant expires;
private List<Challenge> challenges;
private List<List<Challenge>> combinations;
private boolean loaded = false;
protected Authorization(Session session, URL location) {
@ -101,17 +97,8 @@ public class Authorization extends AcmeResource {
}
/**
* Gets all combinations of challenges supported by the server.
*/
public List<List<Challenge>> getCombinations() {
load();
return combinations;
}
/**
* Finds a single {@link Challenge} of the given type. Responding to this
* {@link Challenge} is sufficient for authorization. This is a convenience call to
* {@link #findCombination(String...)}.
* Finds a {@link Challenge} of the given type. Responding to this {@link Challenge}
* is sufficient for authorization.
*
* @param type
* Challenge name (e.g. "http-01")
@ -121,45 +108,11 @@ public class Authorization extends AcmeResource {
* if the type does not match the expected Challenge class type
*/
@SuppressWarnings("unchecked")
public <T extends Challenge> T findChallenge(String type) {
return (T) findCombination(type).stream().findFirst().orElse(null);
}
/**
* Finds a combination of {@link Challenge} types that the client supports. The client
* has to respond to <em>all</em> of the {@link Challenge}s returned. However, this
* method attempts to find the combination with the smallest number of
* {@link Challenge}s.
*
* @param types
* Challenge name or names (e.g. "http-01"), in no particular order.
* Basically this is a collection of all challenge types supported by your
* implementation.
* @return Matching {@link Challenge} combination, or an empty collection if the ACME
* server does not support any of your challenges. The challenges are returned
* in no particular order. The result may be a subset of the types you have
* provided, if fewer challenges are actually required for a successful
* validation.
*/
public Collection<Challenge> findCombination(String... types) {
Collection<String> available = Arrays.asList(types);
Collection<String> combinationTypes = new ArrayList<>();
Collection<Challenge> result = Collections.emptyList();
for (List<Challenge> combination : getCombinations()) {
combinationTypes.clear();
for (Challenge c : combination) {
combinationTypes.add(c.getType());
}
if (available.containsAll(combinationTypes) &&
(result.isEmpty() || result.size() > combination.size())) {
result = combination;
}
}
return Collections.unmodifiableCollection(result);
public <T extends Challenge> T findChallenge(final String type) {
return (T) getChallenges().stream()
.filter(ch -> type.equals(ch.getType()))
.reduce((a, b) -> {throw new AcmeProtocolException("Found more than one challenge of type " + type);})
.orElse(null);
}
/**
@ -240,7 +193,6 @@ public class Authorization extends AcmeResource {
}
challenges = fetchChallenges(json);
combinations = fetchCombinations(json, challenges);
loaded = true;
}
@ -261,39 +213,4 @@ public class Authorization extends AcmeResource {
.collect(toList()));
}
/**
* Fetches all possible combination of {@link Challenge} that are defined in the JSON.
*
* @param json
* {@link JSON} to read
* @param challenges
* List of available {@link Challenge}
* @return List of {@link Challenge} combinations
*/
private List<List<Challenge>> fetchCombinations(JSON json, List<Challenge> challenges) {
JSON.Array jsonCombinations = json.get("combinations").asArray();
if (jsonCombinations.isEmpty()) {
return Arrays.asList(challenges);
}
return Collections.unmodifiableList(jsonCombinations.stream()
.map(JSON.Value::asArray)
.map(this::findChallenges)
.collect(toList()));
}
/**
* Converts an array of challenge indexes to a list of matching {@link Challenge}.
*
* @param combination
* {@link Array} of the challenge indexes
* @return List of matching {@link Challenge}
*/
private List<Challenge> findChallenges(JSON.Array combination) {
return combination.stream()
.mapToInt(JSON.Value::asInt)
.mapToObj(challenges::get)
.collect(toList());
}
}

View File

@ -0,0 +1,171 @@
/*
* 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.stream.Collectors.toList;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.util.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a certificate order.
*/
public class Order extends AcmeResource {
private static final long serialVersionUID = 5435808648658292177L;
private static final Logger LOG = LoggerFactory.getLogger(Order.class);
private Status status;
private Instant expires;
private byte[] csr;
private Instant notBefore;
private Instant notAfter;
private List<URL> authorizations;
private URL certificate;
private boolean loaded = false;
protected Order(Session session, URL location) {
super(session);
setLocation(location);
}
/**
* Creates a new instance of {@link Order} and binds it to the {@link Session}.
*
* @param session
* {@link Session} to be used
* @param location
* Location URL of the order
* @return {@link Order} bound to the session and location
*/
public static Order bind(Session session, URL location) {
return new Order(session, location);
}
/**
* Returns the current status of the order.
*/
public Status getStatus() {
load();
return status;
}
/**
* Gets the expiry date of the authorization, if set by the server.
*/
public Instant getExpires() {
load();
return expires;
}
/**
* Gets the CSR that was used for the order.
*/
public byte[] getCsr() {
load();
return csr;
}
/**
* Gets the "not before" date that was used for the order, or {@code null}.
*/
public Instant getNotBefore() {
load();
return notBefore;
}
/**
* Gets the "not after" date that was used for the order, or {@code null}.
*/
public Instant getNotAfter() {
load();
return notAfter;
}
/**
* Gets the {@link Authorization} required for this order.
*/
public List<Authorization> getAuthorizations() {
load();
Session session = getSession();
return authorizations.stream()
.map(loc -> Authorization.bind(session, loc))
.collect(toList());
}
/**
* Gets the {@link URL} where the certificate can be downloaded from, if it is
* available. {@code null} otherwise.
*/
public URL getCertificateLocation() {
load();
return certificate;
}
/**
* Updates the order to the current account status.
*/
public void update() throws AcmeException {
LOG.debug("update");
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
conn.accept(HttpURLConnection.HTTP_OK);
JSON json = conn.readJsonResponse();
unmarshal(json);
}
}
/**
* Lazily updates the object's state when one of the getters is invoked.
*/
protected void load() {
if (!loaded) {
try {
update();
} catch (AcmeException ex) {
throw new AcmeProtocolException("Could not load lazily", ex);
}
}
}
/**
* Sets order properties according to the given JSON data.
*
* @param json
* JSON data
*/
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.certificate = json.get("certificate").asURL();
this.authorizations = json.get("authorizations").asArray().stream()
.map(JSON.Value::asURL)
.collect(toList());
loaded = true;
}
}

View File

@ -158,6 +158,42 @@ public class Registration extends AcmeResource {
}
}
/**
* Orders a certificate. The certificate will be associated with this registration.
*
* @param csr
* CSR containing the parameters for the certificate being requested
* @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");
LOG.debug("orderCertificate");
try (Connection conn = getSession().provider().connect()) {
JSONBuilder claims = new JSONBuilder();
claims.putBase64("csr", csr);
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;
}
}
/**
* Authorizes a domain. The domain is associated with this registration.
* <p>

View File

@ -23,6 +23,7 @@ public enum Resource {
NEW_AUTHZ("new-authz"),
NEW_CERT("new-cert"),
NEW_NONCE("new-nonce"),
NEW_ORDER("new-order"),
REVOKE_CERT("revoke-cert");
private final String path;

View File

@ -23,7 +23,6 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
@ -32,6 +31,7 @@ import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.util.JSON;
@ -43,12 +43,12 @@ import org.shredzone.acme4j.util.JSONBuilder;
public class AuthorizationTest {
private static final String SNAILMAIL_TYPE = "snail-01"; // a non-existent challenge
private static final String DUPLICATE_TYPE = "duplicate-01"; // a duplicate challenge
private URL locationUrl = url("http://example.com/acme/registration");
/**
* Test that {@link Authorization#findChallenge(String)} does only find standalone
* challenges, and nothing else.
* Test that {@link Authorization#findChallenge(String)} finds challenges.
*/
@Test
public void testFindChallenge() throws IOException {
@ -58,60 +58,30 @@ public class AuthorizationTest {
Challenge c1 = authorization.findChallenge(SNAILMAIL_TYPE);
assertThat(c1, is(nullValue()));
// HttpChallenge is available as standalone challenge
// HttpChallenge is available
Challenge c2 = authorization.findChallenge(Http01Challenge.TYPE);
assertThat(c2, is(notNullValue()));
assertThat(c2, is(instanceOf(Http01Challenge.class)));
// TlsSniChallenge is available, but not as standalone challenge
Challenge c3 = authorization.findChallenge(TlsSni02Challenge.TYPE);
assertThat(c3, is(nullValue()));
// Dns01Challenge is available
Challenge c3 = authorization.findChallenge(Dns01Challenge.TYPE);
assertThat(c3, is(notNullValue()));
assertThat(c3, is(instanceOf(Dns01Challenge.class)));
// TlsSni02Challenge is available
Challenge c4 = authorization.findChallenge(TlsSni02Challenge.TYPE);
assertThat(c4, is(notNullValue()));
assertThat(c4, is(instanceOf(TlsSni02Challenge.class)));
}
/**
* Test that {@link Authorization#findCombination(String...)} does only find proper
* combinations.
* Test that {@link Authorization#findChallenge(String)} fails on duplicate
* challenges.
*/
@Test
@SuppressWarnings("unchecked")
public void testFindCombination() throws IOException {
@Test(expected = AcmeProtocolException.class)
public void testFailDuplicateChallenges() throws IOException {
Authorization authorization = createChallengeAuthorization();
// Standalone challenge
Collection<Challenge> c1 = authorization.findCombination(Http01Challenge.TYPE);
assertThat(c1, hasSize(1));
assertThat(c1, contains(instanceOf(Http01Challenge.class)));
// Available combined challenge
Collection<Challenge> c2 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE);
assertThat(c2, hasSize(2));
assertThat(c2, contains(instanceOf(Dns01Challenge.class),
instanceOf(TlsSni02Challenge.class)));
// Order does not matter
Collection<Challenge> c3 = authorization.findCombination(TlsSni02Challenge.TYPE, Dns01Challenge.TYPE);
assertThat(c3, hasSize(2));
assertThat(c3, contains(instanceOf(Dns01Challenge.class),
instanceOf(TlsSni02Challenge.class)));
// Finds smaller combinations as well
Collection<Challenge> c4 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, SNAILMAIL_TYPE);
assertThat(c4, hasSize(2));
assertThat(c4, contains(instanceOf(Dns01Challenge.class),
instanceOf(TlsSni02Challenge.class)));
// Finds the smallest possible combination
Collection<Challenge> c5 = authorization.findCombination(Dns01Challenge.TYPE, TlsSni02Challenge.TYPE, Http01Challenge.TYPE);
assertThat(c5, hasSize(1));
assertThat(c5, contains(instanceOf(Http01Challenge.class)));
// Finds only entire combinations
Collection<Challenge> c6 = authorization.findCombination(Dns01Challenge.TYPE);
assertThat(c6, is(empty()));
// Does not find challenges that have not been provided
Collection<Challenge> c7 = authorization.findCombination(SNAILMAIL_TYPE);
assertThat(c7, is(empty()));
authorization.findChallenge(DUPLICATE_TYPE);
}
/**
@ -161,12 +131,6 @@ public class AuthorizationTest {
assertThat(auth.getChallenges(), containsInAnyOrder(
(Challenge) httpChallenge, (Challenge) dnsChallenge));
assertThat(auth.getCombinations(), hasSize(2));
assertThat(auth.getCombinations().get(0), containsInAnyOrder(
(Challenge) httpChallenge));
assertThat(auth.getCombinations().get(1), containsInAnyOrder(
(Challenge) httpChallenge, (Challenge) dnsChallenge));
provider.close();
}
@ -279,12 +243,6 @@ public class AuthorizationTest {
assertThat(auth.getChallenges(), containsInAnyOrder(
(Challenge) httpChallenge, (Challenge) dnsChallenge));
assertThat(auth.getCombinations(), hasSize(2));
assertThat(auth.getCombinations().get(0), containsInAnyOrder(
(Challenge) httpChallenge));
assertThat(auth.getCombinations().get(1), containsInAnyOrder(
(Challenge) httpChallenge, (Challenge) dnsChallenge));
provider.close();
}
@ -327,6 +285,7 @@ public class AuthorizationTest {
provider.putTestChallenge(Http01Challenge.TYPE, new Http01Challenge(session));
provider.putTestChallenge(Dns01Challenge.TYPE, new Dns01Challenge(session));
provider.putTestChallenge(TlsSni02Challenge.TYPE, new TlsSni02Challenge(session));
provider.putTestChallenge(DUPLICATE_TYPE, new Challenge(session));
Authorization authorization = new Authorization(session, locationUrl);
authorization.unmarshalAuthorization(getJsonAsObject("authorizationChallenges"));

View File

@ -0,0 +1,133 @@
/*
* 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.util.AcmeUtils.parseTimestamp;
import static org.shredzone.acme4j.util.TestUtils.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.TestUtils;
/**
* Unit tests for {@link Order}.
*/
public class OrderTest {
private URL locationUrl = url("http://example.com/acme/order/1234");
/**
* 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) {
assertThat(url, is(locationUrl));
}
@Override
public int accept(int... httpStatus) throws AcmeException {
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK));
return HttpURLConnection.HTTP_OK;
}
@Override
public JSON readJsonResponse() {
return getJsonAsObject("updateOrderResponse");
}
};
Session session = provider.createSession();
Order order = new Order(session, locationUrl);
order.update();
assertThat(order.getStatus(), is(Status.PENDING));
assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z")));
assertThat(order.getLocation(), is(locationUrl));
assertThat(order.getNotBefore(), is(parseTimestamp("2016-01-01T00:00:00Z")));
assertThat(order.getNotAfter(), is(parseTimestamp("2016-01-08T00:00:00Z")));
assertThat(order.getCertificateLocation(), is(url("https://example.com/acme/cert/1234")));
assertThat(order.getCsr(), is(csr));
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();
}
/**
* Test lazy loading.
*/
@Test
public void testLazyLoading() throws Exception {
final AtomicBoolean requestWasSent = new AtomicBoolean(false);
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendRequest(URL url, Session session) {
requestWasSent.set(true);
assertThat(url, is(locationUrl));
}
@Override
public int accept(int... httpStatus) throws AcmeException {
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK));
return HttpURLConnection.HTTP_OK;
}
@Override
public JSON readJsonResponse() {
return getJsonAsObject("updateOrderResponse");
}
};
Session session = provider.createSession();
Order order = new Order(session, locationUrl);
// Lazy loading
assertThat(requestWasSent.get(), is(false));
assertThat(order.getCertificateLocation(), is(url("https://example.com/acme/cert/1234")));
assertThat(requestWasSent.get(), is(true));
// Subsequent queries do not trigger another load
requestWasSent.set(false);
assertThat(order.getCertificateLocation(), is(url("https://example.com/acme/cert/1234")));
assertThat(order.getStatus(), is(Status.PENDING));
assertThat(order.getExpires(), is(parseTimestamp("2015-03-01T14:09:00Z")));
assertThat(requestWasSent.get(), is(false));
provider.close();
}
}

View File

@ -15,6 +15,7 @@ package org.shredzone.acme4j;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp;
import static org.shredzone.acme4j.util.TestUtils.*;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;
@ -31,6 +32,7 @@ import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jose4j.jws.JsonWebSignature;
@ -267,12 +269,6 @@ public class RegistrationTest {
assertThat(auth.getChallenges(), containsInAnyOrder(
(Challenge) httpChallenge, (Challenge) dnsChallenge));
assertThat(auth.getCombinations(), hasSize(2));
assertThat(auth.getCombinations().get(0), containsInAnyOrder(
(Challenge) httpChallenge));
assertThat(auth.getCombinations().get(1), containsInAnyOrder(
(Challenge) httpChallenge, (Challenge) dnsChallenge));
provider.close();
}
@ -302,6 +298,66 @@ public class RegistrationTest {
provider.close();
}
/**
* Test that a order can be requested.
*/
@Test
public void testOrder() throws AcmeException, IOException {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendSignedRequest(URL url, JSONBuilder claims, Session session) {
assertThat(url, is(resourceUrl));
assertThat(claims.toString(), sameJSONAs(getJson("requestOrderRequest")));
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 getJsonAsObject("requestOrderResponse");
}
@Override
public URL getLocation() {
return locationUrl;
}
};
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
ZoneId utc = ZoneId.of("UTC");
Instant notBefore = LocalDate.of(2016, 1, 1).atStartOfDay(utc).toInstant();
Instant notAfter = LocalDate.of(2016, 1, 8).atStartOfDay(utc).toInstant();
Registration registration = new Registration(provider.createSession(), locationUrl);
Order order = registration.orderCertificate(csr, notBefore, notAfter);
assertThat(order.getLocation(), is(locationUrl));
assertThat(order.getCsr(), is(csr));
assertThat(order.getStatus(), is(Status.PENDING));
assertThat(order.getExpires(), is(parseTimestamp("2016-01-01T00:00:00Z")));
assertThat(order.getLocation(), is(locationUrl));
assertThat(order.getNotBefore(), is(notBefore));
assertThat(order.getNotAfter(), is(notAfter));
assertThat(order.getCertificateLocation(), is(nullValue()));
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();
}
/**
* Test that a certificate can be requested and is delivered synchronously.
*/

View File

@ -32,11 +32,12 @@ public class ResourceTest {
assertThat(Resource.NEW_AUTHZ.path(), is("new-authz"));
assertThat(Resource.NEW_CERT.path(), is("new-cert"));
assertThat(Resource.NEW_NONCE.path(), is("new-nonce"));
assertThat(Resource.NEW_ORDER.path(), is("new-order"));
assertThat(Resource.NEW_REG.path(), is("new-reg"));
assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert"));
// fails if there are untested future Resource values
assertThat(Resource.values().length, is(6));
assertThat(Resource.values().length, is(7));
}
}

View File

@ -0,0 +1,71 @@
/*
* 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.it;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.security.KeyPair;
import java.time.Duration;
import java.time.Instant;
import org.junit.Test;
import org.shredzone.acme4j.Authorization;
import org.shredzone.acme4j.Order;
import org.shredzone.acme4j.Registration;
import org.shredzone.acme4j.RegistrationBuilder;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.TestUtils;
/**
* Tests the complete process of ordering a certificate.
*/
public class OrderIT extends AbstractPebbleIT {
@Test
public void testOrder() throws AcmeException, IOException {
KeyPair keyPair = createKeyPair();
Session session = new Session(pebbleURI(), keyPair);
Registration reg = new RegistrationBuilder().agreeToTermsOfService().create(session);
byte[] csr = TestUtils.getResourceAsByteArray("/csr.der");
Instant notBefore = Instant.now();
Instant notAfter = notBefore.plus(Duration.ofDays(20L));
Order order = reg.orderCertificate(csr, notBefore, notAfter);
assertThat(order.getCsr(), is(csr));
assertThat(order.getNotBefore(), is(notBefore));
assertThat(order.getNotAfter(), is(notAfter));
assertThat(order.getStatus(), is(Status.PENDING));
for (Authorization auth : order.getAuthorizations()) {
processAuthorization(auth);
}
}
private void processAuthorization(Authorization auth) throws AcmeException {
assertThat(auth.getDomain(), is("example.com"));
if (auth.getStatus() == Status.PENDING) {
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
assertThat(challenge, is(notNullValue()));
challenge.trigger();
}
}
}

View File

@ -57,6 +57,7 @@ public class SessionIT extends AbstractPebbleIT {
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_NONCE));
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_REG));
assertIsPebbleUrl(session.resourceUrl(Resource.NEW_ORDER));
}
@Test

View File

@ -100,8 +100,7 @@ newAuthorizationResponse = \
"uri": "https://example.com/authz/asdf/1",\
"token": "DGyRejmCefe7v4NfDGDKfA"\
}\
],\
"combinations": [[0], [0,1]]\
]\
}
updateAuthorizationResponse = \
@ -125,8 +124,41 @@ updateAuthorizationResponse = \
"uri": "https://example.com/authz/asdf/1",\
"token": "DGyRejmCefe7v4NfDGDKfA"\
}\
]\
}
requestOrderRequest = \
{\
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",\
"notBefore": "2016-01-01T00:00:00Z",\
"notAfter": "2016-01-08T00:00:00Z"\
}
requestOrderResponse = \
{\
"status": "pending",\
"expires": "2016-01-01T00:00:00Z",\
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",\
"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"\
]\
}
updateOrderResponse = \
{\
"status": "pending",\
"expires": "2015-03-01T14:09:00Z",\
"csr": "MIIChDCCAWwCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPemmumcNGR0hsPo-2N6nkJ0FcEMdb0_MlucHR0dNeHEvn8vmcQHlYRjkDVX0aypnfKQI3tvhTBKLdlNvbVIW1TQ_Wbqh9TQlC8G3Hog8nRQ2vAzO4sH6nhvdrAFUmq6hkATpU3iQuDvtYu03ExaYHKsItLocl1OndaQizBn5udBv1baOW3Kd790k6lEWGrD-TXo6uwuMha2k_YBGNKd4S4UuPmbPV9SUVW8JSylBSgDhvY3BHv-dfdIMhVwRMZDFaa0mHDIYUiwcEaU5x4P6Q5bGP2wxcUPCLwFsbAK5K6B2T2P3A2fNjGBAlHwEkg6VMvi7jax8MD-oRnku2M2JLAgMBAAGgKTAnBgkqhkiG9w0BCQ4xGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQACnMZdjE1jVqnkHkEeGrMXujWuwuiKLZVa4YZ9fL0UIGOqqf4b9-3JmtEiLB9ycQO5N9rW4V-6_DBMeoeRBLu-wranHnxU4ds6GzNzBxKfI86_8t5pdQK4Cglv7yfseseZRdQtvcR2ejkW0F3SL1DF5Sk3T46aRYiUXxeoNC4Uh3zoIHOv8YGUa-DuZQ6OnHMhPrdsfU09L7KVAMTq1bodjGWmgoIJm4x5JSm19GbhYAm9Q9XWnN56YHqgS3FtS9n3wDxz7Dvo24whto1tUU5hnjrp31rTvyxG3kydoEZf2Ciq_82bQDb40kwnoO6RytPYJVMRIBsP2mCfaFtIt9Eb",\
"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"\
],\
"combinations": [[0], [0,1]]\
"certificate": "https://example.com/acme/cert/1234"\
}
triggerHttpChallenge = \
@ -207,9 +239,16 @@ authorizationChallenges = \
"type": "tls-sni-02",\
"uri": "https://example.com/authz/asdf/2",\
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ"\
},\
{\
"type": "duplicate-01",\
"uri": "https://example.com/authz/asdf/3"\
},\
{\
"type": "duplicate-01",\
"uri": "https://example.com/authz/asdf/4"\
}\
],\
"combinations": [[0], [1,2]]\
]\
}