mirror of https://github.com/shred/acme4j
Add contact-based recovery
parent
8168e6efc7
commit
b00114ad88
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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"}}
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
Loading…
Reference in New Issue