Add Account Key Roll-over

pull/17/merge
Richard Körber 2016-01-16 16:23:01 +01:00
parent d7adc5d486
commit d4a8d449c9
4 changed files with 159 additions and 0 deletions

View File

@ -14,6 +14,7 @@
package org.shredzone.acme4j; package org.shredzone.acme4j;
import java.net.URI; import java.net.URI;
import java.security.KeyPair;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.Challenge;
@ -49,6 +50,23 @@ public interface AcmeClient {
*/ */
void modifyRegistration(Account account, Registration registration) throws AcmeException; void modifyRegistration(Account account, Registration registration) throws AcmeException;
/**
* Modifies the account so it is identified with the new {@link KeyPair}.
* <p>
* 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 * 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. * recovery process by using one of the contact addresses given at account creation.

View File

@ -15,13 +15,18 @@ package org.shredzone.acme4j.impl;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.security.KeyPair;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; 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.Account;
import org.shredzone.acme4j.AcmeClient; import org.shredzone.acme4j.AcmeClient;
import org.shredzone.acme4j.Authorization; 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 @Override
public void recoverRegistration(Account account, Registration registration) throws AcmeException { public void recoverRegistration(Account account, Registration registration) throws AcmeException {
if (account == null) { if (account == null) {

View File

@ -22,10 +22,13 @@ import java.io.IOException;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.security.KeyPair;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.shredzone.acme4j.Account; import org.shredzone.acme4j.Account;
@ -149,6 +152,72 @@ public class AbstractAcmeClientTest {
assertThat(registration.getAgreement(), is(agreementUri)); 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<String, Object> 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. * Test that a {@link Registration} can be recovered by contact-based recovery.
*/ */

View File

@ -39,3 +39,20 @@ reg.setAgreement(agreementUri);
client.modifyRegistration(account, reg); 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.