Merge master v0.10 into draft

pull/55/head
Richard Körber 2017-04-15 17:37:25 +02:00
commit 698d25fd14
11 changed files with 150 additions and 56 deletions

View File

@ -1,6 +1,6 @@
# ACME Java Client ![build status](https://shredzone.org/badge/draft/acme4j.svg)
This is a Java client for the [Automatic Certificate Management Environment (ACME)](https://tools.ietf.org/html/draft-ietf-acme-acme-04) protocol.
This is a Java client for the [Automatic Certificate Management Environment (ACME)](https://tools.ietf.org/html/draft-ietf-acme-acme-06) protocol.
ACME is a protocol that a certificate authority (CA) and an applicant can use to automate the process of verification and certificate issuance.
@ -40,3 +40,4 @@ _acme4j_ is open source software. The source code is distributed under the terms
## Acknowledgements
* I would like to thank Brian Campbell and all the other [jose4j](https://bitbucket.org/b_c/jose4j/wiki/Home) developers. _acme4j_ would not exist without your excellent work.
* I also like to thank everyone who contributed to _acme4j_.

View File

@ -20,7 +20,7 @@
<parent>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j</artifactId>
<version>0.10-SNAPSHOT</version>
<version>0.11-SNAPSHOT</version>
</parent>
<artifactId>acme4j-client</artifactId>

View File

@ -240,7 +240,11 @@ public class Registration extends AcmeResource {
X509Certificate cert = null;
if (rc == HttpURLConnection.HTTP_CREATED) {
cert = conn.readCertificate();
try {
cert = conn.readCertificate();
} catch (AcmeProtocolException ex) {
LOG.warn("Could not parse attached certificate", ex);
}
}
URI chainCertUri = conn.getLink("up");

View File

@ -17,13 +17,13 @@ import java.net.URI;
import java.security.KeyPair;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.StreamSupport;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.TokenChallenge;
@ -40,15 +40,15 @@ import org.shredzone.acme4j.util.JSON;
* volatile data.
*/
public class Session {
private final Map<Resource, URI> resourceMap = new EnumMap<>(Resource.class);
private final AtomicReference<Map<Resource, URI>> resourceMap = new AtomicReference<>();
private final AtomicReference<Metadata> metadata = new AtomicReference<>();
private final URI serverUri;
private final AcmeProvider provider;
private KeyPair keyPair;
private URI keyIdentifier;
private AcmeProvider provider;
private byte[] nonce;
private JSON directoryJson;
private Metadata metadata;
private Locale locale = Locale.getDefault();
protected Instant directoryCacheExpiry;
@ -71,10 +71,25 @@ public class Session {
* {@link URI} of the ACME server
* @param keyPair
* {@link KeyPair} of the ACME account
* @throws IllegalArgumentException
* if no ACME provider was found for the server URI.
*/
public Session(URI serverUri, KeyPair keyPair) {
this.serverUri = Objects.requireNonNull(serverUri, "serverUri");
this.keyPair = Objects.requireNonNull(keyPair, "keyPair");
final URI localServerUri = serverUri;
Iterable<AcmeProvider> providers = ServiceLoader.load(AcmeProvider.class);
provider = StreamSupport.stream(providers.spliterator(), false)
.filter(p -> p.accepts(localServerUri))
.reduce((a, b) -> {
throw new IllegalArgumentException("Both ACME providers "
+ a.getClass().getSimpleName() + " and "
+ b.getClass().getSimpleName() + " accept "
+ localServerUri + ". Please check your classpath.");
})
.orElseThrow(() -> new IllegalArgumentException("No ACME provider found for " + localServerUri));
}
/**
@ -143,32 +158,10 @@ public class Session {
/**
* Returns the {@link AcmeProvider} that is used for this session.
* <p>
* The {@link AcmeProvider} instance is lazily created and cached.
*
* @return {@link AcmeProvider}
*/
public AcmeProvider provider() {
synchronized (this) {
if (provider == null) {
List<AcmeProvider> candidates = new ArrayList<>();
for (AcmeProvider acp : ServiceLoader.load(AcmeProvider.class)) {
if (acp.accepts(serverUri)) {
candidates.add(acp);
}
}
if (candidates.isEmpty()) {
throw new IllegalArgumentException("No ACME provider found for " + serverUri);
} else if (candidates.size() > 1) {
throw new IllegalStateException("There are " + candidates.size() + " "
+ AcmeProvider.class.getSimpleName() + " accepting " + serverUri
+ ". Please check your classpath.");
} else {
provider = candidates.get(0);
}
}
}
return provider;
}
@ -206,7 +199,7 @@ public class Session {
*/
public URI resourceUri(Resource resource) throws AcmeException {
readDirectory();
return resourceMap.get(Objects.requireNonNull(resource, "resource"));
return resourceMap.get().get(Objects.requireNonNull(resource, "resource"));
}
/**
@ -217,7 +210,7 @@ public class Session {
*/
public Metadata getMetadata() throws AcmeException {
readDirectory();
return metadata;
return metadata.get();
}
/**
@ -227,26 +220,28 @@ public class Session {
private void readDirectory() throws AcmeException {
synchronized (this) {
Instant now = Instant.now();
if (directoryJson == null || !directoryCacheExpiry.isAfter(now)) {
directoryJson = provider().directory(this, getServerUri());
directoryCacheExpiry = now.plus(Duration.ofHours(1));
if (directoryJson != null && directoryCacheExpiry.isAfter(now)) {
return;
}
directoryJson = provider().directory(this, getServerUri());
directoryCacheExpiry = now.plus(Duration.ofHours(1));
}
JSON meta = directoryJson.get("meta").asObject();
if (meta != null) {
metadata = new Metadata(meta);
} else {
metadata = new Metadata(JSON.empty());
}
JSON meta = directoryJson.get("meta").asObject();
if (meta != null) {
metadata.set(new Metadata(meta));
} else {
metadata.set(new Metadata(JSON.empty()));
}
resourceMap.clear();
for (Resource res : Resource.values()) {
URI uri = directoryJson.get(res.path()).asURI();
if (uri != null) {
resourceMap.put(res, uri);
}
}
Map<Resource, URI> map = new EnumMap<>(Resource.class);
for (Resource res : Resource.values()) {
URI uri = directoryJson.get(res.path()).asURI();
if (uri != null) {
map.put(res, uri);
}
}
resourceMap.set(map);
}
}

View File

@ -39,6 +39,7 @@ import org.shredzone.acme4j.challenge.Dns01Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.provider.AcmeProvider;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.util.JSON;
@ -399,6 +400,62 @@ public class RegistrationTest {
provider.close();
}
/**
* Test that an unparseable certificate can be requested, and at least its location
* is made available.
*/
@Test
public void testRequestCertificateBrokenSync() throws AcmeException, IOException {
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendSignedRequest(URI uri, JSONBuilder claims, Session session) {
assertThat(uri, is(resourceUri));
assertThat(claims.toString(), sameJSONAs(getJson("requestCertificateRequestWithDate")));
assertThat(session, is(notNullValue()));
}
@Override
public int accept(int... httpStatus) throws AcmeException {
assertThat(httpStatus, isIntArrayContainingInAnyOrder(
HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_ACCEPTED));
return HttpURLConnection.HTTP_CREATED;
}
@Override
public X509Certificate readCertificate() {
throw new AcmeProtocolException("Failed to read certificate");
}
@Override
public URI getLink(String relation) {
switch(relation) {
case "up": return chainUri;
default: return null;
}
}
@Override
public URI getLocation() {
return locationUri;
}
};
provider.putTestResource(Resource.NEW_CERT, resourceUri);
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(), locationUri);
Certificate cert = registration.requestCertificate(csr, notBefore, notAfter);
assertThat(cert.getLocation(), is(locationUri));
assertThat(cert.getChainLocation(), is(chainUri));
provider.close();
}
/**
* Test that the account key can be changed.
*/

View File

@ -775,4 +775,22 @@ public class DefaultConnectionTest {
verifyNoMoreInteractions(mockUrlConnection);
}
/**
* Test that a bad certificate throws an exception.
*/
@Test(expected = AcmeProtocolException.class)
public void testReadBadCertificate() throws Exception {
X509Certificate original = TestUtils.createCertificate();
byte[] badCert = original.getEncoded();
Arrays.sort(badCert); // break it
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/pkix-cert");
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(badCert));
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
conn.conn = mockUrlConnection;
conn.readCertificate();
}
}
}

View File

@ -71,9 +71,9 @@ public class SessionProviderTest {
/**
* There are two testing providers accepting {@code acme://example.net}. Test that
* connecting to this URI will result in an {@link IllegalStateException}.
* connecting to this URI will result in an {@link IllegalArgumentException}.
*/
@Test(expected = IllegalStateException.class)
@Test(expected = IllegalArgumentException.class)
public void testDuplicate() throws Exception {
new Session(new URI("acme://example.net"), keyPair).provider();
}

View File

@ -20,7 +20,7 @@
<parent>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j</artifactId>
<version>0.10-SNAPSHOT</version>
<version>0.11-SNAPSHOT</version>
</parent>
<artifactId>acme4j-example</artifactId>

View File

@ -20,7 +20,7 @@
<parent>
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j</artifactId>
<version>0.10-SNAPSHOT</version>
<version>0.11-SNAPSHOT</version>
</parent>
<artifactId>acme4j-utils</artifactId>

21
pom.xml
View File

@ -19,7 +19,7 @@
<groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j</artifactId>
<version>0.10-SNAPSHOT</version>
<version>0.11-SNAPSHOT</version>
<packaging>pom</packaging>
<name>acme4j</name>
@ -51,9 +51,9 @@
</developers>
<properties>
<bouncycastle.version>1.55</bouncycastle.version>
<jose4j.version>0.5.3</jose4j.version>
<slf4j.version>1.7.21</slf4j.version>
<bouncycastle.version>1.56</bouncycastle.version>
<jose4j.version>0.5.5</jose4j.version>
<slf4j.version>1.7.25</slf4j.version>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
<skipITs>true</skipITs>
</properties>
@ -134,6 +134,19 @@
<localCheckout>true</localCheckout>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<reporting>

View File

@ -2,6 +2,12 @@
This document will help you migrate your code to the latest _acme4j_ version.
## Migration to Version 0.10
Starting with version 0.10, _acme4j_ requires Java 8 or higher. This is also reflected in the API.
The most noticeable change is that the old `java.util.Date` has been replaced by the new `java.time` API in the entire project. If you don't want to migrate your code to the new API, you can use `Date.from()` and `Date.toInstant()` to convert between the different date objects, where necessary.
## Migration to Version 0.9
Version 0.9 brought many changes to the internal API. However, this is only relevant if you run your own CA and make own extensions to _acme4j_ (e.g. if you implement a proprietary `Challenge`). If you use _acme4j_ only for retrieving certificates, you should not notice any changes.