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 827f1c8f..6408db0f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -268,6 +268,28 @@ public class Certificate extends AcmeResource { return renewalInfo; } + /** + * Signals to the CA that this certificate has been successfully replaced by a newer + * one. A revocation of this certificate would not disrupt any ongoing services. + *
+ * This method is only supported by CAs that are providing renewal information
+ * (see {@link #hasRenewalInfo()}. An {@link AcmeNotSupportedException} is thrown
+ * otherwise.
+ *
+ * @since 3.1.0
+ */
+ public void markAsReplaced() throws AcmeException {
+ LOG.debug("mark as replaced");
+ var session = getSession();
+ var renewalInfoUrl = session.resourceUrl(Resource.RENEWAL_INFO);
+ try (var conn = session.connect()) {
+ var claims = new JSONBuilder();
+ claims.put("certID", getCertID());
+ claims.put("replaced", true);
+ conn.sendSignedRequest(renewalInfoUrl, claims, getLogin());
+ }
+ }
+
/**
* Revokes this certificate.
*/
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 02ff0c80..062f89dc 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/CertificateTest.java
@@ -15,6 +15,7 @@ package org.shredzone.acme4j;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.shredzone.acme4j.toolbox.TestUtils.*;
import java.io.ByteArrayOutputStream;
@@ -36,6 +37,7 @@ import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
+import org.shredzone.acme4j.exception.AcmeNotSupportedException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
@@ -358,4 +360,101 @@ public class CertificateTest {
provider.close();
}
+ /**
+ * Test that a certificate is marked as replaced.
+ */
+ @Test
+ public void testMarkedAsReplaced() throws AcmeException, IOException {
+ // certid-cert.pem and certId provided by draft-ietf-acme-ari-01 and known good
+ var certId = "MFswCwYJYIZIAWUDBAIBBCCeWLRusNLb--vmWOkxm34qDjTMWkc3utIhOMoMwKDqbgQg2iiKWySZrD-6c88HMZ6vhIHZPamChLlzGHeZ7pTS8jYCCD6jRWhlRB8c";
+ var certIdCert = TestUtils.createCertificate("/certid-cert.pem");
+ var certResourceUrl = new URL(resourceUrl.toExternalForm() + "/" + certId);
+
+ var provider = new TestableConnectionProvider() {
+ private boolean certRequested = false;
+
+ @Override
+ public int sendCertificateRequest(URL url, Login login) {
+ assertThat(url).isEqualTo(locationUrl);
+ assertThat(login).isNotNull();
+ certRequested = true;
+ return HttpURLConnection.HTTP_OK;
+ }
+
+ @Override
+ public int sendSignedRequest(URL url, JSONBuilder claims, Login login) {
+ assertThat(certRequested).isTrue();
+ assertThat(url).isEqualTo(resourceUrl);
+ assertThatJson(claims.toString()).isEqualTo(getJSON("replacedCertificateRequest").toString());
+ assertThat(login).isNotNull();
+ return HttpURLConnection.HTTP_OK;
+ }
+
+ @Override
+ public List