diff --git a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CSRBuilderTest.java b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CSRBuilderTest.java new file mode 100644 index 00000000..90e76819 --- /dev/null +++ b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CSRBuilderTest.java @@ -0,0 +1,262 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.util; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.security.KeyPair; +import java.util.Arrays; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.openssl.PEMException; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.jcabi.matchers.RegexMatchers; + +/** + * Unit tests for {@link CSRBuilder}. + * + * @author Richard "Shred" Körber + */ +public class CSRBuilderTest { + + private static KeyPair testKey; + + @BeforeClass + public static void setup() { + testKey = KeyPairUtils.createKeyPair(512); + } + + /** + * Test if the generated CSR is plausible. + */ + @Test + public void testGenerate() throws IOException { + CSRBuilder builder = new CSRBuilder(); + builder.addDomain("abc.de"); + builder.addDomain("fg.hi"); + builder.addDomains("jklm.no", "pqr.st"); + builder.addDomains(Arrays.asList("uv.wx", "y.z")); + + builder.setCountry("XX"); + builder.setLocality("Testville"); + builder.setOrganization("Testing Co"); + builder.setOrganizationalUnit("Testunit"); + builder.setState("ABC"); + + assertThat(builder.toString(), is("CN=abc.de,C=XX,L=Testville,O=Testing Co," + + "OU=Testunit,ST=ABC," + + "DNS=abc.de,DNS=fg.hi,DNS=jklm.no,DNS=pqr.st,DNS=uv.wx,DNS=y.z")); + + builder.sign(testKey); + + PKCS10CertificationRequest csr = builder.getCSR(); + assertThat(csr, is(notNullValue())); + assertThat(csr.getEncoded(), is(equalTo(builder.getEncoded()))); + + csrTest(csr); + writerTest(builder); + } + + /** + * Checks if the CSR contains the right parameters. + *

+ * This is not supposed to be a Bouncy Castle test. If the + * {@link PKCS10CertificationRequest} contains the right parameters, we assume that + * Bouncy Castle encodes it properly. + */ + @SuppressWarnings("unchecked") + private void csrTest(PKCS10CertificationRequest csr) { + X500Name name = csr.getSubject(); + assertThat(name.getRDNs(BCStyle.CN), arrayContaining(new RDNMatcher("abc.de"))); + assertThat(name.getRDNs(BCStyle.C), arrayContaining(new RDNMatcher("XX"))); + assertThat(name.getRDNs(BCStyle.L), arrayContaining(new RDNMatcher("Testville"))); + assertThat(name.getRDNs(BCStyle.O), arrayContaining(new RDNMatcher("Testing Co"))); + assertThat(name.getRDNs(BCStyle.OU), arrayContaining(new RDNMatcher("Testunit"))); + assertThat(name.getRDNs(BCStyle.ST), arrayContaining(new RDNMatcher("ABC"))); + + Attribute[] attr = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest); + assertThat(attr.length, is(1)); + ASN1Encodable[] extensions = attr[0].getAttrValues().toArray(); + assertThat(extensions.length, is(1)); + GeneralNames names = GeneralNames.fromExtensions((Extensions) extensions[0], Extension.subjectAlternativeName); + assertThat(names.getNames(), arrayContaining(new GeneralNameMatcher("abc.de"), + new GeneralNameMatcher("fg.hi"), new GeneralNameMatcher("jklm.no"), + new GeneralNameMatcher("pqr.st"), new GeneralNameMatcher("uv.wx"), + new GeneralNameMatcher("y.z"))); + } + + /** + * Checks if the {@link CSRBuilder#write(java.io.Writer)} method generates a correct + * CSR PEM file. + */ + private void writerTest(CSRBuilder builder) throws IOException, PEMException { + // Write CSR to PEM + String pem; + try (StringWriter out = new StringWriter()) { + builder.write(out); + pem = out.toString(); + } + + // Make sure PEM file is properly formatted + assertThat(pem, RegexMatchers.matchesPattern( + "-----BEGIN CERTIFICATE REQUEST-----[\\r\\n]+" + + "([a-zA-Z0-9/+=]+[\\r\\n]+)+" + + "-----END CERTIFICATE REQUEST-----[\\r\\n]*")); + + // Read CSR from PEM + PKCS10CertificationRequest readCsr; + try (PEMParser parser = new PEMParser(new StringReader(pem))) { + readCsr = (PKCS10CertificationRequest) parser.readObject(); + } + + // Verify that both keypairs are the same + assertThat(builder.getCSR(), not(sameInstance(readCsr))); + assertThat(builder.getEncoded(), is(equalTo(readCsr.getEncoded()))); + } + + /** + * Make sure an exception is thrown when no domain is set. + */ + @Test(expected = IllegalStateException.class) + public void testNoDomain() throws IOException { + CSRBuilder builder = new CSRBuilder(); + builder.sign(testKey); + } + + /** + * Make sure all getters will fail if the CSR is not signed. + */ + @Test + public void testNoSign() throws IOException { + CSRBuilder builder = new CSRBuilder(); + + try { + builder.getCSR(); + fail("getCSR(): expected exception was not thrown"); + } catch (IllegalStateException ex) { + // expected + } + + try { + builder.getEncoded(); + fail("getEncoded(): expected exception was not thrown"); + } catch (IllegalStateException ex) { + // expected + } + + try (StringWriter w = new StringWriter()) { + builder.write(w); + fail("write(): expected exception was not thrown"); + } catch (IllegalStateException ex) { + // expected + } + } + + /** + * Matches {@link RDN} values. + */ + private static class RDNMatcher extends BaseMatcher { + private final String expectedValue; + + public RDNMatcher(String expectedValue) { + this.expectedValue = expectedValue; + } + + @Override + public boolean matches(Object item) { + if (!(item instanceof RDN)) { + return false; + } + return expectedValue.equals(((RDN) item).getFirst().getValue().toString()); + } + + @Override + public void describeTo(Description description) { + description.appendValue(expectedValue); + } + + @Override + public void describeMismatch(Object item, Description description) { + if (!(item instanceof RDN)) { + description.appendText("is a ").appendValue(item.getClass()); + } else { + description.appendText("was ").appendValue(((RDN) item).getFirst().getValue()); + } + } + } + + /** + * Matches {@link GeneralName} DNS tagged values. + */ + private static class GeneralNameMatcher extends BaseMatcher { + private final String expectedValue; + + public GeneralNameMatcher(String expectedValue) { + this.expectedValue = expectedValue; + } + + @Override + public boolean matches(Object item) { + if (!(item instanceof GeneralName)) { + return false; + } + + GeneralName gn = (GeneralName) item; + + return gn.getTagNo() == GeneralName.dNSName + && expectedValue.equals(DERIA5String.getInstance(gn.getName()).getString()); + } + + @Override + public void describeTo(Description description) { + description.appendValue(expectedValue); + } + + @Override + public void describeMismatch(Object item, Description description) { + if (!(item instanceof GeneralName)) { + description.appendText("is a ").appendValue(item.getClass()); + return; + } + + GeneralName gn = (GeneralName) item; + if (gn.getTagNo() != GeneralName.dNSName) { + description.appendText("is not DNS"); + } else { + description.appendText("was ").appendValue(DERIA5String.getInstance(gn.getName()).getString()); + } + } + } + +} diff --git a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java new file mode 100644 index 00000000..f08ce35f --- /dev/null +++ b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/CertificateUtilsTest.java @@ -0,0 +1,82 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.util; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.junit.Before; +import org.junit.Test; + +import com.jcabi.matchers.RegexMatchers; + +/** + * Unit tests for {@link CertificateUtils}. + * + * @author Richard "Shred" Körber + */ +public class CertificateUtilsTest { + + private CertificateFactory certificateFactory; + + @Before + public void setup() throws CertificateException { + certificateFactory = CertificateFactory.getInstance("X.509"); + } + + /** + * Test if {@link CertificateUtilsTest#writeX509CertificateTest()} writes a + * proper X.509 certificate. + */ + @Test + public void testWriteX509Certificate() throws IOException, CertificateException { + // Read a demonstration certificate + X509Certificate original; + try (InputStream cert = getClass().getResourceAsStream("/cert.pem")) { + original = (X509Certificate) certificateFactory.generateCertificate(cert); + } + assertThat(original, is(notNullValue())); + + // Write to StringWriter + String pem; + try (StringWriter out = new StringWriter()) { + CertificateUtils.writeX509Certificate(original, out); + pem = out.toString(); + } + + // Make sure it is a good PEM file + assertThat(pem, RegexMatchers.matchesPattern( + "-----BEGIN CERTIFICATE-----[\\r\\n]+" + + "([a-zA-Z0-9/+=]+[\\r\\n]+)+" + + "-----END CERTIFICATE-----[\\r\\n]*")); + + // Read it back in + X509Certificate written; + try (InputStream cert = new ByteArrayInputStream(pem.getBytes("utf-8"))) { + written = (X509Certificate) certificateFactory.generateCertificate(cert); + } + + // Verify that both certificates are the same + assertThat(original.getEncoded(), is(equalTo(written.getEncoded()))); + } + +} diff --git a/acme4j-utils/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java new file mode 100644 index 00000000..f755a9de --- /dev/null +++ b/acme4j-utils/src/test/java/org/shredzone/acme4j/util/KeyPairUtilsTest.java @@ -0,0 +1,82 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2015 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.util; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.security.KeyPair; +import java.security.interfaces.RSAPublicKey; + +import org.junit.Test; + +import com.jcabi.matchers.RegexMatchers; + +/** + * Unit tests for {@link KeyPairUtils}. + * + * @author Richard "Shred" Körber + */ +public class KeyPairUtilsTest { + private static final int KEY_SIZE = 2048; + + /** + * Test that RSA keypairs of the correct size are generated. + */ + @Test + public void testCreateKeyPair() { + KeyPair pair = KeyPairUtils.createKeyPair(KEY_SIZE); + assertThat(pair, is(notNullValue())); + assertThat(pair.getPublic(), is(instanceOf(RSAPublicKey.class))); + + RSAPublicKey pub = (RSAPublicKey) pair.getPublic(); + assertThat(pub.getModulus().bitLength(), is(KEY_SIZE)); + } + + /** + * Test that reading and writing keypairs work correctly. + */ + @Test + public void testWriteAndRead() throws IOException { + // Generate a test keypair + KeyPair pair = KeyPairUtils.createKeyPair(KEY_SIZE); + + // Write keypair to PEM + String pem; + try (StringWriter out = new StringWriter()) { + KeyPairUtils.writeKeyPair(pair, out); + pem = out.toString(); + } + + // Make sure PEM file is properly formatted + assertThat(pem, RegexMatchers.matchesPattern( + "-----BEGIN RSA PRIVATE KEY-----[\\r\\n]+" + + "([a-zA-Z0-9/+=]+[\\r\\n]+)+" + + "-----END RSA PRIVATE KEY-----[\\r\\n]*")); + + // Read keypair from PEM + KeyPair readPair; + try (StringReader in = new StringReader(pem)) { + readPair = KeyPairUtils.readKeyPair(in); + } + + // Verify that both keypairs are the same + assertThat(pair, not(sameInstance(readPair))); + assertThat(pair.getPrivate().getEncoded(), is(equalTo(readPair.getPrivate().getEncoded()))); + } + +} diff --git a/acme4j-utils/src/test/resources/cert.pem b/acme4j-utils/src/test/resources/cert.pem new file mode 100644 index 00000000..6981323c --- /dev/null +++ b/acme4j-utils/src/test/resources/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVzCCAj+gAwIBAgIJAM4KDTzb0Y7NMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg +Q29tcGFueSBMdGQwHhcNMTUxMjEwMDAxMTA4WhcNMjUxMjA3MDAxMTA4WjBCMQsw +CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh +dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +r0g3w4C8xbj/5lzJiDxk0HkEJeZeyruq+0AzOPMigJZ7zxZtX/KUxOIHrQ4qjcFh +l0DmQImoM0wESU+kcsjAHCx8E1lgRVlVsMfLAQPHkg5UybqfadzKT3ALcSD+9F9m +VIP6liC/6KzLTASmx6zM7j92KTl1ArObZr5mh0jvSNORrMhEC4Byn3+NTxjuHON1 +rWppCMwpeNNhFzaAig3O8PY8IyaLXNP2Ac5pXn0iW16S+Im9by7751UeW5a7Dznm +uMEM+WY640ffJDQ4+I64H403uAgvvSu+BGw8SEEZGuBCxoCnG1g6y6OvJyN5TgqF +dGosAfm1u+/MP1seoPdpBQIDAQABo1AwTjAdBgNVHQ4EFgQUrie5ZLOrA/HuhW1b +/CHjzEvj34swHwYDVR0jBBgwFoAUrie5ZLOrA/HuhW1b/CHjzEvj34swDAYDVR0T +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAkSOP0FUgIIUeJTObgXrenHzZpLAk +qXi37dgdYuPhNveo3agueP51N7yIoh6YGShiJ73Rvr+lVYTwFXStrLih1Wh3tWvk +sMxnvocgd7l6USRb5/AgH7eHeFK4DoCAak2hUAcCLDRJN3XMhNLpyJhw7GJxowVI +GUlxcW5Asrmh9qflfyMyjripTP3CdHobmNcNHyScjNncKj37m8vomel9acekTtDl +2Ci7nLdE+3VqQCXMIfLiF3PO0gGpKei0RuVCSOG6W83zVInCPd/l3aluSR+f/VZl +k8KGQ4As4uTQi89j+J1YepzG0ASMZpjVbXeIg5QBAywVxBh5XVTz37KN8A== +-----END CERTIFICATE----- diff --git a/pom.xml b/pom.xml index 42e1ee51..0220510c 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,12 @@ [1.3,) test + + com.jcabi + jcabi-matchers + [1.3,) + test + org.mockito mockito-core