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}