Upgrade to draft-ietf-acme-ari-03

This commit is contained in:
Richard Körber
2024-02-19 07:44:40 +01:00
parent 6a4770c23a
commit 48c32f612d
11 changed files with 126 additions and 99 deletions

View File

@@ -16,6 +16,8 @@ package org.shredzone.acme4j;
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;
@@ -197,7 +199,10 @@ public class Certificate extends AcmeResource {
*
* @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) {
@@ -212,7 +217,7 @@ public class Certificate extends AcmeResource {
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);
}
@@ -236,7 +241,7 @@ public class Certificate extends AcmeResource {
if (!url.endsWith("/")) {
url += '/';
}
url += getCertID();
url += getRenewalUniqueIdentifier(getCertificate());
return new URL(url);
} catch (MalformedURLException ex) {
throw new AcmeProtocolException("Invalid RenewalInfo URL", ex);
@@ -278,28 +283,6 @@ 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.
*
* @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.
*/

View File

@@ -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;
@@ -145,6 +149,31 @@ public class Login {
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.

View File

@@ -15,11 +15,14 @@ package org.shredzone.acme4j;
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;
@@ -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;
@@ -174,6 +178,65 @@ public class OrderBuilder {
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.
@@ -312,6 +375,10 @@ public class OrderBuilder {
}
}
if (replaces != null) {
claims.put("replaces", replaces);
}
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, login);
var order = new Order(login, conn.getLocation());

View File

@@ -15,7 +15,6 @@ 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;
@@ -37,7 +36,6 @@ 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;
@@ -286,8 +284,8 @@ public class CertificateTest {
*/
@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);
@@ -339,7 +337,7 @@ public class CertificateTest {
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()
@@ -365,8 +363,8 @@ public class CertificateTest {
*/
@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);
@@ -405,55 +403,11 @@ public class CertificateTest {
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();
}

View File

@@ -168,6 +168,7 @@ public class OrderBuilderTest {
.autoRenewalLifetime(validity)
.autoRenewalLifetimeAdjust(predate)
.autoRenewalEnableGet()
.replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE")
.create();
try (var softly = new AutoCloseableSoftAssertions()) {

View File

@@ -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

View File

@@ -11,5 +11,6 @@
"lifetime": 604800,
"lifetime-adjust": 518400,
"allow-certificate-get": true
}
},
"replaces": "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE"
}