From d4a8d449c9a4fd534be06fec1231b665dcfba1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sat, 16 Jan 2016 16:23:01 +0100 Subject: [PATCH] Add Account Key Roll-over --- .../java/org/shredzone/acme4j/AcmeClient.java | 18 +++++ .../acme4j/impl/AbstractAcmeClient.java | 55 +++++++++++++++ .../acme4j/impl/AbstractAcmeClientTest.java | 69 +++++++++++++++++++ src/site/markdown/usage/register.md | 17 +++++ 4 files changed, 159 insertions(+) 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 claimMap = claims.toMap(); + assertThat(claimMap.get("resource"), is((Object) "reg")); + assertThat(claimMap.get("newKey"), not(nullValue())); + + try { + StringBuilder expectedPayload = new StringBuilder(); + expectedPayload.append('{'); + expectedPayload.append("\"resource\":\"reg\","); + expectedPayload.append("\"oldKey\":{"); + expectedPayload.append("\"kty\":\"").append(TestUtils.KTY).append("\","); + expectedPayload.append("\"e\":\"").append(TestUtils.E).append("\","); + expectedPayload.append("\"n\":\"").append(TestUtils.N).append("\""); + expectedPayload.append("}}"); + + String newKey = (String) claimMap.get("newKey"); + JsonWebSignature jws = (JsonWebSignature) JsonWebSignature.fromCompactSerialization(newKey); + jws.setKey(newKeyPair.getPublic()); + assertThat(jws.getPayload(), sameJSONAs(expectedPayload.toString())); + } catch (JoseException ex) { + throw new AcmeException("Bad newKey", ex); + } + + assertThat(uri, is(locationUri)); + assertThat(session, is(notNullValue())); + assertThat(account, is(sameInstance(testAccount))); + return HttpURLConnection.HTTP_ACCEPTED; + } + + @Override + public URI getLocation() throws AcmeException { + return locationUri; + } + }; + + TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection); + + client.changeRegistrationKey(testAccount, registration, newKeyPair); + } + + /** + * Test that the same account key is not accepted for change + */ + @Test(expected = IllegalArgumentException.class) + public void testChangeRegistrationSameKey() throws AcmeException, IOException { + Registration registration = new Registration(); + registration.setLocation(locationUri); + + Connection connection = new DummyConnection(); + TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection); + + client.changeRegistrationKey(testAccount, registration, testAccount.getKeyPair()); + } + /** * Test that a {@link Registration} can be recovered by contact-based recovery. */ diff --git a/src/site/markdown/usage/register.md b/src/site/markdown/usage/register.md index bba181d7..0288864d 100644 --- a/src/site/markdown/usage/register.md +++ b/src/site/markdown/usage/register.md @@ -39,3 +39,20 @@ reg.setAgreement(agreementUri); client.modifyRegistration(account, reg); ``` + +## Account Key Roll-Over + +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: + +```java +Registration reg = new Registration(); +reg.setLocation(accountLocationUri); + +KeyPair newKeyPair = ... // new KeyPair to be used + +client.changeRegistrationKey(account, reg, newKeyPair); +``` + +All subsequent calls must now use an `Account` instance with the new key. The old key can be disposed.