diff --git a/README.md b/README.md index 11035d57..59c7bbb6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ACME Java Client  -This is a Java client for the [Automatic Certificate Management Environment (ACME)](https://tools.ietf.org/html/draft-ietf-acme-acme-01) protocol. +This is a Java client for the [Automatic Certificate Management Environment (ACME)](https://tools.ietf.org/html/draft-ietf-acme-acme-04) protocol. ACME is a protocol that a certificate authority (CA) and an applicant can use to automate the process of verification and certificate issuance. @@ -10,7 +10,7 @@ It is an independent open source implementation that is not affiliated with or e ## Features -* Supports ACME protocol up to [draft 02](https://tools.ietf.org/html/draft-ietf-acme-acme-02), with a few parts of [draft 03](https://tools.ietf.org/html/draft-ietf-acme-acme-03) +* Supports ACME protocol up to [draft 02](https://tools.ietf.org/html/draft-ietf-acme-acme-02), with a few parts of [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) * Easy to use Java API * Requires JRE 7 or higher * Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22) @@ -24,7 +24,7 @@ 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). The latest [draft 03](https://tools.ietf.org/html/draft-ietf-acme-acme-03) is partially supported. The missing parts are likely to be removed in the next draft. +_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. 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. 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 6ecdbd51..579283d3 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java @@ -274,8 +274,10 @@ public class Registration extends AcmeResource { } /** - * Changes the {@link KeyPair} associated with the registration. After a successful - * call, the new key pair is used, and the old key pair can be disposed. + * Changes the {@link KeyPair} associated with the registration. + *
+ * After a successful call, the new key pair is used in the bound {@link Session},
+ * and the old key pair can be disposed of.
*
* @param newKeyPair
* new {@link KeyPair} to be used for identifying this account
@@ -289,41 +291,40 @@ public class Registration extends AcmeResource {
throw new IllegalArgumentException("newKeyPair must actually be a new key pair");
}
- LOG.debug("changeKey");
-
- String rollover;
- try {
- final PublicJsonWebKey oldKeyJwk = PublicJsonWebKey.Factory.newPublicJwk(getSession().getKeyPair().getPublic());
- final PublicJsonWebKey newKeyJwk = PublicJsonWebKey.Factory.newPublicJwk(newKeyPair.getPublic());
-
- ClaimBuilder newKeyClaim = new ClaimBuilder();
- newKeyClaim.putResource("reg");
- newKeyClaim.putBase64("newKey", newKeyJwk.calculateThumbprint("SHA-256"));
-
- JsonWebSignature jws = new JsonWebSignature();
- jws.setPayload(newKeyClaim.toString());
- jws.getHeaders().setJwkHeaderValue("jwk", oldKeyJwk);
- jws.setAlgorithmHeaderValue(SignatureUtils.keyAlgorithm(oldKeyJwk));
- jws.setKey(getSession().getKeyPair().getPrivate());
- jws.sign();
-
- rollover = jws.getCompactSerialization();
- } catch (JoseException ex) {
- throw new AcmeProtocolException("Cannot sign newKey", ex);
- }
+ LOG.debug("key-change");
try (Connection conn = getSession().provider().connect()) {
- ClaimBuilder claims = new ClaimBuilder();
- claims.putResource("reg");
- claims.put("rollover", rollover);
+ URI keyChangeUri = getSession().resourceUri(Resource.KEY_CHANGE);
+ PublicJsonWebKey newKeyJwk = PublicJsonWebKey.Factory.newPublicJwk(newKeyPair.getPublic());
- getSession().setKeyPair(newKeyPair);
- int rc = conn.sendSignedRequest(getLocation(), claims, getSession());
+ ClaimBuilder payloadClaim = new ClaimBuilder();
+ payloadClaim.put("account", getLocation());
+ payloadClaim.putKey("newKey", newKeyPair.getPublic());
+
+ JsonWebSignature innerJws = new JsonWebSignature();
+ innerJws.setPayload(payloadClaim.toString());
+ innerJws.getHeaders().setObjectHeaderValue("url", keyChangeUri);
+ innerJws.getHeaders().setJwkHeaderValue("jwk", newKeyJwk);
+ innerJws.setAlgorithmHeaderValue(SignatureUtils.keyAlgorithm(newKeyJwk));
+ innerJws.setKey(newKeyPair.getPrivate());
+ innerJws.sign();
+
+ ClaimBuilder outerClaim = new ClaimBuilder();
+ outerClaim.putResource(Resource.KEY_CHANGE); // Let's Encrypt needs the resource here
+ outerClaim.put("protected", innerJws.getHeaders().getEncodedHeader());
+ outerClaim.put("signature", innerJws.getEncodedSignature());
+ outerClaim.put("payload", innerJws.getEncodedPayload());
+
+ int rc = conn.sendSignedRequest(keyChangeUri, outerClaim, getSession());
if (rc != HttpURLConnection.HTTP_OK) {
conn.throwAcmeException();
}
+
+ getSession().setKeyPair(newKeyPair);
} catch (IOException ex) {
throw new AcmeNetworkException(ex);
+ } catch (JoseException ex) {
+ throw new AcmeProtocolException("Cannot sign key-change", ex);
}
}
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java
index 25499e78..f32d0b69 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java
@@ -18,6 +18,7 @@ package org.shredzone.acme4j.connector;
*/
public enum Resource {
+ KEY_CHANGE("key-change"),
NEW_REG("new-reg"),
NEW_AUTHZ("new-authz"),
NEW_CERT("new-cert"),
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 110e88cb..928e6fa8 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java
@@ -32,6 +32,7 @@ import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.jwx.CompactSerializer;
import org.jose4j.lang.JoseException;
import org.junit.Test;
import org.shredzone.acme4j.challenge.Challenge;
@@ -39,7 +40,6 @@ 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.ClaimBuilder;
@@ -353,28 +353,38 @@ public class RegistrationTest {
final TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
- public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session) {
- assertThat(uri, is(locationUri));
- assertThat(session, is(notNullValue()));
- assertThat(session.getKeyPair(), is(sameInstance(newKeyPair))); // registration has new KeyPair!
-
- Map