Skip to content

Commit

Permalink
Upgrade to draft-ietf-acme-ari-03
Browse files Browse the repository at this point in the history
  • Loading branch information
shred committed Feb 19, 2024
1 parent 6a4770c commit 48c32f6
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 99 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ It is an independent open source implementation that is not affiliated with or e
* Supports [RFC 8738](https://tools.ietf.org/html/rfc8738) IP identifier validation
* Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental)
* Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental)
* Supports [draft-ietf-acme-ari-01](https://www.ietf.org/archive/id/draft-ietf-acme-ari-01.html) for renewal information
* Supports [draft-ietf-acme-ari-03](https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html) for renewal information
* Easy to use Java API
* Requires JRE 11 or higher
* Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22)
Expand Down
31 changes: 7 additions & 24 deletions acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;
import static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;

import java.io.IOException;
import java.io.Writer;
Expand Down Expand Up @@ -197,7 +199,10 @@ public void writeCertificate(Writer out) throws IOException {
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc6960.html">RFC 6960</a>
* @since 3.0.0
* @deprecated Is not needed in the ACME context anymore and will thus be removed in
* a later version.
*/
@Deprecated
public String getCertID() {
var certChain = getCertificateChain();
if (certChain.size() < 2) {
Expand All @@ -212,7 +217,7 @@ public String getCertID() {
var digestCalc = builder.build().get(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256));
var issuerHolder = new X509CertificateHolder(certChain.get(1).getEncoded());
var certId = new CertificateID(digestCalc, issuerHolder, certChain.get(0).getSerialNumber());
return AcmeUtils.base64UrlEncode(certId.toASN1Primitive().getEncoded());
return base64UrlEncode(certId.toASN1Primitive().getEncoded());
} catch (Exception ex) {
throw new AcmeProtocolException("Could not compute Certificate ID", ex);
}
Expand All @@ -236,7 +241,7 @@ public Optional<URL> getRenewalInfoLocation() {
if (!url.endsWith("/")) {
url += '/';
}
url += getCertID();
url += getRenewalUniqueIdentifier(getCertificate());
return new URL(url);
} catch (MalformedURLException ex) {
throw new AcmeProtocolException("Invalid RenewalInfo URL", ex);
Expand Down Expand Up @@ -278,28 +283,6 @@ public RenewalInfo getRenewalInfo() {
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.
*
* @draft This method is currently based on an RFC draft. It may be changed or
* removed without notice to reflect future changes to the draft. SemVer rules
* do not apply here.
* @throws AcmeNotSupportedException if the CA does not support renewal information.
* @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.
*/
Expand Down
29 changes: 29 additions & 0 deletions acme4j-client/src/main/java/org/shredzone/acme4j/Login.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
package org.shredzone.acme4j;

import static java.util.Objects.requireNonNull;
import static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;

import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Objects;

import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
Expand Down Expand Up @@ -145,6 +149,31 @@ public RenewalInfo bindRenewalInfo(URL location) {
return new RenewalInfo(this, requireNonNull(location, "location"));
}

/**
* Creates a new instance of an existing {@link RenewalInfo} and binds it to this
* login.
*
* @param certificate
* {@link X509Certificate} to get the {@link RenewalInfo} for
* @return {@link RenewalInfo} bound to the login
* @draft This method is currently based on an RFC draft. It may be changed or removed
* without notice to reflect future changes to the draft. SemVer rules do not apply
* here.
* @since 3.2.0
*/
public RenewalInfo bindRenewalInfo(X509Certificate certificate) throws AcmeException {
try {
var url = getSession().resourceUrl(Resource.RENEWAL_INFO).toExternalForm();
if (!url.endsWith("/")) {
url += '/';
}
url += getRenewalUniqueIdentifier(certificate);
return bindRenewalInfo(new URL(url));
} catch (MalformedURLException ex) {
throw new AcmeProtocolException("Invalid RenewalInfo URL", ex);
}
}

/**
* Creates a new instance of an existing {@link Challenge} and binds it to this
* login. Use this method only if the resulting challenge type is unknown.
Expand Down
67 changes: 67 additions & 0 deletions acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier;

import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

import edu.umd.cs.findbugs.annotations.Nullable;
Expand All @@ -44,6 +47,7 @@ public class OrderBuilder {
private final Set<Identifier> identifierSet = new LinkedHashSet<>();
private @Nullable Instant notBefore;
private @Nullable Instant notAfter;
private @Nullable String replaces;
private boolean autoRenewal;
private @Nullable Instant autoRenewalStart;
private @Nullable Instant autoRenewalEnd;
Expand Down Expand Up @@ -174,6 +178,65 @@ public OrderBuilder autoRenewal() {
return this;
}

/**
* Notifies the CA that the ordered certificate will replace a previously issued
* certificate. The certificate is identified by its ARI unique identifier.
* <p>
* Optional, only supported if the CA provides renewal information. However, in this
* case the client <em>should</em> include this field.
*
* @param uniqueId
* Certificate's renewal unique identifier.
* @return itself
* @draft This method is currently based on an RFC draft. It may be changed or removed
* without notice to reflect future changes to the draft. SemVer rules do not apply
* here.
* @since 3.2.0
*/
public OrderBuilder replaces(String uniqueId) {
autoRenewal();
this.replaces = Objects.requireNonNull(uniqueId);
return this;
}

/**
* Notifies the CA that the ordered certificate will replace a previously issued
* certificate.
* <p>
* Optional, only supported if the CA provides renewal information. However, in this
* case the client <em>should</em> include this field.
*
* @param certificate
* Certificate to be replaced
* @return itself
* @draft This method is currently based on an RFC draft. It may be changed or removed
* without notice to reflect future changes to the draft. SemVer rules do not apply
* here.
* @since 3.2.0
*/
public OrderBuilder replaces(X509Certificate certificate) {
return replaces(getRenewalUniqueIdentifier(certificate));
}

/**
* Notifies the CA that the ordered certificate will replace a previously issued
* certificate.
* <p>
* Optional, only supported if the CA provides renewal information. However, in this
* case the client <em>should</em> include this field.
*
* @param certificate
* Certificate to be replaced
* @return itself
* @draft This method is currently based on an RFC draft. It may be changed or removed
* without notice to reflect future changes to the draft. SemVer rules do not apply
* here.
* @since 3.2.0
*/
public OrderBuilder replaces(Certificate certificate) {
return replaces(certificate.getCertificate());
}

/**
* Sets the earliest date of validity of the first issued certificate. If not set,
* the start date is the earliest possible date.
Expand Down Expand Up @@ -312,6 +375,10 @@ public Order create() throws AcmeException {
}
}

if (replaces != null) {
claims.put("replaces", replaces);
}

conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);

var order = new Order(login, conn.getLocation());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

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;
Expand All @@ -37,7 +36,6 @@
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;
Expand Down Expand Up @@ -286,8 +284,8 @@ public int sendSignedRequest(URL url, JSONBuilder claims, Session session, KeyPa
*/
@Test
public void testRenewalInfo() throws AcmeException, IOException {
var certId = "MFswCwYJYIZIAWUDBAIBBCCeWLRusNLb--vmWOkxm34qDjTMWkc3utIhOMoMwKDqbgQg2iiKWySZrD-6c88HMZ6vhIHZPamChLlzGHeZ7pTS8jYCCD6jRWhlRB8c";
// certid-cert.pem and certId provided by draft-ietf-acme-ari-01 and known good
// certid-cert.pem and certId provided by draft-ietf-acme-ari-03 and known good
var certId = "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE";
var certIdCert = TestUtils.createCertificate("/certid-cert.pem");
var certResourceUrl = new URL(resourceUrl.toExternalForm() + "/" + certId);
var retryAfterInstant = Instant.now().plus(10L, ChronoUnit.DAYS);
Expand Down Expand Up @@ -339,7 +337,7 @@ public Optional<Instant> getRetryAfter() {
provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl);

var cert = new Certificate(provider.createLogin(), locationUrl);
assertThat(cert.getCertID()).isEqualTo(certId);
assertThat(cert.getCertID()).isEqualTo("MFgwCwYJYIZIAWUDBAIBBCCeWLRusNLb--vmWOkxm34qDjTMWkc3utIhOMoMwKDqbgQg2iiKWySZrD-6c88HMZ6vhIHZPamChLlzGHeZ7pTS8jYCBQCHZUMh");
assertThat(cert.hasRenewalInfo()).isTrue();
assertThat(cert.getRenewalInfoLocation())
.isNotEmpty()
Expand All @@ -365,8 +363,8 @@ public Optional<Instant> getRetryAfter() {
*/
@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";
// certid-cert.pem and certId provided by draft-ietf-acme-ari-03 and known good
var certId = "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE";
var certIdCert = TestUtils.createCertificate("/certid-cert.pem");
var certResourceUrl = new URL(resourceUrl.toExternalForm() + "/" + certId);

Expand Down Expand Up @@ -405,55 +403,11 @@ public Collection<URL> getLinks(String relation) {
provider.putTestResource(Resource.RENEWAL_INFO, resourceUrl);

var cert = new Certificate(provider.createLogin(), locationUrl);
assertThat(cert.getCertID()).isEqualTo(certId);
assertThat(cert.hasRenewalInfo()).isTrue();
assertThat(cert.getRenewalInfoLocation())
.isNotEmpty()
.contains(certResourceUrl);

cert.markAsReplaced();

provider.close();
}

/**
* Test that markAsReplaced() throws an exception if not supported.
*/
@Test
public void testMarkedAsReplacedThrowsIfNotSupported() throws AcmeException, IOException {
var certIdCert = TestUtils.createCertificate("/certid-cert.pem");

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 List<X509Certificate> readCertificates() {
assertThat(certRequested).isTrue();
return certIdCert;
}

@Override
public Collection<URL> getLinks(String relation) {
return Collections.emptyList();
}
};

// We just need a dummy resource to create a directory
provider.putTestResource(Resource.NEW_ORDER, resourceUrl);

assertThatExceptionOfType(AcmeNotSupportedException.class).isThrownBy(() -> {
var cert = new Certificate(provider.createLogin(), locationUrl);
cert.markAsReplaced();
});

provider.close();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ public URL getLocation() {
.autoRenewalLifetime(validity)
.autoRenewalLifetimeAdjust(predate)
.autoRenewalEnableGet()
.replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE")
.create();

try (var softly = new AutoCloseableSoftAssertions()) {
Expand Down
25 changes: 7 additions & 18 deletions acme4j-client/src/test/resources/certid-cert.pem
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIDMDCCAhigAwIBAgIIPqNFaGVEHxwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVbWluaWNhIHJvb3QgY2EgM2ExMzU2MB4XDTIyMDMxNzE3NTEwOVoXDTI0MDQx
NjE3NTEwOVowFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCgm9K/c+il2Pf0f8qhgxn9SKqXq88cOm9ov9AVRbPA
OWAAewqX2yUAwI4LZBGEgzGzTATkiXfoJ3cN3k39cH6tBbb3iSPuEn7OZpIk9D+e
3Q9/hX+N/jlWkaTB/FNA+7aE5IVWhmdczYilXa10V9r+RcvACJt0gsipBZVJ4jfJ
HnWJJGRZzzxqG/xkQmpXxZO7nOPFc8SxYKWdfcgp+rjR2ogYhSz7BfKoVakGPbpX
vZOuT9z4kkHra/WjwlkQhtHoTXdAxH3qC2UjMzO57Tx+otj0CxAv9O7CTJXISywB
vEVcmTSZkHS3eZtvvIwPx7I30ITRkYk/tLl1MbyB3SiZAgMBAAGjeDB2MA4GA1Ud
DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T
AQH/BAIwADAfBgNVHSMEGDAWgBQ4zzDRUaXHVKqlSTWkULGU4zGZpTAWBgNVHREE
DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAx0aYvmCk7JYGNEXe
+hrOfKawkHYzWvA92cI/Oi6h+oSdHZ2UKzwFNf37cVKZ37FCrrv5pFP/xhhHvrNV
EnOx4IaF7OrnaTu5miZiUWuvRQP7ZGmGNFYbLTEF6/dj+WqyYdVaWzxRqHFu1ptC
TXysJCeyiGnR+KOOjOOQ9ZlO5JUK3OE4hagPLfaIpDDy6RXQt3ss0iNLuB1+IOtp
1URpvffLZQ8xPsEgOZyPWOcabTwJrtqBwily+lwPFn2mChUx846LwQfxtsXU/lJg
HX2RteNJx7YYNeX3Uf960mgo5an6vE8QNAsIoNHYrGyEmXDhTRe9mCHyiW2S7fZq
o9q12g==
MIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt
cGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS
BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu
7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf
qzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B
yNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb
+FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIIOhNWtJ7Igr0wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
"lifetime": 604800,
"lifetime-adjust": 518400,
"allow-certificate-get": true
}
},
"replaces": "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE"
}
2 changes: 1 addition & 1 deletion src/doc/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Latest version: ![maven central](https://shredzone.org/maven-central/org.shredzo
* Supports [RFC 8738](https://tools.ietf.org/html/rfc8738) IP identifier validation
* Supports [RFC 8739](https://tools.ietf.org/html/rfc8739) short-term automatic certificate renewal (experimental)
* Supports [RFC 8823](https://tools.ietf.org/html/rfc8823) for S/MIME certificates (experimental)
* Supports [draft-ietf-acme-ari-01](https://www.ietf.org/archive/id/draft-ietf-acme-ari-01.html) for renewal information
* Supports [draft-ietf-acme-ari-03](https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html) for renewal information
* Easy to use Java API
* Requires JRE 11 or higher
* Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22)
Expand Down
3 changes: 3 additions & 0 deletions src/doc/docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ This document will help you migrate your code to the latest _acme4j_ version.
## Migration to Version 3.2.0

- Starting with this version, the `CSRBuilder` won't add the first domain as common name automatically. This permits the issuance of very long domain names, and should have no negative impact otherwise, as this field is usually ignored by CAs anyway. If you should encounter a problem here, you can use `CSRBuilder.setCommonName()` to set the first domain as common name manually. Discussion see [here](https://community.letsencrypt.org/t/questions-re-simplifying-issuance-for-very-long-domain-names/207925/11).
- acme4j was updated to support the latest [draft-ietf-acme-ari-03](https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html) now. It is a breaking change! If you use ARI, make sure your server supports the latest draft before updating to this version of acme4j.
- `Certificate.markAsReplace()` has been removed, because this method is not supported by [draft-ietf-acme-ari-03](https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html) anymore. To mark an existing certificate as replaced, use the new method `OrderBuilder.replaces()` now.
- `Certificate.getCertID()` is not needed in the ACME context anymore. This method has been marked as deprecated. In a future version of acme4j, it will be removed without replacement. Refer to the source code to see how the certificate ID is computed.

## Migration to Version 3.0.0

Expand Down
Loading

0 comments on commit 48c32f6

Please sign in to comment.