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}