Add a way to revoke a certificate without account key pair

pull/55/head
Richard Körber 2017-08-13 14:14:18 +02:00
parent 3881669e22
commit f3735e6ec1
3 changed files with 103 additions and 0 deletions

View File

@ -21,12 +21,14 @@ import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
@ -48,6 +50,8 @@ public class Certificate extends AcmeResource {
private static final long serialVersionUID = 7381527770159084201L;
private static final Logger LOG = LoggerFactory.getLogger(Certificate.class);
protected static BiFunction<URI, KeyPair, Session> revokeSessionFactory = Session::new;
private ArrayList<X509Certificate> certChain = null;
private ArrayList<URL> alternates = null;
@ -183,6 +187,45 @@ public class Certificate extends AcmeResource {
}
}
/**
* Revoke a certificate. This call is meant to be used for revoking certificates if
* the account's key pair was lost.
*
* @param serverUri
* {@link URI} of the ACME server
* @param domainKeyPair
* Key pair the CSR was signed with
* @param cert
* The {@link X509Certificate} to be revoked
* @param reason
* {@link RevocationReason} stating the reason of the revocation that is
* used when generating OCSP responses and CRLs. {@code null} to give no
* reason.
*/
public static void revoke(URI serverUri, KeyPair domainKeyPair, X509Certificate cert,
RevocationReason reason) throws AcmeException {
LOG.debug("revoke immediately");
Session session = revokeSessionFactory.apply(serverUri, domainKeyPair);
URL resUrl = session.resourceUrl(Resource.REVOKE_CERT);
if (resUrl == null) {
throw new AcmeException("Server does not allow certificate revocation");
}
try (Connection conn = session.provider().connect()) {
JSONBuilder claims = new JSONBuilder();
claims.putBase64("certificate", cert.getEncoded());
if (reason != null) {
claims.put("reason", reason.getReasonCode());
}
conn.sendSignedRequest(resUrl, claims, session, true);
conn.accept(HttpURLConnection.HTTP_OK);
} catch (CertificateEncodingException ex) {
throw new AcmeProtocolException("Invalid certificate", ex);
}
}
/**
* Lazily downloads the certificate. Throws a runtime {@link AcmeLazyLoadingException}
* if the download failed.

View File

@ -25,6 +25,7 @@ import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
@ -233,4 +234,46 @@ public class CertificateTest {
assertThat(RevocationReason.code(1), is(RevocationReason.KEY_COMPROMISE));
}
/**
* Test that a certificate can be revoked by its domain key pair.
*/
@Test
public void testRevokeCertificateByKeyPair() throws AcmeException, IOException {
final List<X509Certificate> originalCert = TestUtils.createCertificate();
final KeyPair certKeyPair = TestUtils.createDomainKeyPair();
TestableConnectionProvider provider = new TestableConnectionProvider() {
@Override
public void sendSignedRequest(URL url, JSONBuilder claims, Session session, boolean enforceJwk)
throws AcmeException {
assertThat(url, is(resourceUrl));
assertThat(claims.toString(), sameJSONAs(getJSON("revokeCertificateWithReasonRequest").toString()));
assertThat(session, is(notNullValue()));
assertThat(session.getKeyPair(), is(certKeyPair));
assertThat(enforceJwk, is(true));
}
@Override
public int accept(int... httpStatus) throws AcmeException {
assertThat(httpStatus, isIntArrayContainingInAnyOrder(HttpURLConnection.HTTP_OK));
return HttpURLConnection.HTTP_OK;
}
};
provider.putTestResource(Resource.REVOKE_CERT, resourceUrl);
Session session = provider.createSession();
URI serverUri = session.getServerUri();
Certificate.revokeSessionFactory = (uri, keyPair) -> {
assertThat(uri, is(serverUri));
session.setKeyPair(keyPair);
return session;
};
Certificate.revoke(serverUri, certKeyPair, originalCert.get(0), RevocationReason.KEY_COMPROMISE);
provider.close();
}
}

View File

@ -135,3 +135,20 @@ Optionally, you can provide a revocation reason that the ACME server may use whe
```java
cert.revoke(RevocationReason.KEY_COMPROMISE);
```
## Revocation without account key pair
If you have lost your account key, you can still revoke a certificate as long as you still own the key pair that was used for signing the CSR. `Certificate` provides a special method for this case.
First, create a new `Session` object, but use _the key pair that was used for siging the CSR_. Now invoke the `revoke()` method and pass the `Session`, the certificate to be revoked, and (optionally) a revocation reason.
```java
KeyPair domainKeyPair = ... // the key pair that was used for signing the CSR
URI acmeServerUri = ... // uri of the ACME server
X509Certificate cert = ... // certificate to revoke
Session session = new Session(acmeServerUri, domainKeyPair);
Certificate.revoke(session, cert, RevocationReason.KEY_COMPROMISE);
```
Note that there is no way to revoke a certificate if you should lose both your account's key pair and your domain's key pair.