mirror of https://github.com/shred/acme4j
Add Account Key Roll-over
parent
d7adc5d486
commit
d4a8d449c9
|
@ -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}.
|
||||
* <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
|
||||
* recovery process by using one of the contact addresses given at account creation.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue