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.util;
015
016import static java.time.temporal.ChronoUnit.SECONDS;
017import static org.assertj.core.api.Assertions.assertThat;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.lang.reflect.Modifier;
024import java.net.InetAddress;
025import java.net.UnknownHostException;
026import java.security.KeyPair;
027import java.security.PrivateKey;
028import java.security.PublicKey;
029import java.security.cert.CertificateParsingException;
030import java.security.cert.X509Certificate;
031import java.time.Duration;
032import java.time.Instant;
033import java.util.Date;
034import java.util.HashSet;
035import java.util.Set;
036
037import org.bouncycastle.asn1.ASN1InputStream;
038import org.bouncycastle.asn1.BERTags;
039import org.bouncycastle.asn1.DEROctetString;
040import org.bouncycastle.asn1.x509.GeneralName;
041import org.bouncycastle.pkcs.PKCS10CertificationRequest;
042import org.junit.jupiter.api.Test;
043import org.shredzone.acme4j.Identifier;
044import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
045import org.shredzone.acme4j.toolbox.AcmeUtils;
046
047/**
048 * Unit tests for {@link CertificateUtils}.
049 */
050public class CertificateUtilsTest {
051
052    /**
053     * Test if {@link CertificateUtils#readCSR(InputStream)} reads an identical CSR.
054     */
055    @Test
056    public void testReadCSR() throws IOException {
057        var keypair = KeyPairUtils.createKeyPair(2048);
058
059        var builder = new CSRBuilder();
060        builder.addDomains("example.com", "example.org");
061        builder.sign(keypair);
062
063        var original = builder.getCSR();
064        byte[] pemFile;
065        try (var baos = new ByteArrayOutputStream()) {
066            builder.write(baos);
067            pemFile = baos.toByteArray();
068        }
069
070        try (var bais = new ByteArrayInputStream(pemFile)) {
071            var read = CertificateUtils.readCSR(bais);
072            assertThat(original.getEncoded()).isEqualTo(read.getEncoded());
073        }
074    }
075
076    /**
077     * Test that constructor is private.
078     */
079    @Test
080    public void testPrivateConstructor() throws Exception {
081        var constructor = CertificateUtils.class.getDeclaredConstructor();
082        assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue();
083        constructor.setAccessible(true);
084        constructor.newInstance();
085    }
086
087    /**
088     * Test if
089     * {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, Identifier, byte[])}
090     * with domain name creates a good certificate.
091     */
092    @Test
093    public void testCreateTlsAlpn01Certificate() throws Exception {
094        var keypair = KeyPairUtils.createKeyPair(2048);
095        var subject = "example.com";
096        var acmeValidationV1 = AcmeUtils.sha256hash("rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ");
097
098        var cert = CertificateUtils.createTlsAlpn01Certificate(keypair, Identifier.dns(subject), acmeValidationV1);
099
100        var now = Instant.now();
101        var end = now.plus(Duration.ofDays(8));
102
103        assertThat(cert).isNotNull();
104        assertThat(cert.getNotAfter()).isAfter(Date.from(now));
105        assertThat(cert.getNotAfter()).isBefore(Date.from(end));
106        assertThat(cert.getNotBefore()).isBeforeOrEqualTo(Date.from(now));
107
108        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo("CN=acme.invalid");
109        assertThat(getSANs(cert)).contains(subject);
110
111        assertThat(cert.getCriticalExtensionOIDs()).contains(TlsAlpn01Challenge.ACME_VALIDATION_OID);
112
113        var encodedExtensionValue = cert.getExtensionValue(TlsAlpn01Challenge.ACME_VALIDATION_OID);
114        assertThat(encodedExtensionValue).isNotNull();
115
116        try (var asn = new ASN1InputStream(new ByteArrayInputStream(encodedExtensionValue))) {
117            var derOctetString = (DEROctetString) asn.readObject();
118
119            var test = new byte[acmeValidationV1.length + 2];
120            test[0] = BERTags.OCTET_STRING;
121            test[1] = (byte) acmeValidationV1.length;
122            System.arraycopy(acmeValidationV1, 0, test, 2, acmeValidationV1.length);
123
124            assertThat(derOctetString.getOctets()).isEqualTo(test);
125        }
126
127        cert.verify(keypair.getPublic());
128    }
129
130    /**
131     * Test if
132     * {@link CertificateUtils#createTlsAlpn01Certificate(KeyPair, Identifier, byte[])}
133     * with IP creates a good certificate.
134     */
135    @Test
136    public void testCreateTlsAlpn01CertificateWithIp() throws IOException, CertificateParsingException {
137        var keypair = KeyPairUtils.createKeyPair(2048);
138        var subject = InetAddress.getLocalHost();
139        var acmeValidationV1 = AcmeUtils.sha256hash("rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ");
140
141        var cert = CertificateUtils.createTlsAlpn01Certificate(keypair, Identifier.ip(subject), acmeValidationV1);
142
143        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo("CN=acme.invalid");
144        assertThat(getIpSANs(cert)).contains(subject);
145    }
146
147    /**
148     * Test if {@link CertificateUtils#createTestRootCertificate(String, Instant, Instant,
149     * KeyPair)} generates a valid root certificate.
150     */
151    @Test
152    public void testCreateTestRootCertificate() throws Exception {
153        var keypair = KeyPairUtils.createKeyPair(2048);
154        var subject = "CN=Test Root Certificate";
155        var notBefore = Instant.now().truncatedTo(SECONDS);
156        var notAfter = notBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);
157
158        var cert = CertificateUtils.createTestRootCertificate(subject,
159                notBefore, notAfter, keypair);
160
161        assertThat(cert.getIssuerX500Principal().getName()).isEqualTo(subject);
162        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo(subject);
163        assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore);
164        assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter);
165        assertThat(cert.getSerialNumber()).isNotNull();
166        assertThat(cert.getPublicKey()).isEqualTo(keypair.getPublic());
167        cert.verify(cert.getPublicKey()); // self-signed
168    }
169
170    /**
171     * Test if {@link CertificateUtils#createTestIntermediateCertificate(String, Instant,
172     * Instant, PublicKey, X509Certificate, PrivateKey)} generates a valid intermediate
173     * certificate.
174     */
175    @Test
176    public void testCreateTestIntermediateCertificate() throws Exception {
177        var rootKeypair = KeyPairUtils.createKeyPair(2048);
178        var rootSubject = "CN=Test Root Certificate";
179        var rootNotBefore = Instant.now().minus(Duration.ofDays(1)).truncatedTo(SECONDS);
180        var rootNotAfter = rootNotBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);
181
182        var rootCert = CertificateUtils.createTestRootCertificate(rootSubject,
183                rootNotBefore, rootNotAfter, rootKeypair);
184
185        var keypair = KeyPairUtils.createKeyPair(2048);
186        var subject = "CN=Test Intermediate Certificate";
187        var notBefore = Instant.now().truncatedTo(SECONDS);
188        var notAfter = notBefore.plus(Duration.ofDays(7)).truncatedTo(SECONDS);
189
190        var cert = CertificateUtils.createTestIntermediateCertificate(subject,
191                notBefore, notAfter, keypair.getPublic(), rootCert, rootKeypair.getPrivate());
192
193        assertThat(cert.getIssuerX500Principal().getName()).isEqualTo(rootSubject);
194        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo(subject);
195        assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore);
196        assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter);
197        assertThat(cert.getSerialNumber()).isNotNull();
198        assertThat(cert.getSerialNumber()).isNotEqualTo(rootCert.getSerialNumber());
199        assertThat(cert.getPublicKey()).isEqualTo(keypair.getPublic());
200        cert.verify(rootKeypair.getPublic()); // signed by root
201    }
202
203    /**
204     * Test if {@link CertificateUtils#createTestCertificate(PKCS10CertificationRequest,
205     * Instant, Instant, X509Certificate, PrivateKey)} generates a valid certificate.
206     */
207    @Test
208    public void testCreateTestCertificate() throws Exception {
209        var rootKeypair = KeyPairUtils.createKeyPair(2048);
210        var rootSubject = "CN=Test Root Certificate";
211        var rootNotBefore = Instant.now().minus(Duration.ofDays(1)).truncatedTo(SECONDS);
212        var rootNotAfter = rootNotBefore.plus(Duration.ofDays(14)).truncatedTo(SECONDS);
213
214        var rootCert = CertificateUtils.createTestRootCertificate(rootSubject,
215                rootNotBefore, rootNotAfter, rootKeypair);
216
217        var keypair = KeyPairUtils.createKeyPair(2048);
218        var notBefore = Instant.now().truncatedTo(SECONDS);
219        var notAfter = notBefore.plus(Duration.ofDays(7)).truncatedTo(SECONDS);
220
221        var builder = new CSRBuilder();
222        builder.addDomains("example.org", "www.example.org");
223        builder.addIP(InetAddress.getByName("192.0.2.1"));
224        builder.sign(keypair);
225        var csr = builder.getCSR();
226
227        var cert = CertificateUtils.createTestCertificate(csr, notBefore,
228                notAfter, rootCert, rootKeypair.getPrivate());
229
230        assertThat(cert.getIssuerX500Principal().getName()).isEqualTo(rootSubject);
231        assertThat(cert.getSubjectX500Principal().getName()).isEqualTo("");
232        assertThat(getSANs(cert)).contains("example.org", "www.example.org");
233        assertThat(getIpSANs(cert)).contains(InetAddress.getByName("192.0.2.1"));
234        assertThat(cert.getNotBefore().toInstant()).isEqualTo(notBefore);
235        assertThat(cert.getNotAfter().toInstant()).isEqualTo(notAfter);
236        assertThat(cert.getSerialNumber()).isNotNull();
237        assertThat(cert.getSerialNumber()).isNotEqualTo(rootCert.getSerialNumber());
238        assertThat(cert.getPublicKey()).isEqualTo(keypair.getPublic());
239        cert.verify(rootKeypair.getPublic()); // signed by root
240    }
241
242    /**
243     * Extracts all DNSName SANs from a certificate.
244     *
245     * @param cert
246     *            {@link X509Certificate}
247     * @return Set of DNSName
248     */
249    private Set<String> getSANs(X509Certificate cert) throws CertificateParsingException {
250        var result = new HashSet<String>();
251
252        for (var list : cert.getSubjectAlternativeNames()) {
253            if (((Number) list.get(0)).intValue() == GeneralName.dNSName) {
254                result.add((String) list.get(1));
255            }
256        }
257
258        return result;
259    }
260
261    /**
262     * Extracts all IPAddress SANs from a certificate.
263     *
264     * @param cert
265     *            {@link X509Certificate}
266     * @return Set of IPAddresses
267     */
268    private Set<InetAddress> getIpSANs(X509Certificate cert) throws CertificateParsingException, UnknownHostException {
269        var result = new HashSet<InetAddress>();
270
271        for (var list : cert.getSubjectAlternativeNames()) {
272            if (((Number) list.get(0)).intValue() == GeneralName.iPAddress) {
273                result.add(InetAddress.getByName(list.get(1).toString()));
274            }
275        }
276
277        return result;
278    }
279
280}