mirror of
https://github.com/shred/acme4j.git
synced 2025-12-16 11:24:01 +08:00
Upgrade to draft-ietf-acme-ari-03
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ public class OrderBuilderTest {
|
||||
.autoRenewalLifetime(validity)
|
||||
.autoRenewalLifetimeAdjust(predate)
|
||||
.autoRenewalEnableGet()
|
||||
.replaces("aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE")
|
||||
.create();
|
||||
|
||||
try (var softly = new AutoCloseableSoftAssertions()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
"lifetime": 604800,
|
||||
"lifetime-adjust": 518400,
|
||||
"allow-certificate-get": true
|
||||
}
|
||||
},
|
||||
"replaces": "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user