diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java index eb1d7be6..6ecdbd51 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java @@ -39,6 +39,7 @@ import org.shredzone.acme4j.exception.AcmeNetworkException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.util.ClaimBuilder; +import org.shredzone.acme4j.util.DomainUtils; import org.shredzone.acme4j.util.SignatureUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -173,6 +174,8 @@ public class Registration extends AcmeResource { /** * Authorizes a domain. The domain is associated with this registration. + *

+ * IDN domain names will be ACE encoded automatically. * * @param domain * Domain name to be authorized @@ -189,7 +192,7 @@ public class Registration extends AcmeResource { claims.putResource(Resource.NEW_AUTHZ); claims.object("identifier") .put("type", "dns") - .put("value", domain); + .put("value", DomainUtils.toAce(domain)); int rc = conn.sendSignedRequest(getSession().resourceUri(Resource.NEW_AUTHZ), claims, getSession()); if (rc != HttpURLConnection.HTTP_CREATED) { diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/DomainUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/DomainUtils.java new file mode 100644 index 00000000..6c0d745c --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/DomainUtils.java @@ -0,0 +1,48 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 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.util; + +import java.net.IDN; + +/** + * Utility class for domains and domain names. + */ +public final class DomainUtils { + + private DomainUtils() { + // Utility class without constructor + } + + /** + * ASCII encodes a domain name. + *

+ * The conversion is done as described in + * RFC 3490. Additionally, all + * leading and trailing white spaces are trimmed, and the result is lowercased. + *

+ * It is safe to pass in ACE encoded domains, they will be returned unchanged. + * + * @param domain + * Domain name to encode + * @return Encoded domain name, white space trimmed and lower cased. {@code null} if + * {@code null} was passed in. + */ + public static String toAce(String domain) { + if (domain == null) { + return null; + } + return IDN.toASCII(domain.trim()).toLowerCase(); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/DomainUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/DomainUtilsTest.java new file mode 100644 index 00000000..7b64a8a7 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/DomainUtilsTest.java @@ -0,0 +1,54 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 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.util; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +/** + * Unit tests for {@link DomainUtils}. + */ +public class DomainUtilsTest { + + /** + * Test ACE conversion. + */ + @Test + public void testToAce() { + // Test ASCII domains in different notations + assertThat(DomainUtils.toAce("example.com"), is("example.com")); + assertThat(DomainUtils.toAce(" example.com "), is("example.com")); + assertThat(DomainUtils.toAce("ExAmPlE.CoM"), is("example.com")); + assertThat(DomainUtils.toAce("foo.example.com"), is("foo.example.com")); + assertThat(DomainUtils.toAce("bar.foo.example.com"), is("bar.foo.example.com")); + + // Test IDN domains + assertThat(DomainUtils.toAce("ExÄmþle.¢öM"), is("xn--exmle-hra7p.xn--m-7ba6w")); + + // Test alternate separators + assertThat(DomainUtils.toAce("example\u3002com"), is("example.com")); + assertThat(DomainUtils.toAce("example\uff0ecom"), is("example.com")); + assertThat(DomainUtils.toAce("example\uff61com"), is("example.com")); + + // Test ACE encoded domains, they must not change + assertThat(DomainUtils.toAce("xn--exmle-hra7p.xn--m-7ba6w"), + is("xn--exmle-hra7p.xn--m-7ba6w")); + + // Test null + assertThat(DomainUtils.toAce(null), is(nullValue())); + } + +} diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java index 8b008d89..05de5777 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java @@ -59,17 +59,22 @@ public class CSRBuilder { * Common Name. All domain names will be added as Subject * Alternative Name. *

+ * IDN domain names are ACE encoded automatically. + *

* Note that ACME servers may not accept wildcard domains! */ public void addDomain(String domain) { + String ace = DomainUtils.toAce(domain); if (namelist.isEmpty()) { - namebuilder.addRDN(BCStyle.CN, domain); + namebuilder.addRDN(BCStyle.CN, ace); } - namelist.add(domain); + namelist.add(ace); } /** * Adds a {@link Collection} of domains. + *

+ * IDN domain names are ACE encoded automatically. */ public void addDomains(Collection domains) { for (String domain : domains) { @@ -79,6 +84,8 @@ public class CSRBuilder { /** * Adds multiple domain names. + *

+ * IDN domain names are ACE encoded automatically. */ public void addDomains(String... domains) { for (String domain : domains) {