From c7cd984a24d61aa89edfb65b5f71e0d78f5a52f0 Mon Sep 17 00:00:00 2001 From: Jonas Berlin Date: Sun, 2 Apr 2017 23:21:38 +0300 Subject: [PATCH 01/10] Generate source jars --- pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pom.xml b/pom.xml index c3ea136d..54cf0eb5 100644 --- a/pom.xml +++ b/pom.xml @@ -120,6 +120,18 @@ true + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + From 9900baa53ce7603d027975e8d153fd9ab3fb0846 Mon Sep 17 00:00:00 2001 From: Jonas Berlin Date: Mon, 3 Apr 2017 07:56:31 +0300 Subject: [PATCH 02/10] Add maven-source-plugin version --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 54cf0eb5..50bcb0e0 100644 --- a/pom.xml +++ b/pom.xml @@ -123,6 +123,7 @@ org.apache.maven.plugins maven-source-plugin + 3.0.1 attach-sources From e50c5797d4398a4208c0d3b5cf2ca27087ac8b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 12:03:33 +0200 Subject: [PATCH 03/10] Update dependencies --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 50bcb0e0..4ed843db 100644 --- a/pom.xml +++ b/pom.xml @@ -51,9 +51,9 @@ - 1.55 - 0.5.3 - 1.7.21 + 1.56 + 0.5.5 + 1.7.25 utf-8 From 08eaa61f758732c37a0aaf1df37674817127745a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 12:03:41 +0200 Subject: [PATCH 04/10] No need to lazy-load provider in session --- .../java/org/shredzone/acme4j/Session.java | 42 ++++++++----------- .../acme4j/connector/SessionProviderTest.java | 4 +- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java index f1424e8d..9ac23c19 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java @@ -17,13 +17,12 @@ 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.stream.StreamSupport; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.TokenChallenge; @@ -42,9 +41,9 @@ import org.shredzone.acme4j.util.JSON; public class Session { private final Map resourceMap = new EnumMap<>(Resource.class); private final URI serverUri; + private final AcmeProvider provider; private KeyPair keyPair; - private AcmeProvider provider; private byte[] nonce; private JSON directoryJson; private Metadata metadata; @@ -70,10 +69,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 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)); } /** @@ -128,32 +142,10 @@ public class Session { /** * Returns the {@link AcmeProvider} that is used for this session. - *

- * The {@link AcmeProvider} instance is lazily created and cached. * * @return {@link AcmeProvider} */ public AcmeProvider provider() { - synchronized (this) { - if (provider == null) { - List 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; } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java index 73a8e2ec..ae66b5d2 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java @@ -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(); } From 1f6d8aea0b5dfd02382cd777cc7c2ca86a9d77ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 12:03:50 +0200 Subject: [PATCH 05/10] Fix potential race condition when threads share a session --- .../java/org/shredzone/acme4j/Session.java | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java index 9ac23c19..e0843107 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java @@ -22,6 +22,7 @@ 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; @@ -39,14 +40,14 @@ import org.shredzone.acme4j.util.JSON; * volatile data. */ public class Session { - private final Map resourceMap = new EnumMap<>(Resource.class); + private final AtomicReference> resourceMap = new AtomicReference<>(); + private final AtomicReference metadata = new AtomicReference<>(); private final URI serverUri; private final AcmeProvider provider; private KeyPair keyPair; private byte[] nonce; private JSON directoryJson; - private Metadata metadata; private Locale locale = Locale.getDefault(); protected Instant directoryCacheExpiry; @@ -183,7 +184,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")); } /** @@ -194,7 +195,7 @@ public class Session { */ public Metadata getMetadata() throws AcmeException { readDirectory(); - return metadata; + return metadata.get(); } /** @@ -204,26 +205,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 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); } } From f3dc04f2ea491676a4009ce2037ae6e734d89b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 11:25:19 +0200 Subject: [PATCH 06/10] Certificate URL is available even on unparseable certs --- .../org/shredzone/acme4j/Registration.java | 6 +- .../shredzone/acme4j/RegistrationTest.java | 57 +++++++++++++++++++ .../connector/DefaultConnectionTest.java | 18 ++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java index e2547827..dc50897e 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java @@ -245,7 +245,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"); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java index a0624eb0..b4508b74 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java @@ -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; @@ -397,6 +398,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. */ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java index 2d42e9f0..a1ab6e94 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java @@ -630,4 +630,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(); + } + } + } From a903bec135277740c4a741da889d8b07e212ce24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 12:02:22 +0200 Subject: [PATCH 07/10] Update README, mention the draft branch --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 13fb0631..c862eacf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ACME Java Client ![build status](https://shredzone.org/badge/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. @@ -24,11 +24,9 @@ It is an independent open source implementation that is not affiliated with or e ## Compatibility -_acme4j_ supports all CAs that implement the ACME protocol up to [draft 02](https://tools.ietf.org/html/draft-ietf-acme-acme-02). [draft 03](https://tools.ietf.org/html/draft-ietf-acme-acme-03) and [draft 04](https://tools.ietf.org/html/draft-ietf-acme-acme-04) are partially supported. The missing parts are likely to be removed in the next draft, or are not yet supported by the _Let's Encrypt_ server. +This version of _acme4j_ is tailor-made for _Let's Encrypt_ and other CAs that use the [Boulder](https://github.com/letsencrypt/boulder) server. Boulder [diverges from the ACME specifications](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md), so _acme4j_ cannot yet be used against other servers that fully comply to the ACME specifications. -The most prominent ACME CA, _Let's Encrypt_, [diverges from the specifications](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md). Some parts of the _acme4j_ API may not work with _Let's Encrypt_. Also, the usage of deprecated API parts may be required. - -The _acme4j_ API may change as features are added to, and other features removed from the [ACME specifications](https://github.com/ietf-wg-acme/acme), because they are still work in progress. Also see the [_acme4j_ bug tracker](https://github.com/shred/acme4j/issues) for missing and untested features. +The latest [ACME specifications](https://github.com/ietf-wg-acme/acme) are being implemented in the ["draft" branch](https://github.com/shred/acme4j/tree/draft) of _acme4j_. ## Contribute @@ -42,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_. From 1d882fd0f59e1348bbde82903fe36afba4933d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 12:28:18 +0200 Subject: [PATCH 08/10] Migration guide to v0.10 --- src/site/markdown/migration.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/site/markdown/migration.md b/src/site/markdown/migration.md index 30dd0fab..ea06ee79 100644 --- a/src/site/markdown/migration.md +++ b/src/site/markdown/migration.md @@ -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. From 47827fb148426a558c1f7aea904d341a7c5ad943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 12:32:54 +0200 Subject: [PATCH 09/10] [maven-release-plugin] prepare release v0.10 --- acme4j-client/pom.xml | 2 +- acme4j-example/pom.xml | 2 +- acme4j-utils/pom.xml | 2 +- pom.xml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/acme4j-client/pom.xml b/acme4j-client/pom.xml index 90bc9a04..6e3e47c9 100644 --- a/acme4j-client/pom.xml +++ b/acme4j-client/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 0.10-SNAPSHOT + 0.10 acme4j-client diff --git a/acme4j-example/pom.xml b/acme4j-example/pom.xml index 21b95136..4d37217e 100644 --- a/acme4j-example/pom.xml +++ b/acme4j-example/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 0.10-SNAPSHOT + 0.10 acme4j-example diff --git a/acme4j-utils/pom.xml b/acme4j-utils/pom.xml index 41bba567..0a3845e1 100644 --- a/acme4j-utils/pom.xml +++ b/acme4j-utils/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 0.10-SNAPSHOT + 0.10 acme4j-utils diff --git a/pom.xml b/pom.xml index 4ed843db..bbe76c98 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ org.shredzone.acme4j acme4j - 0.10-SNAPSHOT + 0.10 pom acme4j @@ -37,7 +37,7 @@ https://github.com/shred/acme4j/ scm:git:git@github.com:shred/acme4j.git scm:git:git@github.com:shred/acme4j.git - HEAD + v0.10 GitHub From 30257d0529c3c123b3a1e1ca0471165905248b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 14 Apr 2017 12:32:55 +0200 Subject: [PATCH 10/10] [maven-release-plugin] prepare for next development iteration --- acme4j-client/pom.xml | 2 +- acme4j-example/pom.xml | 2 +- acme4j-utils/pom.xml | 2 +- pom.xml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/acme4j-client/pom.xml b/acme4j-client/pom.xml index 6e3e47c9..e4e9d74d 100644 --- a/acme4j-client/pom.xml +++ b/acme4j-client/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 0.10 + 0.11-SNAPSHOT acme4j-client diff --git a/acme4j-example/pom.xml b/acme4j-example/pom.xml index 4d37217e..e86e0f0e 100644 --- a/acme4j-example/pom.xml +++ b/acme4j-example/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 0.10 + 0.11-SNAPSHOT acme4j-example diff --git a/acme4j-utils/pom.xml b/acme4j-utils/pom.xml index 0a3845e1..7dea129c 100644 --- a/acme4j-utils/pom.xml +++ b/acme4j-utils/pom.xml @@ -20,7 +20,7 @@ org.shredzone.acme4j acme4j - 0.10 + 0.11-SNAPSHOT acme4j-utils diff --git a/pom.xml b/pom.xml index bbe76c98..ef9412fa 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ org.shredzone.acme4j acme4j - 0.10 + 0.11-SNAPSHOT pom acme4j @@ -37,7 +37,7 @@ https://github.com/shred/acme4j/ scm:git:git@github.com:shred/acme4j.git scm:git:git@github.com:shred/acme4j.git - v0.10 + HEAD GitHub