From 2ca2f4b26463fc2524a5917115db4e04b8f449e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Wed, 13 Aug 2025 20:48:16 +0200 Subject: [PATCH] Add Actalis support (fixes #173) --- README.md | 2 +- acme4j-client/src/main/java/module-info.java | 1 + .../provider/actalis/ActalisAcmeProvider.java | 87 +++++++++++++++++++ .../acme4j/provider/actalis/package-info.java | 29 +++++++ ...org.shredzone.acme4j.provider.AcmeProvider | 7 +- .../actalis/ActalisAcmeProviderTest.java | 59 +++++++++++++ .../org/shredzone/acme4j/it/ProviderIT.java | 13 +++ src/doc/docs/ca/actalis.md | 19 ++++ src/doc/docs/ca/index.md | 1 + src/doc/docs/index.md | 2 +- src/doc/mkdocs.yml | 1 + 11 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProvider.java create mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/package-info.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProviderTest.java create mode 100644 src/doc/docs/ca/actalis.md diff --git a/README.md b/README.md index ac8242e1..7b55393b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This Java client helps to connect to an ACME server, and performing all necessar * Supports [draft-ietf-acme-dns-account-label-00](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/) for DNS labeled with ACME account ID challenges (experimental) * Easy to use Java API * Requires JRE 17 or higher -* Supports [Buypass](https://buypass.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and all other CAs that comply with the ACME protocol (RFC 8555). Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs. +* Supports [Actalis](https://www.actalis.com/), [Buypass](https://buypass.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and **all other CAs that comply with the ACME protocol (RFC 8555)**. Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs. * Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22) * Extensive unit and integration tests * Adheres to [Semantic Versioning](https://semver.org/) diff --git a/acme4j-client/src/main/java/module-info.java b/acme4j-client/src/main/java/module-info.java index 54f1c363..10f228d1 100644 --- a/acme4j-client/src/main/java/module-info.java +++ b/acme4j-client/src/main/java/module-info.java @@ -36,6 +36,7 @@ module org.shredzone.acme4j { provides org.shredzone.acme4j.provider.AcmeProvider with org.shredzone.acme4j.provider.GenericAcmeProvider, + org.shredzone.acme4j.provider.actalis.ActalisAcmeProvider, org.shredzone.acme4j.provider.buypass.BuypassAcmeProvider, org.shredzone.acme4j.provider.google.GoogleAcmeProvider, org.shredzone.acme4j.provider.letsencrypt.LetsEncryptAcmeProvider, diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProvider.java new file mode 100644 index 00000000..4f9818e3 --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProvider.java @@ -0,0 +1,87 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2025 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.provider.actalis; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Map; + +import org.shredzone.acme4j.Session; +import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.exception.AcmeProtocolException; +import org.shredzone.acme4j.provider.AbstractAcmeProvider; +import org.shredzone.acme4j.provider.AcmeProvider; +import org.shredzone.acme4j.toolbox.JSON; + +/** + * An {@link AcmeProvider} for Actalis. + *

+ * The {@code serverUri} is {@code "acme://actalis.com"} for the production server. + *

+ * If you want to use Actalis, always prefer to use this provider. + * + * @see Actalis S.p.A. + * @since 4.0.0 + */ +public class ActalisAcmeProvider extends AbstractAcmeProvider { + + private static final String PRODUCTION_DIRECTORY_URL = "https://acme-api.actalis.com/acme/directory"; + + @Override + public boolean accepts(URI serverUri) { + return "acme".equals(serverUri.getScheme()) + && "actalis.com".equals(serverUri.getHost()); + } + + @Override + public URL resolve(URI serverUri) { + var path = serverUri.getPath(); + String directoryUrl; + if (path == null || path.isEmpty() || "/".equals(path)) { + directoryUrl = PRODUCTION_DIRECTORY_URL; + } else { + throw new IllegalArgumentException("Unknown URI " + serverUri); + } + + try { + return URI.create(directoryUrl).toURL(); + } catch (MalformedURLException ex) { + throw new AcmeProtocolException(directoryUrl, ex); + } + } + + @Override + @SuppressWarnings("unchecked") + public JSON directory(Session session, URI serverUri) throws AcmeException { + // This is a workaround as actalis.com uses "home" instead of "website" to + // refer to its homepage in the metadata. + var superdirectory = super.directory(session, serverUri); + if (superdirectory == null) { + return null; + } + + var directory = superdirectory.toMap(); + var meta = directory.get("meta"); + if (meta instanceof Map) { + var metaMap = ((Map) meta); + if (metaMap.containsKey("home") && !metaMap.containsKey("website")) { + metaMap.put("website", metaMap.remove("home")); + } + } + return JSON.fromMap(directory); + } + + +} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/package-info.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/package-info.java new file mode 100644 index 00000000..2df15ca2 --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/actalis/package-info.java @@ -0,0 +1,29 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2025 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * This package contains an {@link org.shredzone.acme4j.provider.AcmeProvider} for the + * Actalis server. + * + * @see Actalis S.p.A. + */ +@ReturnValuesAreNonnullByDefault +@DefaultAnnotationForParameters(NonNull.class) +@DefaultAnnotationForFields(NonNull.class) +package org.shredzone.acme4j.provider.actalis; + +import edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields; +import edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; diff --git a/acme4j-client/src/main/resources/META-INF/services/org.shredzone.acme4j.provider.AcmeProvider b/acme4j-client/src/main/resources/META-INF/services/org.shredzone.acme4j.provider.AcmeProvider index 30818632..48b80b59 100644 --- a/acme4j-client/src/main/resources/META-INF/services/org.shredzone.acme4j.provider.AcmeProvider +++ b/acme4j-client/src/main/resources/META-INF/services/org.shredzone.acme4j.provider.AcmeProvider @@ -1,8 +1,11 @@ -# Buypass: https://buypass.com/ +# Actalis: https://www.actalis.com/ +org.shredzone.acme4j.provider.actalis.ActalisAcmeProvider + +# Buypass: https://buypass.com/ org.shredzone.acme4j.provider.buypass.BuypassAcmeProvider -# Google Trust Services: https://pki.goog/ +# Google Trust Services: https://pki.goog/ org.shredzone.acme4j.provider.google.GoogleAcmeProvider # Let's Encrypt: https://letsencrypt.org diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProviderTest.java new file mode 100644 index 00000000..6529e932 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/actalis/ActalisAcmeProviderTest.java @@ -0,0 +1,59 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2025 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.provider.actalis; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.shredzone.acme4j.toolbox.TestUtils.url; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.assertj.core.api.AutoCloseableSoftAssertions; +import org.junit.jupiter.api.Test; + +public class ActalisAcmeProviderTest { + + private static final String PRODUCTION_DIRECTORY_URL = "https://acme-api.actalis.com/acme/directory"; + + /** + * Tests if the provider accepts the correct URIs. + */ + @Test + public void testAccepts() throws URISyntaxException { + var provider = new ActalisAcmeProvider(); + + try (var softly = new AutoCloseableSoftAssertions()) { + softly.assertThat(provider.accepts(new URI("acme://actalis.com"))).isTrue(); + softly.assertThat(provider.accepts(new URI("acme://actalis.com/"))).isTrue(); + softly.assertThat(provider.accepts(new URI("acme://example.com"))).isFalse(); + softly.assertThat(provider.accepts(new URI("http://example.com/acme"))).isFalse(); + softly.assertThat(provider.accepts(new URI("https://example.com/acme"))).isFalse(); + } + } + + /** + * Test if acme URIs are properly resolved. + */ + @Test + public void testResolve() throws URISyntaxException { + var provider = new ActalisAcmeProvider(); + + assertThat(provider.resolve(new URI("acme://actalis.com"))).isEqualTo(url(PRODUCTION_DIRECTORY_URL)); + assertThat(provider.resolve(new URI("acme://actalis.com/"))).isEqualTo(url(PRODUCTION_DIRECTORY_URL)); + + assertThrows(IllegalArgumentException.class, () -> provider.resolve(new URI("acme://letsencrypt.org/v99"))); + } + +} \ No newline at end of file diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java index fcbeccc4..cddabde1 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/ProviderIT.java @@ -37,6 +37,19 @@ import org.shredzone.acme4j.exception.AcmeException; */ public class ProviderIT { + /** + * Test Actalis + */ + @Test + public void testActalis() throws AcmeException, MalformedURLException { + var session = new Session("acme://actalis.com"); + assertThat(session.getMetadata().getWebsite()).hasValue(URI.create("https://www.actalis.com").toURL()); + assertThatNoException().isThrownBy(() -> session.resourceUrl(Resource.NEW_ACCOUNT)); + assertThat(session.getMetadata().isExternalAccountRequired()).isTrue(); + assertThat(session.getMetadata().isAutoRenewalEnabled()).isFalse(); + assertThat(session.resourceUrlOptional(Resource.RENEWAL_INFO)).isNotEmpty(); + } + /** * Test Buypass */ diff --git a/src/doc/docs/ca/actalis.md b/src/doc/docs/ca/actalis.md new file mode 100644 index 00000000..7014a183 --- /dev/null +++ b/src/doc/docs/ca/actalis.md @@ -0,0 +1,19 @@ +# Actalis + +Website: [Actalis](https://www.actalis.com) + +Available since acme4j 4.0.0 + +## Connection URIs + +* `acme://actalis.com` - Production server + +Actalis does not provide a staging server (as of August 2025). + +## Note + +* Actalis requires account creation with [key identifier](../usage/account.md#external-account-binding). + +## Disclaimer + +_acme4j_ is not officially supported or endorsed by Actalis. If you have _acme4j_ related issues, please do not ask them for support, but [open an issue here](https://codeberg.org/shred/acme4j/issues). diff --git a/src/doc/docs/ca/index.md b/src/doc/docs/ca/index.md index 2c949f00..c120e5ab 100644 --- a/src/doc/docs/ca/index.md +++ b/src/doc/docs/ca/index.md @@ -9,6 +9,7 @@ _acme4j_ should support any CA that is providing an ACME server. The _acme4j_ package contains these providers (in alphabetical order): +* [Actalis](actalis.md) * [Buypass](buypass.md) * [Google](google.md) * [Let's Encrypt](letsencrypt.md) diff --git a/src/doc/docs/index.md b/src/doc/docs/index.md index 2714e30e..1859db83 100644 --- a/src/doc/docs/index.md +++ b/src/doc/docs/index.md @@ -24,7 +24,7 @@ Latest version: ![maven central](https://shredzone.org/maven-central/org.shredzo * Supports [draft-ietf-acme-dns-account-label-00](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-account-label/) for DNS labeled with ACME account ID challenges (experimental) * Easy to use Java API * Requires JRE 17 or higher -* Supports [Buypass](https://buypass.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and all other CAs that comply with the ACME protocol (RFC 8555). Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs. +* Supports [Actalis](https://www.actalis.com/), [Buypass](https://buypass.com/), [Google Trust Services](https://pki.goog/), [Let's Encrypt](https://letsencrypt.org/), [SSL.com](https://www.ssl.com/), [ZeroSSL](https://zerossl.com/), and **all other CAs that comply with the ACME protocol (RFC 8555)**. Note that _acme4j_ is an independent project that is not supported or endorsed by any of the CAs. * Built with maven, packages available at [Maven Central](http://search.maven.org/#search|ga|1|g%3A%22org.shredzone.acme4j%22) * Extensive unit and integration tests * Adheres to [Semantic Versioning](https://semver.org/) diff --git a/src/doc/mkdocs.yml b/src/doc/mkdocs.yml index 9660fbd4..c7bc7271 100644 --- a/src/doc/mkdocs.yml +++ b/src/doc/mkdocs.yml @@ -44,6 +44,7 @@ nav: - 'challenge/tls-alpn-01.md' - CA: - 'ca/index.md' + - 'ca/actalis.md' - 'ca/buypass.md' - 'ca/google.md' - 'ca/letsencrypt.md'