diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java index 81edc599..3ac3f6de 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java @@ -14,6 +14,7 @@ package org.shredzone.acme4j; import java.net.URI; +import java.security.KeyPair; import java.security.cert.X509Certificate; import org.shredzone.acme4j.challenge.Challenge; @@ -49,6 +50,23 @@ public interface AcmeClient { */ void modifyRegistration(Account account, Registration registration) throws AcmeException; + /** + * Modifies the account so it is identified with the new {@link KeyPair}. + *
+ * Starting from the next call, {@link Account} must use the new {@link KeyPair} for
+ * identification.
+ *
+ * @param account
+ * {@link Account} to change the identification key pair of
+ * @param registration
+ * {@link Registration} containing the account location URI. Other
+ * properties are ignored.
+ * @param newKeyPair
+ * new {@link KeyPair} to be used for identifying this account
+ */
+ void changeRegistrationKey(Account account, Registration registration, KeyPair newKeyPair)
+ throws AcmeException;
+
/**
* Recovers an account by contact-based recovery. The server starts an out-of-band
* recovery process by using one of the contact addresses given at account creation.
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java
index 65366982..571764d5 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/impl/AbstractAcmeClient.java
@@ -15,13 +15,18 @@ package org.shredzone.acme4j.impl;
import java.net.HttpURLConnection;
import java.net.URI;
+import java.security.KeyPair;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import org.jose4j.jws.AlgorithmIdentifiers;
+import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.Authorization;
@@ -166,6 +171,56 @@ public abstract class AbstractAcmeClient implements AcmeClient {
}
}
+ @Override
+ public void changeRegistrationKey(Account account, Registration registration, KeyPair newKeyPair)
+ throws AcmeException {
+ if (account == null) {
+ throw new NullPointerException("account must not be null");
+ }
+ if (registration == null) {
+ throw new NullPointerException("registration must not be null");
+ }
+ if (registration.getLocation() == null) {
+ throw new IllegalArgumentException("registration location must not be null. Use newRegistration() if not known.");
+ }
+ if (newKeyPair == null) {
+ throw new NullPointerException("newKeyPair must not be null");
+ }
+ if (Arrays.equals(account.getKeyPair().getPrivate().getEncoded(),
+ newKeyPair.getPrivate().getEncoded())) {
+ throw new IllegalArgumentException("newKeyPair must actually be a new key pair");
+ }
+
+ String newKey;
+ try {
+ ClaimBuilder oldKeyClaim = new ClaimBuilder();
+ oldKeyClaim.putResource("reg");
+ oldKeyClaim.putKey("oldKey", account.getKeyPair().getPublic());
+
+ JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(oldKeyClaim.toString());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
+ jws.setKey(newKeyPair.getPrivate());
+ jws.sign();
+
+ newKey = jws.getCompactSerialization();
+ } catch (JoseException ex) {
+ throw new IllegalArgumentException("Bad newKeyPair", ex);
+ }
+
+ LOG.debug("changeRegistrationKey");
+ try (Connection conn = createConnection()) {
+ ClaimBuilder claims = new ClaimBuilder();
+ claims.putResource("reg");
+ claims.put("newKey", newKey);
+
+ int rc = conn.sendSignedRequest(registration.getLocation(), claims, session, account);
+ if (rc != HttpURLConnection.HTTP_ACCEPTED) {
+ conn.throwAcmeException();
+ }
+ }
+ }
+
@Override
public void recoverRegistration(Account account, Registration registration) throws AcmeException {
if (account == null) {
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java
index 697898a4..74d971d9 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/impl/AbstractAcmeClientTest.java
@@ -22,10 +22,13 @@ import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
+import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
+import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.lang.JoseException;
import org.junit.Before;
import org.junit.Test;
import org.shredzone.acme4j.Account;
@@ -149,6 +152,72 @@ public class AbstractAcmeClientTest {
assertThat(registration.getAgreement(), is(agreementUri));
}
+ /**
+ * Test that the account key can be changed.
+ */
+ @Test
+ public void testChangeRegistrationKey() throws AcmeException, IOException {
+ Registration registration = new Registration();
+ registration.setLocation(locationUri);
+
+ final KeyPair newKeyPair = TestUtils.createDomainKeyPair();
+
+ Connection connection = new DummyConnection() {
+ @Override
+ public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Account account) throws AcmeException {
+ Map