diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Dns01Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Dns01Challenge.java index cda83946..ae2f84d7 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Dns01Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Dns01Challenge.java @@ -13,13 +13,9 @@ */ package org.shredzone.acme4j.challenge; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import static org.shredzone.acme4j.util.AcmeUtils.*; -import org.jose4j.base64url.Base64Url; import org.shredzone.acme4j.Session; -import org.shredzone.acme4j.exception.AcmeProtocolException; /** * Implements the {@value TYPE} challenge. @@ -47,14 +43,7 @@ public class Dns01Challenge extends TokenChallenge { * record. */ public String getDigest() { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(getAuthorization().getBytes("UTF-8")); - byte[] digest = md.digest(); - return Base64Url.encode(digest); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { - throw new AcmeProtocolException("Failed to compute digest", ex); - } + return base64UrlEncode(sha256hash(getAuthorization())); } @Override diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni01Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni01Challenge.java index 56b8e0d6..6ab04f5f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni01Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni01Challenge.java @@ -13,12 +13,9 @@ */ package org.shredzone.acme4j.challenge; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import static org.shredzone.acme4j.util.AcmeUtils.*; import org.shredzone.acme4j.Session; -import org.shredzone.acme4j.exception.AcmeProtocolException; /** * Implements the {@value TYPE} challenge. @@ -30,7 +27,6 @@ import org.shredzone.acme4j.exception.AcmeProtocolException; @Deprecated public class TlsSni01Challenge extends TokenChallenge { private static final long serialVersionUID = 7370329525205430573L; - private static final char[] HEX = "0123456789abcdef".toCharArray(); /** * Challenge type name: {@value} @@ -65,32 +61,8 @@ public class TlsSni01Challenge extends TokenChallenge { protected void authorize() { super.authorize(); - String hash = computeHash(getAuthorization()); + String hash = hexEncode(sha256hash(getAuthorization())); subject = hash.substring(0, 32) + '.' + hash.substring(32) + ".acme.invalid"; } - /** - * Computes a hash according to the specifications. - * - * @param z - * Value to be hashed - * @return Hash - */ - private String computeHash(String z) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(z.getBytes("UTF-8")); - byte[] raw = md.digest(); - char[] result = new char[raw.length * 2]; - for (int ix = 0; ix < raw.length; ix++) { - int val = raw[ix] & 0xFF; - result[ix * 2] = HEX[val >>> 4]; - result[ix * 2 + 1] = HEX[val & 0x0F]; - } - return new String(result); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { - throw new AcmeProtocolException("Could not compute hash", ex); - } - } - } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni02Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni02Challenge.java index ad5413d1..63cc5852 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni02Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TlsSni02Challenge.java @@ -13,19 +13,15 @@ */ package org.shredzone.acme4j.challenge; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import static org.shredzone.acme4j.util.AcmeUtils.*; import org.shredzone.acme4j.Session; -import org.shredzone.acme4j.exception.AcmeProtocolException; /** * Implements the {@value TYPE} challenge. */ public class TlsSni02Challenge extends TokenChallenge { private static final long serialVersionUID = 8921833167878544518L; - private static final char[] HEX = "0123456789abcdef".toCharArray(); /** * Challenge type name: {@value} @@ -70,35 +66,11 @@ public class TlsSni02Challenge extends TokenChallenge { protected void authorize() { super.authorize(); - String tokenHash = computeHash(getToken()); + String tokenHash = hexEncode(sha256hash(getToken())); subject = tokenHash.substring(0, 32) + '.' + tokenHash.substring(32) + ".token.acme.invalid"; - String kaHash = computeHash(getAuthorization()); + String kaHash = hexEncode(sha256hash(getAuthorization())); sanB = kaHash.substring(0, 32) + '.' + kaHash.substring(32) + ".ka.acme.invalid"; } - /** - * Computes a hash according to the specifications. - * - * @param z - * Value to be hashed - * @return Hash - */ - private String computeHash(String z) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(z.getBytes("UTF-8")); - byte[] raw = md.digest(); - char[] result = new char[raw.length * 2]; - for (int ix = 0; ix < raw.length; ix++) { - int val = raw[ix] & 0xFF; - result[ix * 2] = HEX[val >>> 4]; - result[ix * 2 + 1] = HEX[val & 0x0F]; - } - return new String(result); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { - throw new AcmeProtocolException("Could not compute hash", ex); - } - } - } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java index 1ddf8f2f..474da1e0 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java @@ -13,9 +13,10 @@ */ package org.shredzone.acme4j.challenge; +import static org.shredzone.acme4j.util.AcmeUtils.base64UrlEncode; + import java.security.PublicKey; -import org.jose4j.base64url.Base64Url; import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.lang.JoseException; import org.shredzone.acme4j.Session; @@ -83,7 +84,7 @@ public class TokenChallenge extends Challenge { PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(pk); return getToken() + '.' - + Base64Url.encode(jwk.calculateThumbprint("SHA-256")); + + base64UrlEncode(jwk.calculateThumbprint("SHA-256")); } catch (JoseException ex) { throw new AcmeProtocolException("Cannot compute key thumbprint", ex); } 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 new file mode 100644 index 00000000..0128602e --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/AcmeUtils.java @@ -0,0 +1,81 @@ +/* + * 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.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.jose4j.base64url.Base64Url; +import org.shredzone.acme4j.exception.AcmeProtocolException; + +/** + * Contains utility methods that are frequently used for the ACME protocol. + *
+ * This class is internal. You may use it in your own code, but be warned that methods may + * change their signature or disappear without prior announcement. + */ +public final class AcmeUtils { + private static final char[] HEX = "0123456789abcdef".toCharArray(); + + private AcmeUtils() { + // Utility class without constructor + } + + /** + * Computes a SHA-256 hash of the given string. + * + * @param z + * String to hash + * @return Hash + */ + public static byte[] sha256hash(String z) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(z.getBytes("UTF-8")); + return md.digest(); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { + throw new AcmeProtocolException("Could not compute hash", ex); + } + } + + /** + * Hex encodes the given byte array. + * + * @param data + * byte array to hex encode + * @return Hex encoded string of the data (with lower case characters) + */ + public static String hexEncode(byte[] data) { + char[] result = new char[data.length * 2]; + for (int ix = 0; ix < data.length; ix++) { + int val = data[ix] & 0xFF; + result[ix * 2] = HEX[val >>> 4]; + result[ix * 2 + 1] = HEX[val & 0x0F]; + } + return new String(result); + } + + /** + * Base64 encodes the given byte array, using URL style encoding. + * + * @param data + * byte array to base64 encode + * @return base64 encoded string + */ + public static String base64UrlEncode(byte[] data) { + return Base64Url.encode(data); + } + +} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/ClaimBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/ClaimBuilder.java index ec4e92a7..3cf669c0 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/ClaimBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/ClaimBuilder.java @@ -13,6 +13,8 @@ */ package org.shredzone.acme4j.util; +import static org.shredzone.acme4j.util.AcmeUtils.base64UrlEncode; + import java.security.Key; import java.security.PublicKey; import java.text.SimpleDateFormat; @@ -22,7 +24,6 @@ import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; -import org.jose4j.base64url.Base64Url; import org.jose4j.json.JsonUtil; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.PublicJsonWebKey; @@ -130,7 +131,7 @@ public class ClaimBuilder { * @return {@code this} */ public ClaimBuilder putBase64(String key, byte[] data) { - return put(key, Base64Url.encode(data)); + return put(key, base64UrlEncode(data)); } /** 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 new file mode 100644 index 00000000..9a40d36e --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/AcmeUtilsTest.java @@ -0,0 +1,57 @@ +/* + * 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 static org.shredzone.acme4j.util.AcmeUtils.*; + +import javax.xml.bind.DatatypeConverter; + +import org.junit.Test; + +/** + * Unit tests for {@link AcmeUtils}. + */ +public class AcmeUtilsTest { + + /** + * Test sha-256 hash. + */ + @Test + public void testSha256Hash() { + byte[] hash = sha256hash("foobar"); + byte[] expected = DatatypeConverter.parseHexBinary("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"); + assertThat(hash, is(expected)); + } + + /** + * Test hex encode. + */ + @Test + public void testHexEncode() { + String hexEncode = hexEncode(sha256hash("foobar")); + assertThat(hexEncode, is("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")); + } + + /** + * Test base64 URL encode. + */ + @Test + public void testBase64UrlEncode() { + String base64UrlEncode = base64UrlEncode(sha256hash("foobar")); + assertThat(base64UrlEncode, is("w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI")); + } + +}