001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2016 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j.toolbox; 015 016import java.io.UnsupportedEncodingException; 017import java.net.IDN; 018import java.security.MessageDigest; 019import java.security.NoSuchAlgorithmException; 020import java.time.Instant; 021import java.time.ZoneId; 022import java.time.ZonedDateTime; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import org.jose4j.base64url.Base64Url; 027import org.jose4j.jwk.EllipticCurveJsonWebKey; 028import org.jose4j.jwk.JsonWebKey; 029import org.jose4j.jwk.RsaJsonWebKey; 030import org.jose4j.jws.AlgorithmIdentifiers; 031import org.jose4j.jws.JsonWebSignature; 032import org.shredzone.acme4j.exception.AcmeProtocolException; 033 034/** 035 * Contains utility methods that are frequently used for the ACME protocol. 036 * <p> 037 * This class is internal. You may use it in your own code, but be warned that methods may 038 * change their signature or disappear without prior announcement. 039 */ 040public final class AcmeUtils { 041 private static final char[] HEX = "0123456789abcdef".toCharArray(); 042 private static final String ACME_ERROR_PREFIX = "urn:ietf:params:acme:error:"; 043 private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:"; 044 045 private static final Pattern DATE_PATTERN = Pattern.compile( 046 "^(\\d{4})-(\\d{2})-(\\d{2})T" 047 + "(\\d{2}):(\\d{2}):(\\d{2})" 048 + "(?:\\.(\\d{1,3})\\d*)?" 049 + "(Z|[+-]\\d{2}:?\\d{2})$", Pattern.CASE_INSENSITIVE); 050 051 private static final Pattern TZ_PATTERN = Pattern.compile( 052 "([+-])(\\d{2}):?(\\d{2})$"); 053 054 private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile( 055 "([^;]+)(?:;.*?charset=(\"?)([a-z0-9_-]+)(\\2))?.*", Pattern.CASE_INSENSITIVE); 056 057 private AcmeUtils() { 058 // Utility class without constructor 059 } 060 061 /** 062 * Computes a SHA-256 hash of the given string. 063 * 064 * @param z 065 * String to hash 066 * @return Hash 067 */ 068 public static byte[] sha256hash(String z) { 069 try { 070 MessageDigest md = MessageDigest.getInstance("SHA-256"); 071 md.update(z.getBytes("UTF-8")); 072 return md.digest(); 073 } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { 074 throw new AcmeProtocolException("Could not compute hash", ex); 075 } 076 } 077 078 /** 079 * Hex encodes the given byte array. 080 * 081 * @param data 082 * byte array to hex encode 083 * @return Hex encoded string of the data (with lower case characters) 084 */ 085 public static String hexEncode(byte[] data) { 086 char[] result = new char[data.length * 2]; 087 for (int ix = 0; ix < data.length; ix++) { 088 int val = data[ix] & 0xFF; 089 result[ix * 2] = HEX[val >>> 4]; 090 result[ix * 2 + 1] = HEX[val & 0x0F]; 091 } 092 return new String(result); 093 } 094 095 /** 096 * Base64 encodes the given byte array, using URL style encoding. 097 * 098 * @param data 099 * byte array to base64 encode 100 * @return base64 encoded string 101 */ 102 public static String base64UrlEncode(byte[] data) { 103 return Base64Url.encode(data); 104 } 105 106 /** 107 * ASCII encodes a domain name. 108 * <p> 109 * The conversion is done as described in 110 * <a href="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</a>. Additionally, all 111 * leading and trailing white spaces are trimmed, and the result is lowercased. 112 * <p> 113 * It is safe to pass in ACE encoded domains, they will be returned unchanged. 114 * 115 * @param domain 116 * Domain name to encode 117 * @return Encoded domain name, white space trimmed and lower cased. {@code null} if 118 * {@code null} was passed in. 119 */ 120 public static String toAce(String domain) { 121 if (domain == null) { 122 return null; 123 } 124 return IDN.toASCII(domain.trim()).toLowerCase(); 125 } 126 127 /** 128 * Analyzes the key used in the {@link JsonWebKey}, and returns the key algorithm 129 * identifier for {@link JsonWebSignature}. 130 * 131 * @param jwk 132 * {@link JsonWebKey} to analyze 133 * @return algorithm identifier 134 * @throws IllegalArgumentException 135 * there is no corresponding algorithm identifier for the key 136 */ 137 public static String keyAlgorithm(JsonWebKey jwk) { 138 if (jwk instanceof EllipticCurveJsonWebKey) { 139 EllipticCurveJsonWebKey ecjwk = (EllipticCurveJsonWebKey) jwk; 140 141 switch (ecjwk.getCurveName()) { 142 case "P-256": 143 return AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256; 144 145 case "P-384": 146 return AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384; 147 148 case "P-521": 149 return AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512; 150 151 default: 152 throw new IllegalArgumentException("Unknown EC name " 153 + ecjwk.getCurveName()); 154 } 155 156 } else if (jwk instanceof RsaJsonWebKey) { 157 return AlgorithmIdentifiers.RSA_USING_SHA256; 158 159 } else { 160 throw new IllegalArgumentException("Unknown algorithm " + jwk.getAlgorithm()); 161 } 162 } 163 164 /** 165 * Parses a RFC 3339 formatted date. 166 * 167 * @param str 168 * Date string 169 * @return {@link Instant} that was parsed 170 * @throws IllegalArgumentException 171 * if the date string was not RFC 3339 formatted 172 * @see <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a> 173 */ 174 public static Instant parseTimestamp(String str) { 175 Matcher m = DATE_PATTERN.matcher(str); 176 if (!m.matches()) { 177 throw new IllegalArgumentException("Illegal date: " + str); 178 } 179 180 int year = Integer.parseInt(m.group(1)); 181 int month = Integer.parseInt(m.group(2)); 182 int dom = Integer.parseInt(m.group(3)); 183 int hour = Integer.parseInt(m.group(4)); 184 int minute = Integer.parseInt(m.group(5)); 185 int second = Integer.parseInt(m.group(6)); 186 187 StringBuilder msStr = new StringBuilder(); 188 if (m.group(7) != null) { 189 msStr.append(m.group(7)); 190 } 191 while (msStr.length() < 3) { 192 msStr.append('0'); 193 } 194 int ms = Integer.parseInt(msStr.toString()); 195 196 String tz = m.group(8); 197 if ("Z".equalsIgnoreCase(tz)) { 198 tz = "GMT"; 199 } else { 200 tz = TZ_PATTERN.matcher(tz).replaceAll("GMT$1$2:$3"); 201 } 202 203 return ZonedDateTime.of( 204 year, month, dom, hour, minute, second, ms * 1_000_000, 205 ZoneId.of(tz)).toInstant(); 206 } 207 208 /** 209 * Strips the acme error prefix from the error string. 210 * <p> 211 * For example, for "urn:ietf:params:acme:error:conflict", "conflict" is returned. 212 * <p> 213 * This method also handles the deprecated prefix "urn:acme:error:" that is still in 214 * use at Let's Encrypt. 215 * 216 * @param type 217 * Error type to strip the prefix from. {@code null} is safe. 218 * @return Stripped error type, or {@code null} if the prefix was not found. 219 */ 220 public static String stripErrorPrefix(String type) { 221 if (type != null && type.startsWith(ACME_ERROR_PREFIX)) { 222 return type.substring(ACME_ERROR_PREFIX.length()); 223 } else if (type != null && type.startsWith(ACME_ERROR_PREFIX_DEPRECATED)) { 224 return type.substring(ACME_ERROR_PREFIX_DEPRECATED.length()); 225 } else { 226 return null; 227 } 228 } 229 230 /** 231 * Extracts the content type of a Content-Type header. 232 * 233 * @param header 234 * Content-Type header 235 * @return Content-Type, or {@code null} if the header was invalid or empty 236 * @throws AcmeProtocolException 237 * if the Content-Type header contains a different charset than "utf-8". 238 */ 239 public static String getContentType(String header) { 240 if (header != null) { 241 Matcher m = CONTENT_TYPE_PATTERN.matcher(header); 242 if (m.matches()) { 243 String charset = m.group(3); 244 if (charset != null && !"utf-8".equalsIgnoreCase(charset)) { 245 throw new AcmeProtocolException("Unsupported charset " + charset); 246 } 247 return m.group(1).trim().toLowerCase(); 248 } 249 } 250 return null; 251 } 252 253}