
org.shredzone.acme4j.util.AcmeUtils Maven / Gradle / Ivy
/*
* 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.net.IDN;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
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;
/**
* 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 static final String ACME_ERROR_PREFIX = "urn:ietf:params:acme:error:";
private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:";
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
}
/**
* 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);
}
/**
* 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 Instant} that was parsed
* @throws IllegalArgumentException
* if the date string was not RFC 3339 formatted
* @see RFC 3339
*/
public static Instant 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");
}
return ZonedDateTime.of(
year, month, dom, hour, minute, second, ms * 1_000_000,
ZoneId.of(tz)).toInstant();
}
/**
* Strips the acme error prefix from the error string.
*
* For example, for "urn:ietf:params:acme:error:conflict", "conflict" is returned.
*
* This method also handles the deprecated prefix "urn:acme:error:" that is still in
* use at Let's Encrypt.
*
* @param type
* Error type to strip the prefix from. {@code null} is safe.
* @return Stripped error type, or {@code null} if the prefix was not found.
*/
public static String stripErrorPrefix(String type) {
if (type != null && type.startsWith(ACME_ERROR_PREFIX)) {
return type.substring(ACME_ERROR_PREFIX.length());
} else if (type != null && type.startsWith(ACME_ERROR_PREFIX_DEPRECATED)) {
return type.substring(ACME_ERROR_PREFIX_DEPRECATED.length());
} else {
return null;
}
}
}