diff --git a/README.md b/README.md index 74fbe8c2..9182e455 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 and certificate revocation. +* Support of account recovery. * `proofOfPossession-01` and `tls-sni-01` challenge support. * Better error handling. * Some hardening (like plausibility checks). 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 de349623..6b8a8dad 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeClient.java @@ -109,4 +109,14 @@ public interface AcmeClient { */ X509Certificate downloadCertificate(URI certUri) throws AcmeException; + /** + * Revokes a certificate. + * + * @param account + * {@link Account} to be used for conversation + * @param certificate + * Certificate to revoke + */ + void revokeCertificate(Account account, X509Certificate certificate) throws AcmeException; + } 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 4d6a41ad..a6b5817d 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 @@ -20,7 +20,10 @@ package org.shredzone.acme4j.connector; */ public enum Resource { - NEW_REG("new-reg"), NEW_AUTHZ("new-authz"), NEW_CERT("new-cert"); + NEW_REG("new-reg"), + NEW_AUTHZ("new-authz"), + NEW_CERT("new-cert"), + REVOKE_CERT("revoke-cert"); /** * 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 a15179a7..d785e3df 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 @@ -15,6 +15,7 @@ package org.shredzone.acme4j.impl; import java.net.HttpURLConnection; import java.net.URI; +import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; @@ -280,4 +281,26 @@ public abstract class AbstractAcmeClient implements AcmeClient { } } + @Override + public void revokeCertificate(Account account, X509Certificate certificate) throws AcmeException { + LOG.debug("revokeCertificate"); + URI resUri = resourceUri(Resource.REVOKE_CERT); + if (resUri == null) { + throw new AcmeException("CA does not support certificate revocation"); + } + + try (Connection conn = createConnection()) { + ClaimBuilder claims = new ClaimBuilder(); + claims.putResource(Resource.REVOKE_CERT); + claims.putBase64("certificate", certificate.getEncoded()); + + int rc = conn.sendSignedRequest(resUri, claims, session, account); + if (rc != HttpURLConnection.HTTP_OK) { + conn.throwAcmeException(); + } + } catch (CertificateEncodingException ex) { + throw new IllegalArgumentException("Invalid certificate", ex); + } + } + } 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 5625bced..1fcd419b 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 @@ -33,9 +33,10 @@ public class ResourceTest { assertThat(Resource.NEW_AUTHZ.path(), is("new-authz")); assertThat(Resource.NEW_CERT.path(), is("new-cert")); assertThat(Resource.NEW_REG.path(), is("new-reg")); + assertThat(Resource.REVOKE_CERT.path(), is("revoke-cert")); // fails if there are untested future Resource values - assertThat(Resource.values().length, is(3)); + assertThat(Resource.values().length, is(4)); } /** 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 9a71a1dd..ae53f0a3 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 @@ -336,6 +336,30 @@ public class AbstractAcmeClientTest { assertThat(downloadedCert, is(sameInstance(originalCert))); } + /** + * Test that a certificate can be revoked. + */ + @Test + public void testRevokeCertificate() throws AcmeException, IOException { + final X509Certificate cert = TestUtils.createCertificate(); + + 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("revokeCertificateRequest"))); + assertThat(session, is(notNullValue())); + assertThat(account, is(sameInstance(testAccount))); + return HttpURLConnection.HTTP_OK; + } + }; + + TestableAbstractAcmeClient client = new TestableAbstractAcmeClient(connection); + client.putTestResource(Resource.REVOKE_CERT, resourceUri); + + client.revokeCertificate(testAccount, cert); + } + /** * Extends the {@link AbstractAcmeClient} to be tested, and implements the abstract * methods with a simple implementation specially made for testing purposes. diff --git a/acme4j-client/src/test/resources/json.properties b/acme4j-client/src/test/resources/json.properties index 24935c19..60407f68 100644 --- a/acme4j-client/src/test/resources/json.properties +++ b/acme4j-client/src/test/resources/json.properties @@ -89,6 +89,12 @@ requestCertificateRequest = \ "resource":"new-cert"\ } +revokeCertificateRequest = \ + {\ + "certificate": "MIIDVzCCAj-gAwIBAgIJAM4KDTzb0Y7NMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwHhcNMTUxMjEwMDAxMTA4WhcNMjUxMjA3MDAxMTA4WjBCMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0g3w4C8xbj_5lzJiDxk0HkEJeZeyruq-0AzOPMigJZ7zxZtX_KUxOIHrQ4qjcFhl0DmQImoM0wESU-kcsjAHCx8E1lgRVlVsMfLAQPHkg5UybqfadzKT3ALcSD-9F9mVIP6liC_6KzLTASmx6zM7j92KTl1ArObZr5mh0jvSNORrMhEC4Byn3-NTxjuHON1rWppCMwpeNNhFzaAig3O8PY8IyaLXNP2Ac5pXn0iW16S-Im9by7751UeW5a7DznmuMEM-WY640ffJDQ4-I64H403uAgvvSu-BGw8SEEZGuBCxoCnG1g6y6OvJyN5TgqFdGosAfm1u-_MP1seoPdpBQIDAQABo1AwTjAdBgNVHQ4EFgQUrie5ZLOrA_HuhW1b_CHjzEvj34swHwYDVR0jBBgwFoAUrie5ZLOrA_HuhW1b_CHjzEvj34swDAYDVR0TBAUwAwEB_zANBgkqhkiG9w0BAQsFAAOCAQEAkSOP0FUgIIUeJTObgXrenHzZpLAkqXi37dgdYuPhNveo3agueP51N7yIoh6YGShiJ73Rvr-lVYTwFXStrLih1Wh3tWvksMxnvocgd7l6USRb5_AgH7eHeFK4DoCAak2hUAcCLDRJN3XMhNLpyJhw7GJxowVIGUlxcW5Asrmh9qflfyMyjripTP3CdHobmNcNHyScjNncKj37m8vomel9acekTtDl2Ci7nLdE-3VqQCXMIfLiF3PO0gGpKei0RuVCSOG6W83zVInCPd_l3aluSR-f_VZlk8KGQ4As4uTQi89j-J1YepzG0ASMZpjVbXeIg5QBAywVxBh5XVTz37KN8A",\ + "resource":"revoke-cert"\ + } + dnsChallenge = \ diff --git a/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java b/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java index 72b44003..a9873679 100644 --- a/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java +++ b/acme4j-example/src/main/java/org/shredzone/acme4j/ClientTest.java @@ -188,6 +188,9 @@ public class ClientTest { try (FileWriter fw = new FileWriter(DOMAIN_CERT_FILE)) { CertificateUtils.writeX509Certificate(cert, fw); } + + // Revoke the certificate (uncomment if needed...) + // client.revokeCertificate(account, cert); } public static void main(String... args) { diff --git a/src/site/markdown/usage/certificate.md b/src/site/markdown/usage/certificate.md index a6acfbaa..fb98baad 100644 --- a/src/site/markdown/usage/certificate.md +++ b/src/site/markdown/usage/certificate.md @@ -1,7 +1,9 @@ -# Request a Certificate +# Certificates Once you completed all the previous steps, it's time to request the signed certificate. +## Request a Certificate + To do so, prepare a PKCS#10 CSR file. A single domain may be set as _Common Name_. Multiple domains must be provided as _Subject Alternative Name_. Other properties (_Organization_, _Organization Unit_ etc.) depend on the CA. Some may require these properties to be set, while others may ignore them when generating the certificate. CSR files can be generated with command line tools like `openssl`. Unfortunately the standard Java does not offer classes for that, so you'd have to resort to [Bouncy Castle](http://www.bouncycastle.org/java.html) if you want to create a CSR programmatically. In the `acme4j-utils` module, there is also a [`CSRBuilder`](../apidocs/org/shredzone/acme4j/util/CSRBuilder.html) for your convenience: @@ -36,3 +38,22 @@ Congratulations! You have just created your first certificate via _acme4j_. ## Renewal Renewing your certificate depends on the CA. Some may require you to go through the authorization process again, while others may just provide an updated certificate for download at the `certUri` above. + +## Revocation + +To revoke a certificate, just pass the it to the respective method: + +```java +X509Certificate cert = ... // certificate to be revoked +client.revokeCertificate(account, cert); +``` + +As an exception, ACME servers also accept the domain's key pair for revoking a certificate. _acme4j_ does not directly support this way of revocation. However, you can do so with this tiny hack: + +```java +KeyPair domainKeyPair = ... // KeyPair to be used for HTTPS encryption +X509Certificate cert = ... // certificate to be revoked +client.revokeCertificate(new Account(domainKeyPair), cert); +``` + +If you have the choice, you should always prefer to use your account key.