From 25b00313b2dbcc622c1212fa5dc59d378362ff64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 16 Dec 2016 01:48:38 +0100 Subject: [PATCH] Merge utility classes --- .../org/shredzone/acme4j/Authorization.java | 5 +- .../org/shredzone/acme4j/Registration.java | 8 +- .../shredzone/acme4j/challenge/Challenge.java | 5 +- .../acme4j/connector/DefaultConnection.java | 5 +- .../org/shredzone/acme4j/util/AcmeUtils.java | 125 +++++++++ .../shredzone/acme4j/util/DomainUtils.java | 48 ---- .../shredzone/acme4j/util/SignatureUtils.java | 68 ----- .../acme4j/util/TimestampParser.java | 88 ------- .../shredzone/acme4j/AuthorizationTest.java | 8 +- .../acme4j/challenge/ChallengeTest.java | 4 +- .../shredzone/acme4j/util/AcmeUtilsTest.java | 238 +++++++++++++++++- .../acme4j/util/DomainUtilsTest.java | 54 ---- .../acme4j/util/SignatureUtilsTest.java | 89 ------- .../acme4j/util/TimestampParserTest.java | 175 ------------- .../org/shredzone/acme4j/util/CSRBuilder.java | 4 +- 15 files changed, 384 insertions(+), 540 deletions(-) delete mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/util/DomainUtils.java delete mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/util/SignatureUtils.java delete mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/util/TimestampParser.java delete mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/util/DomainUtilsTest.java delete mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/util/SignatureUtilsTest.java delete mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java index 98c59820..db9f5c0b 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java @@ -13,6 +13,8 @@ */ package org.shredzone.acme4j; +import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; + import java.net.HttpURLConnection; import java.net.URI; import java.util.ArrayList; @@ -28,7 +30,6 @@ import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.util.ClaimBuilder; -import org.shredzone.acme4j.util.TimestampParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -236,7 +237,7 @@ public class Authorization extends AcmeResource { String jsonExpires = (String) json.get("expires"); if (jsonExpires != null) { - expires = TimestampParser.parse(jsonExpires); + expires = parseTimestamp(jsonExpires); } Map jsonIdentifier = (Map) json.get("identifier"); 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 0541b41a..7101f159 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java @@ -13,6 +13,8 @@ */ package org.shredzone.acme4j; +import static org.shredzone.acme4j.util.AcmeUtils.*; + import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; @@ -38,8 +40,6 @@ import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.util.AcmeUtils; 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; @@ -188,7 +188,7 @@ public class Registration extends AcmeResource { claims.putResource(Resource.NEW_AUTHZ); claims.object("identifier") .put("type", "dns") - .put("value", DomainUtils.toAce(domain)); + .put("value", toAce(domain)); conn.sendSignedRequest(getSession().resourceUri(Resource.NEW_AUTHZ), claims, getSession()); conn.accept(HttpURLConnection.HTTP_CREATED); @@ -289,7 +289,7 @@ public class Registration extends AcmeResource { innerJws.setPayload(payloadClaim.toString()); innerJws.getHeaders().setObjectHeaderValue("url", keyChangeUri); innerJws.getHeaders().setJwkHeaderValue("jwk", newKeyJwk); - innerJws.setAlgorithmHeaderValue(SignatureUtils.keyAlgorithm(newKeyJwk)); + innerJws.setAlgorithmHeaderValue(keyAlgorithm(newKeyJwk)); innerJws.setKey(newKeyPair.getPrivate()); innerJws.sign(); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java index cea5e042..618c39ff 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java @@ -13,6 +13,8 @@ */ package org.shredzone.acme4j.challenge; +import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; + import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; @@ -35,7 +37,6 @@ import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.util.AcmeUtils; import org.shredzone.acme4j.util.ClaimBuilder; -import org.shredzone.acme4j.util.TimestampParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -130,7 +131,7 @@ public class Challenge extends AcmeResource { public Date getValidated() { String valStr = get(KEY_VALIDATED); if (valStr != null) { - return TimestampParser.parse(valStr); + return parseTimestamp(valStr); } else { return null; } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index 336c51a9..20dc2862 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -13,6 +13,8 @@ */ package org.shredzone.acme4j.connector; +import static org.shredzone.acme4j.util.AcmeUtils.keyAlgorithm; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -51,7 +53,6 @@ import org.shredzone.acme4j.exception.AcmeServerException; import org.shredzone.acme4j.exception.AcmeUnauthorizedException; import org.shredzone.acme4j.util.AcmeUtils; import org.shredzone.acme4j.util.ClaimBuilder; -import org.shredzone.acme4j.util.SignatureUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -140,7 +141,7 @@ public class DefaultConnection implements Connection { jws.getHeaders().setObjectHeaderValue("nonce", Base64Url.encode(session.getNonce())); jws.getHeaders().setObjectHeaderValue("url", uri); jws.getHeaders().setJwkHeaderValue("jwk", jwk); - jws.setAlgorithmHeaderValue(SignatureUtils.keyAlgorithm(jwk)); + jws.setAlgorithmHeaderValue(keyAlgorithm(jwk)); jws.setKey(keypair.getPrivate()); byte[] outputData = jws.getCompactSerialization().getBytes("utf-8"); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java index 96ce9f67..9ddf54f8 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java @@ -14,10 +14,22 @@ package org.shredzone.acme4j.util; import java.io.UnsupportedEncodingException; +import java.net.IDN; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.jose4j.base64url.Base64Url; +import org.jose4j.jwk.EllipticCurveJsonWebKey; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.RsaJsonWebKey; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; import org.shredzone.acme4j.exception.AcmeProtocolException; /** @@ -29,6 +41,15 @@ import org.shredzone.acme4j.exception.AcmeProtocolException; public final class AcmeUtils { private static final char[] HEX = "0123456789abcdef".toCharArray(); + private static final Pattern DATE_PATTERN = Pattern.compile( + "^(\\d{4})-(\\d{2})-(\\d{2})T" + + "(\\d{2}):(\\d{2}):(\\d{2})" + + "(?:\\.(\\d{1,3})\\d*)?" + + "(Z|[+-]\\d{2}:?\\d{2})$", Pattern.CASE_INSENSITIVE); + + private static final Pattern TZ_PATTERN = Pattern.compile( + "([+-])(\\d{2}):?(\\d{2})$"); + private AcmeUtils() { // Utility class without constructor } @@ -93,4 +114,108 @@ public final class AcmeUtils { } } + /** + * 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(); + } + + /** + * Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm + * identifier for {@link JsonWebSignature}. + * + * @param jwk + * {@link JsonWebKey} to analyze + * @return algorithm identifier + * @throws IllegalArgumentException + * there is no corresponding algorithm identifier for the key + */ + public static String keyAlgorithm(JsonWebKey jwk) { + if (jwk instanceof EllipticCurveJsonWebKey) { + EllipticCurveJsonWebKey ecjwk = (EllipticCurveJsonWebKey) jwk; + + switch (ecjwk.getCurveName()) { + case "P-256": + return AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256; + + case "P-384": + return AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384; + + case "P-521": + return AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512; + + default: + throw new IllegalArgumentException("Unknown EC name " + + ecjwk.getCurveName()); + } + + } else if (jwk instanceof RsaJsonWebKey) { + return AlgorithmIdentifiers.RSA_USING_SHA256; + + } else { + throw new IllegalArgumentException("Unknown algorithm " + jwk.getAlgorithm()); + } + } + + /** + * Parses a RFC 3339 formatted date. + * + * @param str + * Date string + * @return {@link Date} that was parsed + * @throws IllegalArgumentException + * if the date string was not RFC 3339 formatted + * @see RFC 3339 + */ + public static Date parseTimestamp(String str) { + Matcher m = DATE_PATTERN.matcher(str); + if (!m.matches()) { + throw new IllegalArgumentException("Illegal date: " + str); + } + + int year = Integer.parseInt(m.group(1)); + int month = Integer.parseInt(m.group(2)); + int dom = Integer.parseInt(m.group(3)); + int hour = Integer.parseInt(m.group(4)); + int minute = Integer.parseInt(m.group(5)); + int second = Integer.parseInt(m.group(6)); + + StringBuilder msStr = new StringBuilder(); + if (m.group(7) != null) { + msStr.append(m.group(7)); + } + while (msStr.length() < 3) { + msStr.append('0'); + } + int ms = Integer.parseInt(msStr.toString()); + + String tz = m.group(8); + if ("Z".equalsIgnoreCase(tz)) { + tz = "GMT"; + } else { + tz = TZ_PATTERN.matcher(tz).replaceAll("GMT$1$2:$3"); + } + + Calendar cal = GregorianCalendar.getInstance(TimeZone.getTimeZone(tz)); + cal.clear(); + cal.set(year, month - 1, dom, hour, minute, second); + cal.set(Calendar.MILLISECOND, ms); + return cal.getTime(); + } + } 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 deleted file mode 100644 index 6c0d745c..00000000 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/DomainUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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/main/java/org/shredzone/acme4j/util/SignatureUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/SignatureUtils.java deleted file mode 100644 index e1afc35a..00000000 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/SignatureUtils.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 org.jose4j.jwk.EllipticCurveJsonWebKey; -import org.jose4j.jwk.JsonWebKey; -import org.jose4j.jwk.RsaJsonWebKey; -import org.jose4j.jws.AlgorithmIdentifiers; -import org.jose4j.jws.JsonWebSignature; - -/** - * Utility class for signatures. - */ -public final class SignatureUtils { - - private SignatureUtils() { - // Utility class without constructor - } - - /** - * Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm - * identifier for {@link JsonWebSignature}. - * - * @param jwk - * {@link JsonWebKey} to analyze - * @return algorithm identifier - * @throws IllegalArgumentException - * there is no corresponding algorithm identifier for the key - */ - public static String keyAlgorithm(JsonWebKey jwk) { - if (jwk instanceof EllipticCurveJsonWebKey) { - EllipticCurveJsonWebKey ecjwk = (EllipticCurveJsonWebKey) jwk; - - switch (ecjwk.getCurveName()) { - case "P-256": - return AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256; - - case "P-384": - return AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384; - - case "P-521": - return AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512; - - default: - throw new IllegalArgumentException("Unknown EC name " - + ecjwk.getCurveName()); - } - - } else if (jwk instanceof RsaJsonWebKey) { - return AlgorithmIdentifiers.RSA_USING_SHA256; - - } else { - throw new IllegalArgumentException("Unknown algorithm " + jwk.getAlgorithm()); - } - } - -} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/TimestampParser.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/TimestampParser.java deleted file mode 100644 index 285d54cb..00000000 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/TimestampParser.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * acme4j - Java ACME client - * - * Copyright (C) 2015 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.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Parses a timestamp as defined in RFC 3339. - * - * @see RFC 3339 - */ -public final class TimestampParser { - - private static final Pattern DATE_PATTERN = Pattern.compile( - "^(\\d{4})-(\\d{2})-(\\d{2})T" - + "(\\d{2}):(\\d{2}):(\\d{2})" - + "(?:\\.(\\d{1,3})\\d*)?" - + "(Z|[+-]\\d{2}:?\\d{2})$", Pattern.CASE_INSENSITIVE); - - private static final Pattern TZ_PATTERN = Pattern.compile( - "([+-])(\\d{2}):?(\\d{2})$"); - - private TimestampParser() { - // Utility class without constructor - } - - /** - * Parses a RFC 3339 formatted date. - * - * @param str - * Date string - * @return {@link Date} that was parsed - * @throws IllegalArgumentException - * if the date string was not RFC 3339 formatted - */ - public static Date parse(String str) { - Matcher m = DATE_PATTERN.matcher(str); - if (!m.matches()) { - throw new IllegalArgumentException("Illegal date: " + str); - } - - int year = Integer.parseInt(m.group(1)); - int month = Integer.parseInt(m.group(2)); - int dom = Integer.parseInt(m.group(3)); - int hour = Integer.parseInt(m.group(4)); - int minute = Integer.parseInt(m.group(5)); - int second = Integer.parseInt(m.group(6)); - - StringBuilder msStr = new StringBuilder(); - if (m.group(7) != null) { - msStr.append(m.group(7)); - } - while (msStr.length() < 3) { - msStr.append('0'); - } - int ms = Integer.parseInt(msStr.toString()); - - String tz = m.group(8); - if ("Z".equalsIgnoreCase(tz)) { - tz = "GMT"; - } else { - tz = TZ_PATTERN.matcher(tz).replaceAll("GMT$1$2:$3"); - } - - Calendar cal = GregorianCalendar.getInstance(TimeZone.getTimeZone(tz)); - cal.clear(); - cal.set(year, month - 1, dom, hour, minute, second); - cal.set(Calendar.MILLISECOND, ms); - return cal.getTime(); - } - -} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java index ff2ab6cc..8dfbcc59 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; import static org.shredzone.acme4j.util.TestUtils.*; import java.io.IOException; @@ -34,7 +35,6 @@ import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.provider.TestableConnectionProvider; import org.shredzone.acme4j.util.ClaimBuilder; -import org.shredzone.acme4j.util.TimestampParser; /** * Unit tests for {@link Authorization}. @@ -149,7 +149,7 @@ public class AuthorizationTest { assertThat(auth.getDomain(), is("example.org")); assertThat(auth.getStatus(), is(Status.VALID)); - assertThat(auth.getExpires(), is(TimestampParser.parse("2016-01-02T17:12:40Z"))); + assertThat(auth.getExpires(), is(parseTimestamp("2016-01-02T17:12:40Z"))); assertThat(auth.getLocation(), is(locationUri)); assertThat(auth.getChallenges(), containsInAnyOrder( @@ -207,7 +207,7 @@ public class AuthorizationTest { requestWasSent.set(false); assertThat(auth.getDomain(), is("example.org")); assertThat(auth.getStatus(), is(Status.VALID)); - assertThat(auth.getExpires(), is(TimestampParser.parse("2016-01-02T17:12:40Z"))); + assertThat(auth.getExpires(), is(parseTimestamp("2016-01-02T17:12:40Z"))); assertThat(requestWasSent.get(), is(false)); provider.close(); @@ -262,7 +262,7 @@ public class AuthorizationTest { assertThat(auth.getDomain(), is("example.org")); assertThat(auth.getStatus(), is(Status.VALID)); - assertThat(auth.getExpires(), is(TimestampParser.parse("2016-01-02T17:12:40Z"))); + assertThat(auth.getExpires(), is(parseTimestamp("2016-01-02T17:12:40Z"))); assertThat(auth.getLocation(), is(locationUri)); assertThat(auth.getChallenges(), containsInAnyOrder( diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java index de1fcfa1..20d34a58 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.challenge; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; import static org.shredzone.acme4j.util.TestUtils.*; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; @@ -43,7 +44,6 @@ import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.provider.TestableConnectionProvider; import org.shredzone.acme4j.util.ClaimBuilder; import org.shredzone.acme4j.util.TestUtils; -import org.shredzone.acme4j.util.TimestampParser; /** * Unit tests for {@link Challenge}. @@ -116,7 +116,7 @@ public class ChallengeTest { assertThat(challenge.getType(), is("generic-01")); assertThat(challenge.getStatus(), is(Status.VALID)); assertThat(challenge.getLocation(), is(new URI("http://example.com/challenge/123"))); - assertThat(challenge.getValidated(), is(TimestampParser.parse("2015-12-12T17:19:36.336785823Z"))); + assertThat(challenge.getValidated(), is(parseTimestamp("2015-12-12T17:19:36.336785823Z"))); assertThat((String) challenge.get("type"), is("generic-01")); assertThat(challenge.getUrl("uri"), is(new URL("http://example.com/challenge/123"))); assertThat(challenge.get("not-present"), is(nullValue())); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java index c626616f..60313656 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java @@ -13,12 +13,27 @@ */ package org.shredzone.acme4j.util; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.shredzone.acme4j.util.AcmeUtils.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.security.KeyPair; +import java.security.Security; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + import javax.xml.bind.DatatypeConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.jose4j.jwk.PublicJsonWebKey; +import org.junit.BeforeClass; import org.junit.Test; /** @@ -26,6 +41,22 @@ import org.junit.Test; */ public class AcmeUtilsTest { + @BeforeClass + public static void setup() { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * Test that constructor is private. + */ + @Test + public void testPrivateConstructor() throws Exception { + Constructor constructor = AcmeUtils.class.getDeclaredConstructor(); + assertThat(Modifier.isPrivate(constructor.getModifiers()), is(true)); + constructor.setAccessible(true); + constructor.newInstance(); + } + /** * Test sha-256 hash. */ @@ -69,4 +100,209 @@ public class AcmeUtilsTest { } } + /** + * Test ACE conversion. + */ + @Test + public void testToAce() { + // Test ASCII domains in different notations + assertThat(toAce("example.com"), is("example.com")); + assertThat(toAce(" example.com "), is("example.com")); + assertThat(toAce("ExAmPlE.CoM"), is("example.com")); + assertThat(toAce("foo.example.com"), is("foo.example.com")); + assertThat(toAce("bar.foo.example.com"), is("bar.foo.example.com")); + + // Test IDN domains + assertThat(toAce("ExÄmþle.¢öM"), is("xn--exmle-hra7p.xn--m-7ba6w")); + + // Test alternate separators + assertThat(toAce("example\u3002com"), is("example.com")); + assertThat(toAce("example\uff0ecom"), is("example.com")); + assertThat(toAce("example\uff61com"), is("example.com")); + + // Test ACE encoded domains, they must not change + assertThat(toAce("xn--exmle-hra7p.xn--m-7ba6w"), + is("xn--exmle-hra7p.xn--m-7ba6w")); + + // Test null + assertThat(toAce(null), is(nullValue())); + } + + /** + * Test if RSA using SHA-256 keys are properly detected. + */ + @Test + public void testRsaKey() throws Exception { + KeyPair rsaKeyPair = TestUtils.createKeyPair(); + final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(rsaKeyPair.getPublic()); + + String type = keyAlgorithm(jwk); + + assertThat(type, is("RS256")); + } + + /** + * Test if ECDSA using NIST P-256 curve and SHA-256 keys are properly detected. + */ + @Test + public void testP256ECKey() throws Exception { + KeyPair ecKeyPair = TestUtils.createECKeyPair("secp256r1"); + final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); + + String type = keyAlgorithm(jwk); + + assertThat(type, is("ES256")); + } + + /** + * Test if ECDSA using NIST P-384 curve and SHA-384 keys are properly detected. + */ + @Test + public void testP384ECKey() throws Exception { + KeyPair ecKeyPair = TestUtils.createECKeyPair("secp384r1"); + final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); + + String type = keyAlgorithm(jwk); + + assertThat(type, is("ES384")); + } + + /** + * Test if ECDSA using NIST P-521 curve and SHA-512 keys are properly detected. + */ + @Test + public void testP521ECKey() throws Exception { + KeyPair ecKeyPair = TestUtils.createECKeyPair("secp521r1"); + final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); + + String type = keyAlgorithm(jwk); + + assertThat(type, is("ES512")); + } + + /** + * Test valid strings. + */ + @Test + public void testParser() { + assertThat(parseTimestamp("2015-12-27T22:58:35.006769519Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.00676951Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.0067695Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.006769Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.00676Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.0067Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.006Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.01Z"), isDate(2015, 12, 27, 22, 58, 35, 10)); + assertThat(parseTimestamp("2015-12-27T22:58:35.2Z"), isDate(2015, 12, 27, 22, 58, 35, 200)); + assertThat(parseTimestamp("2015-12-27T22:58:35Z"), isDate(2015, 12, 27, 22, 58, 35)); + assertThat(parseTimestamp("2015-12-27t22:58:35z"), isDate(2015, 12, 27, 22, 58, 35)); + + assertThat(parseTimestamp("2015-12-27T22:58:35.006769519+02:00"), isDate(2015, 12, 27, 20, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.006+02:00"), isDate(2015, 12, 27, 20, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35+02:00"), isDate(2015, 12, 27, 20, 58, 35)); + + assertThat(parseTimestamp("2015-12-27T21:58:35.006769519-02:00"), isDate(2015, 12, 27, 23, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T21:58:35.006-02:00"), isDate(2015, 12, 27, 23, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T21:58:35-02:00"), isDate(2015, 12, 27, 23, 58, 35)); + + assertThat(parseTimestamp("2015-12-27T22:58:35.006769519+0200"), isDate(2015, 12, 27, 20, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35.006+0200"), isDate(2015, 12, 27, 20, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T22:58:35+0200"), isDate(2015, 12, 27, 20, 58, 35)); + + assertThat(parseTimestamp("2015-12-27T21:58:35.006769519-0200"), isDate(2015, 12, 27, 23, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T21:58:35.006-0200"), isDate(2015, 12, 27, 23, 58, 35, 6)); + assertThat(parseTimestamp("2015-12-27T21:58:35-0200"), isDate(2015, 12, 27, 23, 58, 35)); + } + + /** + * Test invalid strings. + */ + @Test + public void testInvalid() { + try { + parseTimestamp(""); + fail("accepted empty string"); + } catch (IllegalArgumentException ex) { + // expected + } + + try { + parseTimestamp("abc"); + fail("accepted nonsense string"); + } catch (IllegalArgumentException ex) { + // expected + } + + try { + parseTimestamp("2015-12-27"); + fail("accepted year only string"); + } catch (IllegalArgumentException ex) { + // expected + } + + try { + parseTimestamp("2015-12-27T"); + fail("accepted year only string"); + } catch (IllegalArgumentException ex) { + // expected + } + } + + /** + * Matches the given time. + */ + private DateMatcher isDate(int year, int month, int dom, int hour, int minute, int second) { + return isDate(year, month, dom, hour, minute, second, 0); + } + + /** + * Matches the given time and milliseconds. + */ + private DateMatcher isDate(int year, int month, int dom, int hour, int minute, int second, int ms) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.clear(); + cal.set(year, month - 1, dom, hour, minute, second); + cal.set(Calendar.MILLISECOND, ms); + return new DateMatcher(cal); + } + + /** + * Date matcher that gives a readable output on mismatch. + */ + private static class DateMatcher extends BaseMatcher { + private final Calendar cal; + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH); + + public DateMatcher(Calendar cal) { + this.cal = cal; + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + @Override + public boolean matches(Object item) { + if (!(item instanceof Date)) { + return false; + } + + Date date = (Date) item; + return date.equals(cal.getTime()); + } + + @Override + public void describeTo(Description description) { + description.appendValue(sdf.format(cal.getTime())); + } + + @Override + public void describeMismatch(Object item, Description description) { + if (!(item instanceof Date)) { + description.appendText("is not a Date"); + return; + } + + Date date = (Date) item; + description.appendText("was ").appendValue(sdf.format(date)); + } + } + } 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 deleted file mode 100644 index 7b64a8a7..00000000 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/DomainUtilsTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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-client/src/test/java/org/shredzone/acme4j/util/SignatureUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/SignatureUtilsTest.java deleted file mode 100644 index d665bbc1..00000000 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/SignatureUtilsTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.is; -import static org.junit.Assert.assertThat; - -import java.security.KeyPair; -import java.security.Security; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.jose4j.jwk.PublicJsonWebKey; -import org.junit.BeforeClass; -import org.junit.Test; - -/** - * Unit tests for {@link SignatureUtils}. - */ -public class SignatureUtilsTest { - - @BeforeClass - public static void setup() { - Security.addProvider(new BouncyCastleProvider()); - } - - /** - * Test if RSA using SHA-256 keys are properly detected. - */ - @Test - public void testRsaKey() throws Exception { - KeyPair rsaKeyPair = TestUtils.createKeyPair(); - final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(rsaKeyPair.getPublic()); - - String type = SignatureUtils.keyAlgorithm(jwk); - - assertThat(type, is("RS256")); - } - - /** - * Test if ECDSA using NIST P-256 curve and SHA-256 keys are properly detected. - */ - @Test - public void testP256ECKey() throws Exception { - KeyPair ecKeyPair = TestUtils.createECKeyPair("secp256r1"); - final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); - - String type = SignatureUtils.keyAlgorithm(jwk); - - assertThat(type, is("ES256")); - } - - /** - * Test if ECDSA using NIST P-384 curve and SHA-384 keys are properly detected. - */ - @Test - public void testP384ECKey() throws Exception { - KeyPair ecKeyPair = TestUtils.createECKeyPair("secp384r1"); - final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); - - String type = SignatureUtils.keyAlgorithm(jwk); - - assertThat(type, is("ES384")); - } - - /** - * Test if ECDSA using NIST P-521 curve and SHA-512 keys are properly detected. - */ - @Test - public void testP521ECKey() throws Exception { - KeyPair ecKeyPair = TestUtils.createECKeyPair("secp521r1"); - final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(ecKeyPair.getPublic()); - - String type = SignatureUtils.keyAlgorithm(jwk); - - assertThat(type, is("ES512")); - } - -} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java deleted file mode 100644 index 2639268d..00000000 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TimestampParserTest.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * acme4j - Java ACME client - * - * Copyright (C) 2015 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.is; -import static org.junit.Assert.*; -import static org.shredzone.acme4j.util.TimestampParser.parse; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.junit.Test; - -/** - * Unit tests for {@link TimestampParser}. - */ -public class TimestampParserTest { - - /** - * Test valid strings. - */ - @Test - public void testParser() { - assertThat(parse("2015-12-27T22:58:35.006769519Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.00676951Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.0067695Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.006769Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.00676Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.0067Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.006Z"), isDate(2015, 12, 27, 22, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.01Z"), isDate(2015, 12, 27, 22, 58, 35, 10)); - assertThat(parse("2015-12-27T22:58:35.2Z"), isDate(2015, 12, 27, 22, 58, 35, 200)); - assertThat(parse("2015-12-27T22:58:35Z"), isDate(2015, 12, 27, 22, 58, 35)); - assertThat(parse("2015-12-27t22:58:35z"), isDate(2015, 12, 27, 22, 58, 35)); - - assertThat(parse("2015-12-27T22:58:35.006769519+02:00"), isDate(2015, 12, 27, 20, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.006+02:00"), isDate(2015, 12, 27, 20, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35+02:00"), isDate(2015, 12, 27, 20, 58, 35)); - - assertThat(parse("2015-12-27T21:58:35.006769519-02:00"), isDate(2015, 12, 27, 23, 58, 35, 6)); - assertThat(parse("2015-12-27T21:58:35.006-02:00"), isDate(2015, 12, 27, 23, 58, 35, 6)); - assertThat(parse("2015-12-27T21:58:35-02:00"), isDate(2015, 12, 27, 23, 58, 35)); - - assertThat(parse("2015-12-27T22:58:35.006769519+0200"), isDate(2015, 12, 27, 20, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35.006+0200"), isDate(2015, 12, 27, 20, 58, 35, 6)); - assertThat(parse("2015-12-27T22:58:35+0200"), isDate(2015, 12, 27, 20, 58, 35)); - - assertThat(parse("2015-12-27T21:58:35.006769519-0200"), isDate(2015, 12, 27, 23, 58, 35, 6)); - assertThat(parse("2015-12-27T21:58:35.006-0200"), isDate(2015, 12, 27, 23, 58, 35, 6)); - assertThat(parse("2015-12-27T21:58:35-0200"), isDate(2015, 12, 27, 23, 58, 35)); - } - - /** - * Test invalid strings. - */ - @Test - public void testInvalid() { - try { - parse(""); - fail("accepted empty string"); - } catch (IllegalArgumentException ex) { - // expected - } - - try { - parse("abc"); - fail("accepted nonsense string"); - } catch (IllegalArgumentException ex) { - // expected - } - - try { - parse("2015-12-27"); - fail("accepted year only string"); - } catch (IllegalArgumentException ex) { - // expected - } - - try { - parse("2015-12-27T"); - fail("accepted year only string"); - } catch (IllegalArgumentException ex) { - // expected - } - } - - /** - * Test that constructor is private. - */ - @Test - public void testPrivateConstructor() throws Exception { - Constructor constructor = TimestampParser.class.getDeclaredConstructor(); - assertThat(Modifier.isPrivate(constructor.getModifiers()), is(true)); - constructor.setAccessible(true); - constructor.newInstance(); - } - - /** - * Matches the given time. - */ - private DateMatcher isDate(int year, int month, int dom, int hour, int minute, int second) { - return isDate(year, month, dom, hour, minute, second, 0); - } - - /** - * Matches the given time and milliseconds. - */ - private DateMatcher isDate(int year, int month, int dom, int hour, int minute, int second, int ms) { - Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - cal.clear(); - cal.set(year, month - 1, dom, hour, minute, second); - cal.set(Calendar.MILLISECOND, ms); - return new DateMatcher(cal); - } - - /** - * Date matcher that gives a readable output on mismatch. - */ - private static class DateMatcher extends BaseMatcher { - - private final Calendar cal; - private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH); - - public DateMatcher(Calendar cal) { - this.cal = cal; - sdf.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - @Override - public boolean matches(Object item) { - if (!(item instanceof Date)) { - return false; - } - - Date date = (Date) item; - return date.equals(cal.getTime()); - } - - @Override - public void describeTo(Description description) { - description.appendValue(sdf.format(cal.getTime())); - } - - @Override - public void describeMismatch(Object item, Description description) { - if (!(item instanceof Date)) { - description.appendText("is not a Date"); - return; - } - - Date date = (Date) item; - description.appendText("was ").appendValue(sdf.format(date)); - } - - } - -} 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 97af4d95..49daf9de 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 @@ -13,6 +13,8 @@ */ package org.shredzone.acme4j.util; +import static org.shredzone.acme4j.util.AcmeUtils.toAce; + import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -64,7 +66,7 @@ public class CSRBuilder { * Note that ACME servers may not accept wildcard domains! */ public void addDomain(String domain) { - String ace = DomainUtils.toAce(domain); + String ace = toAce(domain); if (namelist.isEmpty()) { namebuilder.addRDN(BCStyle.CN, ace); }