mirror of https://github.com/shred/acme4j
Add Account Key Roll-over
parent
d7adc5d486
commit
d4a8d449c9
|
@ -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.
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue