From b00114ad884af31974a39cebc386eb647abe6e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Mon, 21 Dec 2015 01:32:30 +0100 Subject: [PATCH] Add contact-based recovery --- README.md | 2 +- .../java/org/shredzone/acme4j/AcmeClient.java | 16 ++++++- .../shredzone/acme4j/connector/Resource.java | 3 +- .../acme4j/impl/AbstractAcmeClient.java | 36 +++++++++++++++ .../acme4j/connector/ResourceTest.java | 3 +- .../acme4j/impl/AbstractAcmeClientTest.java | 44 +++++++++++++++++++ .../src/test/resources/json.properties | 6 +++ src/site/markdown/usage/recovery.md | 25 +++++++++++ src/site/markdown/usage/register.md | 2 + src/site/site.xml | 1 + 10 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 src/site/markdown/usage/recovery.md diff --git a/README.md b/README.md index 9b7a8a2a..9089f405 100644 --- a/README.md +++ b/README.md @@ -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: -* Support of account recovery. +* Support of MAC-based account recovery. * `proofOfPossession-01` and `tls-sni-01` challenge support. ## License 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 579526e0..81edc599 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java @@ -44,11 +44,23 @@ public interface AcmeClient { * @param account * {@link Account} that is registered * @param registration - * {@link Registration} containing updated registration data. Set the - * account location via {@link Registration#setLocation(URI)}! + * {@link Registration} containing updated registration data and the + * account location URI */ 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 + * New {@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. * diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java index a6b5817d..870beac5 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java @@ -23,7 +23,8 @@ public enum Resource { NEW_REG("new-reg"), NEW_AUTHZ("new-authz"), 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. 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 62b8fcd6..57bc6e25 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 @@ -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 public void newAuthorization(Account account, Authorization auth) throws AcmeException { if (account == null) { diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java index 1fcd419b..eb145f65 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java @@ -34,9 +34,10 @@ public class ResourceTest { assertThat(Resource.NEW_CERT.path(), is("new-cert")); assertThat(Resource.NEW_REG.path(), is("new-reg")); assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert")); + assertThat(Resource.RECOVER_REG.path(), is("recover-reg")); // fails if there are untested future Resource values - assertThat(Resource.values().length, is(4)); + assertThat(Resource.values().length, is(5)); } /** 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 b17b3d7f..9b28aa4f 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 @@ -53,12 +53,14 @@ public class AbstractAcmeClientTest { private Account testAccount; private URI resourceUri; private URI locationUri; + private URI anotherLocationUri; private URI agreementUri; @Before public void setup() throws IOException, URISyntaxException { resourceUri = new URI("https://example.com/acme/some-resource"); 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"); testAccount = new Account(TestUtils.createKeyPair()); } @@ -146,6 +148,48 @@ public class AbstractAcmeClientTest { 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. */ diff --git a/acme4j-client/src/test/resources/json.properties b/acme4j-client/src/test/resources/json.properties index 72273b3a..1d90ee17 100644 --- a/acme4j-client/src/test/resources/json.properties +++ b/acme4j-client/src/test/resources/json.properties @@ -21,6 +21,12 @@ modifyRegistration = \ "agreement":"http://example.com/agreement.pdf",\ "contact":["mailto:foo2@example.com"]} +recoverRegistration = \ + {"resource":"recover-reg",\ + "method":"contact",\ + "base":"https://example.com/acme/some-location",\ + "contact":["mailto:foo@example.com"]} + newAuthorizationRequest = \ {"resource":"new-authz",\ "identifier":{"type":"dns","value":"example.org"}} diff --git a/src/site/markdown/usage/recovery.md b/src/site/markdown/usage/recovery.md new file mode 100644 index 00000000..b4300db2 --- /dev/null +++ b/src/site/markdown/usage/recovery.md @@ -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. diff --git a/src/site/markdown/usage/register.md b/src/site/markdown/usage/register.md index 25c48db7..838e3545 100644 --- a/src/site/markdown/usage/register.md +++ b/src/site/markdown/usage/register.md @@ -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. +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 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. diff --git a/src/site/site.xml b/src/site/site.xml index 8df72303..b11b76f4 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -34,6 +34,7 @@ +