From 6a4770c23a48620a9951eeb5a31102caa32fedef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Sun, 18 Feb 2024 16:16:29 +0100 Subject: [PATCH] Get unique identifier according to draft-ietf-acme-ari-03 --- .../shredzone/acme4j/toolbox/AcmeUtils.java | 61 +++++++++++++++++++ .../src/test/resources/ari-example-cert.pem | 9 +++ 2 files changed, 70 insertions(+) create mode 100644 acme4j-client/src/test/resources/ari-example-cert.pem diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java index 9752bd66..fd10cbc9 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java @@ -16,21 +16,29 @@ package org.shredzone.acme4j.toolbox; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; +import java.io.UncheckedIOException; import java.io.Writer; import java.net.IDN; import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.Arrays; import java.util.Base64; import java.util.Locale; import java.util.Objects; +import java.util.Optional; import java.util.regex.Pattern; import edu.umd.cs.findbugs.annotations.Nullable; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.Certificate; +import org.bouncycastle.cert.X509CertificateHolder; import org.shredzone.acme4j.exception.AcmeProtocolException; /** @@ -323,4 +331,57 @@ public final class AcmeUtils { } } + /** + * Returns the certificate's unique identifier for renewal according to + * draft-ietf-acme-ari-03. + * + * @param certificate + * Certificate to get the unique identifier for. + * @return Unique identifier + * @throws AcmeProtocolException + * if the certificate is invalid or does not provide the necessary + * information. + */ + public static String getRenewalUniqueIdentifier(X509Certificate certificate) { + try { + var cert = new X509CertificateHolder(certificate.getEncoded()); + + var aki = Optional.of(cert) + .map(X509CertificateHolder::getExtensions) + .map(AuthorityKeyIdentifier::fromExtensions) + .map(AuthorityKeyIdentifier::getKeyIdentifier) + .map(AcmeUtils::base64UrlEncode) + .orElseThrow(() -> new AcmeProtocolException("Missing or invalid Authority Key Identifier")); + + var sn = Optional.of(cert) + .map(X509CertificateHolder::toASN1Structure) + .map(Certificate::getSerialNumber) + .map(AcmeUtils::getRawInteger) + .map(AcmeUtils::base64UrlEncode) + .orElseThrow(() -> new AcmeProtocolException("Missing or invalid serial number")); + + return aki + '.' + sn; + } catch (Exception ex) { + throw new AcmeProtocolException("Invalid certificate", ex); + } + } + + /** + * Gets the raw integer array from ASN1Integer. This is done by encoding it to a byte + * array, and then skipping the INTEGER identifier. Other methods of ASN1Integer only + * deliver a parsed integer value that might have been mangled. + * + * @param integer + * ASN1Integer to convert to raw + * @return Byte array of the raw integer + */ + private static byte[] getRawInteger(ASN1Integer integer) { + try { + var encoded = integer.getEncoded(); + return Arrays.copyOfRange(encoded, 2, encoded.length); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } diff --git a/acme4j-client/src/test/resources/ari-example-cert.pem b/acme4j-client/src/test/resources/ari-example-cert.pem new file mode 100644 index 00000000..fe19f8a6 --- /dev/null +++ b/acme4j-client/src/test/resources/ari-example-cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt +cGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS +BgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu +7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf +qzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B +yNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb ++FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK +-----END CERTIFICATE-----