diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java index bf8e7fc3..90e7dd5f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -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 revokeSessionFactory = Session::new; + private ArrayList certChain = null; private ArrayList 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. diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java index 76859240..8ec4f79c 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java @@ -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 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(); + } + } diff --git a/src/site/markdown/usage/certificate.md b/src/site/markdown/usage/certificate.md index ca0d0381..a4ad6d93 100644 --- a/src/site/markdown/usage/certificate.md +++ b/src/site/markdown/usage/certificate.md @@ -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.