001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2015 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 static java.nio.charset.StandardCharsets.UTF_8; 017import static java.util.stream.Collectors.toUnmodifiableList; 018 019import java.io.ByteArrayOutputStream; 020import java.io.FileOutputStream; 021import java.io.IOException; 022import java.io.UncheckedIOException; 023import java.net.MalformedURLException; 024import java.net.URI; 025import java.net.URL; 026import java.security.InvalidAlgorithmParameterException; 027import java.security.KeyFactory; 028import java.security.KeyPair; 029import java.security.KeyPairGenerator; 030import java.security.MessageDigest; 031import java.security.NoSuchAlgorithmException; 032import java.security.SecureRandom; 033import java.security.cert.CertificateException; 034import java.security.cert.CertificateFactory; 035import java.security.cert.X509Certificate; 036import java.security.spec.ECGenParameterSpec; 037import java.security.spec.InvalidKeySpecException; 038import java.security.spec.PKCS8EncodedKeySpec; 039import java.security.spec.X509EncodedKeySpec; 040import java.util.Base64; 041import java.util.List; 042import java.util.TreeMap; 043 044import javax.crypto.SecretKey; 045 046import edu.umd.cs.findbugs.annotations.Nullable; 047import org.jose4j.json.JsonUtil; 048import org.jose4j.jwk.JsonWebKey; 049import org.jose4j.jwk.JsonWebKey.OutputControlLevel; 050import org.jose4j.keys.HmacKey; 051import org.shredzone.acme4j.Login; 052import org.shredzone.acme4j.Problem; 053import org.shredzone.acme4j.Session; 054import org.shredzone.acme4j.connector.Connection; 055import org.shredzone.acme4j.connector.NetworkSettings; 056import org.shredzone.acme4j.provider.AcmeProvider; 057 058/** 059 * Some utility methods for unit tests. 060 */ 061public final class TestUtils { 062 public static final String N = "pZsTKY41y_CwgJ0VX7BmmGs_7UprmXQMGPcnSbBeJAjZHA9SyyJKaWv4fNUdBIAX3Y2QoZixj50nQLyLv2ng3pvEoRL0sx9ZHgp5ndAjpIiVQ_8V01TTYCEDUc9ii7bjVkgFAb4ValZGFJZ54PcCnAHvXi5g0ELORzGcTuRqHVAUckMV2otr0g0u_5bWMm6EMAbBrGQCgUGjbZQHjava1Y-5tHXZkPBahJ2LvKRqMmJUlr0anKuJJtJUG03DJYAxABv8YAaXFBnGw6kKJRpUFAC55ry4sp4kGy0NrK2TVWmZW9kStniRv4RaJGI9aZGYwQy2kUykibBNmWEQUlIwIw"; 063 public static final String E = "AQAB"; 064 public static final String KTY = "RSA"; 065 public static final String THUMBPRINT = "HnWjTDnyqlCrm6tZ-6wX-TrEXgRdeNu9G71gqxSO6o0"; 066 067 public static final String D_N = "tP7p9wOe0NWocwLu7h233i1JqUPW1MeLeilyHY7oMKnXZFyf1l0saqLcrBtOj3EyaG6qVfpiLEWEIiuWclPYSR_QSt9lCi9xAoWbYq9-mqseehXPaejynlIMsP2UiCAenSHjJEer6Ug6nFelGVgav3mypwYFUdvc18wI00clKYhRAc4dZodilRzDTLy95V1S3RCxGf-lE0XYg7ieO_ovSMERtH_7NsjZnBiaE7mwm0YZzreCr8oSuHwhC63kgY27FnCgH0h63LICSPVVDJZPLcWAmSXv1k0qoVTsRzFutRN6RB_96wqTTBi8Qm98lyCpXcsxa3BH-4TCvLEaa2KkeQ"; 068 public static final String D_E = "AQAB"; 069 public static final String D_KTY = "RSA"; 070 public static final String D_THUMBPRINT = "0VPbh7-I6swlkBu0TrNKSQp6d69bukzeQA0ksuX3FFs"; 071 072 public static final String ACME_SERVER_URI = "https://example.com/acme"; 073 public static final String ACCOUNT_URL = "https://example.com/acme/account/1"; 074 075 public static final String DUMMY_NONCE = Base64.getUrlEncoder().withoutPadding().encodeToString("foo-nonce-foo".getBytes()); 076 077 public static final String CERT_ISSUER = "Pebble Intermediate CA 645fc5"; 078 079 public static final NetworkSettings DEFAULT_NETWORK_SETTINGS = new NetworkSettings(); 080 081 private TestUtils() { 082 // utility class without constructor 083 } 084 085 /** 086 * Reads a resource as byte array. 087 * 088 * @param name 089 * Resource name 090 * @return Resource content as byte array. 091 */ 092 public static byte[] getResourceAsByteArray(String name) throws IOException { 093 var buffer = new byte[2048]; 094 try (var in = TestUtils.class.getResourceAsStream(name); 095 var out = new ByteArrayOutputStream()) { 096 int len; 097 while ((len = in.read(buffer)) >= 0) { 098 out.write(buffer, 0, len); 099 } 100 return out.toByteArray(); 101 } 102 } 103 104 /** 105 * Reads a JSON string from json test files and parses it. 106 * 107 * @param key 108 * JSON resource 109 * @return Parsed JSON resource 110 */ 111 public static JSON getJSON(String key) { 112 try { 113 return JSON.parse(TestUtils.class.getResourceAsStream("/json/" + key + ".json")); 114 } catch (IOException ex) { 115 throw new UncheckedIOException(ex); 116 } 117 } 118 119 /** 120 * Creates a {@link Session} instance. It uses {@link #ACME_SERVER_URI} as server URI. 121 */ 122 public static Session session() { 123 return new Session(URI.create(ACME_SERVER_URI)); 124 } 125 126 /** 127 * Creates a {@link Login} instance. It uses {@link #ACME_SERVER_URI} as server URI, 128 * {@link #ACCOUNT_URL} as account URL, and a random key pair. 129 */ 130 public static Login login() { 131 try { 132 return session().login(new URL(ACCOUNT_URL), createKeyPair()); 133 } catch (IOException ex) { 134 throw new UncheckedIOException(ex); 135 } 136 } 137 138 /** 139 * Creates an {@link URL} from a String. Only throws a runtime exception if the URL is 140 * malformed. 141 * 142 * @param url 143 * URL to use 144 * @return {@link URL} object 145 */ 146 public static URL url(String url) { 147 try { 148 return new URL(url); 149 } catch (MalformedURLException ex) { 150 throw new IllegalArgumentException(url, ex); 151 } 152 } 153 154 /** 155 * Creates a {@link Session} instance. It uses {@link #ACME_SERVER_URI} as server URI. 156 * 157 * @param provider 158 * {@link AcmeProvider} to be used in this session 159 */ 160 public static Session session(final AcmeProvider provider) { 161 return new Session(URI.create(ACME_SERVER_URI)) { 162 @Override 163 public AcmeProvider provider() { 164 return provider; 165 } 166 167 @Override 168 public Connection connect() { 169 return provider.connect(getServerUri(), DEFAULT_NETWORK_SETTINGS); 170 } 171 }; 172 } 173 174 /** 175 * Creates a standard account {@link KeyPair} for testing. The key pair is read from a 176 * test resource and is guaranteed not to change between test runs. 177 * <p> 178 * The constants {@link #N}, {@link #E}, {@link #KTY} and {@link #THUMBPRINT} are 179 * related to the returned key pair and can be used for asserting results. 180 * 181 * @return {@link KeyPair} for testing 182 */ 183 public static KeyPair createKeyPair() throws IOException { 184 try { 185 var keyFactory = KeyFactory.getInstance(KTY); 186 187 var publicKeySpec = new X509EncodedKeySpec(getResourceAsByteArray("/public.key")); 188 var publicKey = keyFactory.generatePublic(publicKeySpec); 189 190 var privateKeySpec = new PKCS8EncodedKeySpec(getResourceAsByteArray("/private.key")); 191 var privateKey = keyFactory.generatePrivate(privateKeySpec); 192 193 return new KeyPair(publicKey, privateKey); 194 } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { 195 throw new IOException(ex); 196 } 197 } 198 199 /** 200 * Creates a standard domain key pair for testing. This keypair is read from a test 201 * resource and is guaranteed not to change between test runs. 202 * <p> 203 * The constants {@link #D_N}, {@link #D_E}, {@link #D_KTY} and {@link #D_THUMBPRINT} 204 * are related to the returned key pair and can be used for asserting results. 205 * 206 * @return {@link KeyPair} for testing 207 */ 208 public static KeyPair createDomainKeyPair() throws IOException { 209 try { 210 var keyFactory = KeyFactory.getInstance(KTY); 211 212 var publicKeySpec = new X509EncodedKeySpec(getResourceAsByteArray("/domain-public.key")); 213 var publicKey = keyFactory.generatePublic(publicKeySpec); 214 215 var privateKeySpec = new PKCS8EncodedKeySpec(getResourceAsByteArray("/domain-private.key")); 216 var privateKey = keyFactory.generatePrivate(privateKeySpec); 217 218 return new KeyPair(publicKey, privateKey); 219 } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { 220 throw new IOException(ex); 221 } 222 } 223 224 /** 225 * Creates a random ECC key pair with the given curve name. 226 * 227 * @param name 228 * Curve name 229 * @return {@link KeyPair} for testing 230 */ 231 public static KeyPair createECKeyPair(String name) throws IOException { 232 try { 233 var ecSpec = new ECGenParameterSpec(name); 234 var keyGen = KeyPairGenerator.getInstance("EC"); 235 keyGen.initialize(ecSpec, new SecureRandom()); 236 return keyGen.generateKeyPair(); 237 } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException ex) { 238 throw new IOException(ex); 239 } 240 } 241 242 /** 243 * Creates a HMAC key using the given hash algorithm. 244 * 245 * @param algorithm 246 * Name of the hash algorithm to be used 247 * @return {@link SecretKey} for testing 248 */ 249 public static SecretKey createSecretKey(String algorithm) throws IOException { 250 try { 251 var md = MessageDigest.getInstance(algorithm); 252 md.update("Turpentine".getBytes()); // A random password 253 var macKey = md.digest(); 254 return new HmacKey(macKey); 255 } catch (NoSuchAlgorithmException ex) { 256 throw new IOException(ex); 257 } 258 } 259 260 /** 261 * Creates a standard certificate chain for testing. This certificate is read from a 262 * test resource and is guaranteed not to change between test runs. 263 * 264 * @param resource 265 * Name of the resource 266 * @return List of {@link X509Certificate} for testing 267 */ 268 public static List<X509Certificate> createCertificate(String resource) throws IOException { 269 try (var in = TestUtils.class.getResourceAsStream(resource)) { 270 var cf = CertificateFactory.getInstance("X.509"); 271 return cf.generateCertificates(in).stream() 272 .map(c -> (X509Certificate) c) 273 .collect(toUnmodifiableList()); 274 } catch (CertificateException ex) { 275 throw new IOException(ex); 276 } 277 } 278 279 /** 280 * Creates a {@link Problem} with the given type and details. 281 * 282 * @param type 283 * Problem type 284 * @param detail 285 * Problem details 286 * @param instance 287 * Instance, or {@code null} 288 * @return Created {@link Problem} object 289 */ 290 public static Problem createProblem(URI type, String detail, @Nullable URL instance) { 291 var jb = new JSONBuilder(); 292 jb.put("type", type); 293 jb.put("detail", detail); 294 if (instance != null) { 295 jb.put("instance", instance); 296 } 297 298 return new Problem(jb.toJSON(), url("https://example.com/acme/1")); 299 } 300 301 /** 302 * Generates a new keypair for unit tests, and return its N, E, KTY and THUMBPRINT 303 * parameters to be set in the {@link TestUtils} class. 304 */ 305 public static void main(String... args) throws Exception { 306 var keyGen = KeyPairGenerator.getInstance("RSA"); 307 keyGen.initialize(2048); 308 var keyPair = keyGen.generateKeyPair(); 309 310 try (var out = new FileOutputStream("public.key")) { 311 out.write(keyPair.getPublic().getEncoded()); 312 } 313 314 try (var out = new FileOutputStream("private.key")) { 315 out.write(keyPair.getPrivate().getEncoded()); 316 } 317 318 var jwk = JsonWebKey.Factory.newJwk(keyPair.getPublic()); 319 var params = new TreeMap<>(jwk.toParams(OutputControlLevel.PUBLIC_ONLY)); 320 var md = MessageDigest.getInstance("SHA-256"); 321 md.update(JsonUtil.toJson(params).getBytes(UTF_8)); 322 var thumbprint = md.digest(); 323 324 System.out.println("N = " + params.get("n")); 325 System.out.println("E = " + params.get("e")); 326 System.out.println("KTY = " + params.get("kty")); 327 System.out.println("THUMBPRINT = " + Base64.getUrlEncoder().withoutPadding().encodeToString(thumbprint)); 328 } 329 330}