Add contact-based recovery

pull/17/merge
Richard Körber 2015-12-21 01:32:30 +01:00
parent 8168e6efc7
commit b00114ad88
10 changed files with 133 additions and 5 deletions

View File

@ -36,7 +36,7 @@ See the [online documentation](http://www.shredzone.org/maven/acme4j/) for how t
The following features are planned to be completed for the first beta release, but are still missing: The following features are planned to be completed for the first beta release, but are still missing:
* Support of account recovery. * Support of MAC-based account recovery.
* `proofOfPossession-01` and `tls-sni-01` challenge support. * `proofOfPossession-01` and `tls-sni-01` challenge support.
## License ## License

View File

@ -44,11 +44,23 @@ public interface AcmeClient {
* @param account * @param account
* {@link Account} that is registered * {@link Account} that is registered
* @param registration * @param registration
* {@link Registration} containing updated registration data. Set the * {@link Registration} containing updated registration data and the
* account location via {@link Registration#setLocation(URI)}! * account location URI
*/ */
void modifyRegistration(Account account, Registration registration) throws AcmeException; void modifyRegistration(Account account, Registration registration) 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.
*
* @param account
* <em>New</em> {@link Account} to associate the recovered account with
* @param registration
* {@link Registration}, with the account location URI set
* @throws AcmeException
*/
void recoverRegistration(Account account, Registration registration) throws AcmeException;
/** /**
* Creates a new {@link Authorization} for a domain. * Creates a new {@link Authorization} for a domain.
* *

View File

@ -23,7 +23,8 @@ public enum Resource {
NEW_REG("new-reg"), NEW_REG("new-reg"),
NEW_AUTHZ("new-authz"), NEW_AUTHZ("new-authz"),
NEW_CERT("new-cert"), NEW_CERT("new-cert"),
REVOKE_CERT("revoke-cert"); REVOKE_CERT("revoke-cert"),
RECOVER_REG("recover-reg");
/** /**
* Parses the string and returns a matching {@link Resource} instance. * Parses the string and returns a matching {@link Resource} instance.

View File

@ -162,6 +162,42 @@ public abstract class AbstractAcmeClient implements AcmeClient {
} }
} }
@Override
public void recoverRegistration(Account account, Registration registration) 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");
}
LOG.debug("recoverRegistration");
try (Connection conn = createConnection()) {
ClaimBuilder claims = new ClaimBuilder();
claims.putResource(Resource.RECOVER_REG);
claims.put("method", "contact");
claims.put("base", registration.getLocation());
if (!registration.getContacts().isEmpty()) {
claims.put("contact", registration.getContacts());
}
int rc = conn.sendSignedRequest(resourceUri(Resource.RECOVER_REG), claims, session, account);
if (rc != HttpURLConnection.HTTP_CREATED) {
conn.throwAcmeException();
}
registration.setLocation(conn.getLocation());
URI tos = conn.getLink("terms-of-service");
if (tos != null) {
registration.setAgreement(tos);
}
}
}
@Override @Override
public void newAuthorization(Account account, Authorization auth) throws AcmeException { public void newAuthorization(Account account, Authorization auth) throws AcmeException {
if (account == null) { if (account == null) {

View File

@ -34,9 +34,10 @@ public class ResourceTest {
assertThat(Resource.NEW_CERT.path(), is("new-cert")); assertThat(Resource.NEW_CERT.path(), is("new-cert"));
assertThat(Resource.NEW_REG.path(), is("new-reg")); assertThat(Resource.NEW_REG.path(), is("new-reg"));
assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert")); assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert"));
assertThat(Resource.RECOVER_REG.path(), is("recover-reg"));
// fails if there are untested future Resource values // fails if there are untested future Resource values
assertThat(Resource.values().length, is(4)); assertThat(Resource.values().length, is(5));
} }
/** /**

View File

@ -53,12 +53,14 @@ public class AbstractAcmeClientTest {
private Account testAccount; private Account testAccount;
private URI resourceUri; private URI resourceUri;
private URI locationUri; private URI locationUri;
private URI anotherLocationUri;
private URI agreementUri; private URI agreementUri;
@Before @Before
public void setup() throws IOException, URISyntaxException { public void setup() throws IOException, URISyntaxException {
resourceUri = new URI("https://example.com/acme/some-resource"); resourceUri = new URI("https://example.com/acme/some-resource");
locationUri = new URI("https://example.com/acme/some-location"); locationUri = new URI("https://example.com/acme/some-location");
anotherLocationUri = new URI("https://example.com/acme/another-location");
agreementUri = new URI("http://example.com/agreement.pdf"); agreementUri = new URI("http://example.com/agreement.pdf");
testAccount = new Account(TestUtils.createKeyPair()); testAccount = new Account(TestUtils.createKeyPair());
} }
@ -146,6 +148,48 @@ public class AbstractAcmeClientTest {
assertThat(registration.getAgreement(), is(agreementUri)); assertThat(registration.getAgreement(), is(agreementUri));
} }
/**
* Test that a {@link Registration} can be recovered by contact-based recovery.
*/
@Test
public void testRecoverRegistration() throws AcmeException {
Registration registration = new Registration();
registration.addContact("mailto:foo@example.com");
registration.setLocation(locationUri);
Connection connection = new DummyConnection() {
@Override
public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Account account) throws AcmeException {
assertThat(uri, is(resourceUri));
assertThat(claims.toString(), sameJSONAs(getJson("recoverRegistration")));
assertThat(session, is(notNullValue()));
assertThat(account, is(sameInstance(testAccount)));
return HttpURLConnection.HTTP_CREATED;
}
@Override
public URI getLocation() throws AcmeException {
return anotherLocationUri;
}
@Override
public URI getLink(String relation) throws AcmeException {
switch(relation) {
case "terms-of-service": return agreementUri;
default: return null;
}
}
};
TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection);
client.putTestResource(Resource.RECOVER_REG, resourceUri);
client.recoverRegistration(testAccount, registration);
assertThat(registration.getLocation(), is(anotherLocationUri));
assertThat(registration.getAgreement(), is(agreementUri));
}
/** /**
* Test that a new {@link Authorization} can be created. * Test that a new {@link Authorization} can be created.
*/ */

View File

@ -21,6 +21,12 @@ modifyRegistration = \
"agreement":"http://example.com/agreement.pdf",\ "agreement":"http://example.com/agreement.pdf",\
"contact":["mailto:foo2@example.com"]} "contact":["mailto:foo2@example.com"]}
recoverRegistration = \
{"resource":"recover-reg",\
"method":"contact",\
"base":"https://example.com/acme/some-location",\
"contact":["mailto:foo@example.com"]}
newAuthorizationRequest = \ newAuthorizationRequest = \
{"resource":"new-authz",\ {"resource":"new-authz",\
"identifier":{"type":"dns","value":"example.org"}} "identifier":{"type":"dns","value":"example.org"}}

View File

@ -0,0 +1,25 @@
# Account Recovery
The ACME server identifies your account by the public key that you provided on registration. If you lose your key pair, you will be unable to access your account.
ACME offers two ways of recovering access to your authorizations and certificates in case you have lost your key pair. However, both ways involve creating a new account, and transfering your data to it. You will not be able to regain access to your old account.
Individual CAs may offer further ways of recovery, which are not part of this documentation.
## Contact-Based Recovery
On this recovery method, the CA contacts the account owner via one of the contact addresses given on account creation. The owner is asked to take some action (e.g. clicking on a link in an email). If it was successful, the account data is transferred to the new account.
To initiate contact-based recovery, you first need to create a new key pair and an `Account` object. Then create a `Registration` object by passing the location URI of your _old_ account to the constructor. Finally, start the recovery process by invoking `recoverRegistration()`:
```java
Account account = ... // your new account
URI oldAccountUri = ... // location of your old account
Registration reg = new Registration(oldAccountUri);
client.recoverRegistration(account, reg);
URI newAccountUri = reg.getLocation();
```
`newAccountUri` is the location URI of your _new_ account.

View File

@ -19,6 +19,8 @@ After invocating `newRegistration()`, the `location` property contains the URI o
`newRegistration()` may fail and throw an `AcmeException` for various reasons. When your public key was already registered with the CA, an `AcmeConflictException` is thrown, but the `location` property will still hold your account URI after the call. This may be helpful if you forgot your account URI and need to recover it. `newRegistration()` may fail and throw an `AcmeException` for various reasons. When your public key was already registered with the CA, an `AcmeConflictException` is thrown, but the `location` property will still hold your account URI after the call. This may be helpful if you forgot your account URI and need to recover it.
You should always copy the `location` to a safe place. If you should lose your key pair, you will need it to [recover](./recovery.html) access to your account. Unlike your key pair, the `location` is an information that does not need security precautions.
## Update an Account ## Update an Account
At some point, you may want to update your account. For example your contact address might have changed, or you were asked by the CA to accept the current terms and conditions. At some point, you may want to update your account. For example your contact address might have changed, or you were asked by the CA to accept the current terms and conditions.

View File

@ -34,6 +34,7 @@
<item name="Account" href="usage/register.html"/> <item name="Account" href="usage/register.html"/>
<item name="Authorization" href="usage/authorization.html"/> <item name="Authorization" href="usage/authorization.html"/>
<item name="Certificate" href="usage/certificate.html"/> <item name="Certificate" href="usage/certificate.html"/>
<item name="Recovery" href="usage/recovery.html"/>
</item> </item>
<item name="Challenges" href="challenge/index.html"> <item name="Challenges" href="challenge/index.html">
<item name="HTTP" href="challenge/http.html"/> <item name="HTTP" href="challenge/http.html"/>