Merge utility classes

pull/30/head
Richard Körber 2016-12-16 01:48:38 +01:00
parent 0a288fa290
commit 25b00313b2
15 changed files with 384 additions and 540 deletions

View File

@ -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<String, Object> jsonIdentifier = (Map<String, Object>) json.get("identifier");

View File

@ -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();

View File

@ -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;
}

View File

@ -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");

View File

@ -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.
* <p>
* The conversion is done as described in
* <a href="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</a>. Additionally, all
* leading and trailing white spaces are trimmed, and the result is lowercased.
* <p>
* 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 <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>
*/
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();
}
}

View File

@ -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.
* <p>
* The conversion is done as described in
* <a href="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</a>. Additionally, all
* leading and trailing white spaces are trimmed, and the result is lowercased.
* <p>
* 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();
}
}

View File

@ -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());
}
}
}

View File

@ -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 <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>
*/
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();
}
}

View File

@ -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(

View File

@ -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()));

View File

@ -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<AcmeUtils> 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<Date> {
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));
}
}
}

View File

@ -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()));
}
}

View File

@ -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"));
}
}

View File

@ -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<TimestampParser> 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<Date> {
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));
}
}
}

View File

@ -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);
}