mirror of https://github.com/shred/acme4j
Implement key-change as documented in draft-04
parent
22961b3fba
commit
66956e5587
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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<String, Object> claimMap = claims.toMap();
|
||||
assertThat(claimMap.get("resource"), is((Object) "reg"));
|
||||
assertThat(claimMap.get("rollover"), not(nullValue()));
|
||||
|
||||
public int sendSignedRequest(URI uri, ClaimBuilder payload, Session session) {
|
||||
try {
|
||||
assertThat(uri, is(locationUri));
|
||||
assertThat(session, is(notNullValue()));
|
||||
assertThat(session.getKeyPair(), is(sameInstance(oldKeyPair)));
|
||||
|
||||
Map<String, Object> json = payload.toMap();
|
||||
assertThat((String) json.get("resource"), is("key-change")); // Required by Let's Encrypt
|
||||
|
||||
String encodedHeader = (String) json.get("protected");
|
||||
String encodedSignature = (String) json.get("signature");
|
||||
String encodedPayload = (String) json.get("payload");
|
||||
|
||||
String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature);
|
||||
JsonWebSignature jws = new JsonWebSignature();
|
||||
jws.setCompactSerialization(serialized);
|
||||
jws.setKey(newKeyPair.getPublic());
|
||||
assertThat(jws.verifySignature(), is(true));
|
||||
|
||||
String decodedPayload = jws.getPayload();
|
||||
|
||||
StringBuilder expectedPayload = new StringBuilder();
|
||||
expectedPayload.append('{');
|
||||
expectedPayload.append("\"resource\":\"reg\",");
|
||||
expectedPayload.append("\"newKey\":\"").append(TestUtils.D_THUMBPRINT).append("\"");
|
||||
expectedPayload.append("}");
|
||||
|
||||
String rollover = (String) claimMap.get("rollover");
|
||||
JsonWebSignature jws = (JsonWebSignature) JsonWebSignature.fromCompactSerialization(rollover);
|
||||
jws.setKey(oldKeyPair.getPublic()); // signed with the old KeyPair!
|
||||
assertThat(jws.getPayload(), sameJSONAs(expectedPayload.toString()));
|
||||
expectedPayload.append("\"account\":\"").append(resourceUri).append("\",");
|
||||
expectedPayload.append("\"newKey\":{");
|
||||
expectedPayload.append("\"kty\":\"").append(TestUtils.D_KTY).append("\",");
|
||||
expectedPayload.append("\"e\":\"").append(TestUtils.D_E).append("\",");
|
||||
expectedPayload.append("\"n\":\"").append(TestUtils.D_N).append("\"");
|
||||
expectedPayload.append("}}");
|
||||
assertThat(decodedPayload, sameJSONAs(expectedPayload.toString()));
|
||||
} catch (JoseException ex) {
|
||||
throw new AcmeProtocolException("Bad rollover", ex);
|
||||
fail("decoding inner payload failed");
|
||||
}
|
||||
|
||||
return HttpURLConnection.HTTP_OK;
|
||||
|
@ -382,10 +392,12 @@ public class RegistrationTest {
|
|||
|
||||
@Override
|
||||
public URI getLocation() {
|
||||
return locationUri;
|
||||
return resourceUri;
|
||||
}
|
||||
};
|
||||
|
||||
provider.putTestResource(Resource.KEY_CHANGE, locationUri);
|
||||
|
||||
Session session = new Session(new URI(TestUtils.ACME_SERVER_URI), oldKeyPair) {
|
||||
@Override
|
||||
public AcmeProvider provider() {
|
||||
|
@ -393,8 +405,12 @@ public class RegistrationTest {
|
|||
};
|
||||
};
|
||||
|
||||
Registration registration = new Registration(session, locationUri);
|
||||
assertThat(session.getKeyPair(), is(sameInstance(oldKeyPair)));
|
||||
|
||||
Registration registration = new Registration(session, resourceUri);
|
||||
registration.changeKey(newKeyPair);
|
||||
|
||||
assertThat(session.getKeyPair(), is(sameInstance(newKeyPair)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,13 +28,14 @@ public class ResourceTest {
|
|||
*/
|
||||
@Test
|
||||
public void testPath() {
|
||||
assertThat(Resource.KEY_CHANGE.path(), is("key-change"));
|
||||
assertThat(Resource.NEW_AUTHZ.path(), is("new-authz"));
|
||||
assertThat(Resource.NEW_CERT.path(), is("new-cert"));
|
||||
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(4));
|
||||
assertThat(Resource.values().length, is(5));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -42,8 +42,6 @@ registration.modify()
|
|||
|
||||
## Account Key Roll-Over
|
||||
|
||||
> **CAUTION**: Account Key Roll-Over is currently not supported by _Let's Encrypt_. It silently ignores your new key, and gives you the fatal impression that you can safely dispose your old key after that.
|
||||
|
||||
It is also possible to change the key pair that is associated with your account, for example if you suspect that your key has been compromised.
|
||||
|
||||
The following example changes the key pair:
|
||||
|
@ -54,7 +52,9 @@ KeyPair newKeyPair = ... // new KeyPair to be used
|
|||
registration.changeKey(newKeyPair);
|
||||
```
|
||||
|
||||
All subsequent calls must now use the new key pair. The old key pair can be disposed. The key is automatically updated on the `Session` that was bound to the `Registration`.
|
||||
After a successful change, all subsequent calls related to this account must use the new key pair. The key is automatically updated on the `Session` that was bound to this `Registration`.
|
||||
|
||||
The old key pair can be disposed of after that. However, I recommend to keep a backup of the old key pair until the key change was proven to be successful, by making a subsequent call with the new key pair. Otherwise you might lock yourself out from your account if the key change should have failed silently, for whatever reason.
|
||||
|
||||
## Deactivate an Account
|
||||
|
||||
|
@ -66,4 +66,4 @@ registration.deactivate();
|
|||
|
||||
Depending on the CA, the related authorizations may be automatically deactivated as well. The certificates may still be valid until expiration or explicit revocation. If you want to make sure the certificates are invalidated as well, revoke them prior to deactivation of your account.
|
||||
|
||||
There is no way to reactivate the account once it is deactivated!
|
||||
Be very careful: There is no way to reactivate the account once it is deactivated!
|
||||
|
|
Loading…
Reference in New Issue