/* * Copyright (C) 2017-2025 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.util; import org.bouncycastle.asn1.*; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.OtherName; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.jivesoftware.util.cert.SANCertificateIdentityMapping; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import java.io.InputStream; import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.time.*; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.*; /** * Unit test to validate the functionality of @{link CertificateManager}. * * @author Guus der Kinderen, guus@goodbytes.nl */ public class CertificateManagerTest { public static final ASN1ObjectIdentifier XMPP_ADDR_OID = new ASN1ObjectIdentifier( "1.3.6.1.5.5.7.8.5" ); public static final ASN1ObjectIdentifier DNS_SRV_OID = new ASN1ObjectIdentifier( "1.3.6.1.5.5.7.8.7" ); public static final int KEY_SIZE = 2048; public static final String KEY_ALGORITHM = "RSA"; public static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; private static KeyPair subjectKeyPair; private static KeyPair issuerKeyPair; private static ContentSigner contentSigner; @BeforeAll public static void initialize() throws Exception { final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); keyPairGenerator.initialize( KEY_SIZE ); subjectKeyPair = keyPairGenerator.generateKeyPair(); issuerKeyPair = keyPairGenerator.generateKeyPair(); contentSigner = new JcaContentSignerBuilder( SIGNATURE_ALGORITHM ).build( issuerKeyPair.getPrivate() ); } /** * {@link CertificateManager#getServerIdentities(X509Certificate)} should return: * * * when a certificate contains: * */ @Test public void testServerIdentitiesCommonNameOnly() throws Exception { // Setup fixture. final String subjectCommonName = "MySubjectCommonName"; final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( new X500Name( "CN=MyIssuer" ), // Issuer BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number Date.from( Instant.now().plus(Duration.ofDays(30)) ), // Not before 30 days ago Date.from( Instant.now().minus(Duration.ofDays(99)) ), // Not after 99 days from now new X500Name( "CN=" + subjectCommonName ), // Subject subjectKeyPair.getPublic() ); final X509CertificateHolder certificateHolder = builder.build( contentSigner ); final X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder ); // Execute system under test final List serverIdentities = CertificateManager.getServerIdentities( cert ); // Verify result assertEquals( 1, serverIdentities.size() ); assertEquals( subjectCommonName, serverIdentities.get( 0 ) ); } /** * {@link CertificateManager#getServerIdentities(X509Certificate)} should return: * * * when a certificate contains: * */ @Test public void testServerIdentitiesXmppAddr() throws Exception { // Setup fixture. final String subjectCommonName = "MySubjectCommonName"; final String subjectAltNameXmppAddr = "MySubjectAltNameXmppAddr"; final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( new X500Name( "CN=MyIssuer" ), // Issuer BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number Date.from( Instant.now().plus(Duration.ofDays(30)) ), // Not before 30 days ago Date.from( Instant.now().minus(Duration.ofDays(99)) ), // Not after 99 days from now new X500Name( "CN=" + subjectCommonName ), // Subject subjectKeyPair.getPublic() ); final OtherName otherName = new OtherName(XMPP_ADDR_OID, new DERUTF8String( subjectAltNameXmppAddr ) ); final GeneralNames subjectAltNames = new GeneralNames( new GeneralName(GeneralName.otherName, otherName ) ); builder.addExtension( Extension.subjectAlternativeName, true, subjectAltNames ); final X509CertificateHolder certificateHolder = builder.build( contentSigner ); X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder ); // FIXME: Unsure why, but without this back-and-forth, tests will fail on Java 17. final String value = CertificateManager.toPemRepresentation(cert); final Collection chain = CertificateManager.parseCertificates(value); cert = chain.iterator().next(); // Execute system under test final List serverIdentities = CertificateManager.getServerIdentities( cert ); // Verify result assertEquals( 1, serverIdentities.size() ); assertTrue( serverIdentities.contains( subjectAltNameXmppAddr )); assertFalse( serverIdentities.contains( subjectCommonName ) ); } /** * {@link CertificateManager#getServerIdentities(X509Certificate)} should return: *
    *
  • the 'DNS SRV' subjectAltName value
  • *
  • explicitly not the Common Name
  • *
* * when a certificate contains: *
    *
  • a subjectAltName entry of type otherName with an ASN.1 Object Identifier of "id-on-dnsSRV"
  • *
*/ @Test public void testServerIdentitiesDnsSrv() throws Exception { // Setup fixture. final String subjectCommonName = "MySubjectCommonName"; final String subjectAltNameDnsSrv = "MySubjectAltNameXmppAddr"; final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( new X500Name( "CN=MyIssuer" ), // Issuer BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number Date.from( Instant.now().plus(Duration.ofDays(30)) ), // Not before 30 days ago Date.from( Instant.now().minus(Duration.ofDays(99)) ), // Not after 99 days from now new X500Name( "CN=" + subjectCommonName ), // Subject subjectKeyPair.getPublic() ); final OtherName otherName = new OtherName(DNS_SRV_OID, new DERIA5String( "_xmpp-server."+subjectAltNameDnsSrv ) ); final GeneralNames subjectAltNames = new GeneralNames( new GeneralName(GeneralName.otherName, otherName ) ); builder.addExtension( Extension.subjectAlternativeName, true, subjectAltNames ); final X509CertificateHolder certificateHolder = builder.build( contentSigner ); X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder ); // FIXME: Unsure why, but without this back-and-forth, tests will fail on Java 17. final String value = CertificateManager.toPemRepresentation(cert); final Collection chain = CertificateManager.parseCertificates(value); cert = chain.iterator().next(); // Execute system under test final List serverIdentities = CertificateManager.getServerIdentities( cert ); // Verify result assertEquals( 1, serverIdentities.size() ); assertTrue( serverIdentities.contains( subjectAltNameDnsSrv )); assertFalse( serverIdentities.contains( subjectCommonName ) ); } /** * {@link CertificateManager#getServerIdentities(X509Certificate)} should return: *
    *
  • the DNS subjectAltName value
  • *
  • explicitly not the Common Name
  • *
* * when a certificate contains: *
    *
  • a subjectAltName entry of type DNS
  • *
*/ @Test public void testServerIdentitiesDNS() throws Exception { // Setup fixture. final String subjectCommonName = "MySubjectCommonName"; final String subjectAltNameDNS = "MySubjectAltNameDNS"; final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( new X500Name( "CN=MyIssuer" ), // Issuer BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number Date.from( Instant.now().plus(Duration.ofDays(30)) ), // Not before 30 days ago Date.from( Instant.now().minus(Duration.ofDays(99)) ), // Not after 99 days from now new X500Name( "CN=" + subjectCommonName ), // Subject subjectKeyPair.getPublic() ); final GeneralNames generalNames = new GeneralNames(new GeneralName(GeneralName.dNSName, subjectAltNameDNS)); builder.addExtension( Extension.subjectAlternativeName, false, generalNames ); final X509CertificateHolder certificateHolder = builder.build( contentSigner ); X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder ); // FIXME: Unsure why, but without this back-and-forth, tests will fail on Java 17. final String value = CertificateManager.toPemRepresentation(cert); final Collection chain = CertificateManager.parseCertificates(value); cert = chain.iterator().next(); // Execute system under test final List serverIdentities = CertificateManager.getServerIdentities( cert ); // Verify result assertEquals( 1, serverIdentities.size() ); assertTrue( serverIdentities.contains( subjectAltNameDNS ) ); assertFalse( serverIdentities.contains( subjectCommonName ) ); } /** * {@link CertificateManager#getServerIdentities(X509Certificate)} should return: *
    *
  • the DNS subjectAltName value
  • *
  • the 'xmppAddr' subjectAltName value
  • *
  • explicitly not the Common Name
  • *
* * when a certificate contains: *
    *
  • a subjectAltName entry of type DNS
  • *
  • a subjectAltName entry of type otherName with an ASN.1 Object Identifier of "id-on-xmppAddr"
  • *
*/ @Test public void testServerIdentitiesXmppAddrAndDNS() throws Exception { // Setup fixture. final String subjectCommonName = "MySubjectCommonName"; final String subjectAltNameXmppAddr = "MySubjectAltNameXmppAddr"; final String subjectAltNameDNS = "MySubjectAltNameDNS"; final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( new X500Name( "CN=MyIssuer" ), // Issuer BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ), // Random serial number Date.from( Instant.now().plus(Duration.ofDays(30)) ), // Not before 30 days ago Date.from( Instant.now().minus(Duration.ofDays(99)) ), // Not after 99 days from now new X500Name( "CN=" + subjectCommonName ), // Subject subjectKeyPair.getPublic() ); final OtherName otherName = new OtherName(XMPP_ADDR_OID, new DERUTF8String( subjectAltNameXmppAddr ) ); final GeneralNames subjectAltNames = new GeneralNames( new GeneralName[] { new GeneralName( GeneralName.otherName, otherName ), new GeneralName( GeneralName.dNSName, subjectAltNameDNS ) }); builder.addExtension( Extension.subjectAlternativeName, true, subjectAltNames ); final X509CertificateHolder certificateHolder = builder.build( contentSigner ); X509Certificate cert = new JcaX509CertificateConverter().getCertificate( certificateHolder ); // FIXME: Unsure why, but without this back-and-forth, tests will fail on Java 17. final String value = CertificateManager.toPemRepresentation(cert); final Collection chain = CertificateManager.parseCertificates(value); cert = chain.iterator().next(); // Execute system under test final List serverIdentities = CertificateManager.getServerIdentities( cert ); // Verify result assertEquals( 2, serverIdentities.size() ); assertTrue( serverIdentities.contains( subjectAltNameXmppAddr )); assertFalse( serverIdentities.contains( subjectCommonName ) ); } /** * Tests a PEM generated by OpenSSL using this config file: * * * [ req ] * default_bits = 2048 * distinguished_name = req_distinguished_name * req_extensions = req_ext * x509_extensions = v3_ca # The main difference * prompt = no * * [ req_distinguished_name ] * C = US * ST = YourState * L = YourCity * O = YourOrganization * OU = YourUnit * CN = yourdomain.com * * [ req_ext ] * subjectAltName = @alt_names * * [ v3_ca ] * subjectAltName = @alt_names * basicConstraints = CA:TRUE * keyUsage = digitalSignature, keyEncipherment * extendedKeyUsage = serverAuth, clientAuth * * [ alt_names ] * otherName.0 = 1.3.6.1.5.5.7.8.7;IA5:_xmpp-server.service.example.com * otherName.1 = 1.3.6.1.5.5.7.8.7;IA5:_dns.service.example.net * otherName.2 = 1.3.6.1.5.5.7.8.5;UTF8:user@example.com * otherName.3 = 1.3.6.1.5.5.7.8.5;UTF8:not-a-user.example.com * URI.1 = xmpp:third-one.net * DNS.1 = yourdomain.com * DNS.2 = anotherdomain.com * IP.1 = 192.168.1.1 * * * Using: * * $ openssl req -new -key mykey.key -out mycsr.csr -config san.cnf * $ openssl x509 -req -days 365 -in mycsr.csr -signkey mykey.key -out mycert.crt -extfile san.cnf -extensions v3_ca * Signature ok * subject=C = US, ST = YourState, L = YourCity, O = YourOrganization, OU = YourUnit, CN = yourdomain.com * Getting Private key * * * @see OF-2904 */ @Test public void testPrebuiltPEM() throws Exception { // Setup test fixture. final Collection chain = CertificateManager.parseCertificates( """ -----BEGIN CERTIFICATE----- MIIErTCCA5WgAwIBAgIULIC8uiTUXMHADnhPH6YH2BoFcOIwDQYJKoZIhvcNAQEL BQAwezELMAkGA1UEBhMCVVMxEjAQBgNVBAgMCVlvdXJTdGF0ZTERMA8GA1UEBwwI WW91ckNpdHkxGTAXBgNVBAoMEFlvdXJPcmdhbml6YXRpb24xETAPBgNVBAsMCFlv dXJVbml0MRcwFQYDVQQDDA55b3VyZG9tYWluLmNvbTAeFw0yNDExMTAxNjI4MzZa Fw0yNTExMTAxNjI4MzZaMHsxCzAJBgNVBAYTAlVTMRIwEAYDVQQIDAlZb3VyU3Rh dGUxETAPBgNVBAcMCFlvdXJDaXR5MRkwFwYDVQQKDBBZb3VyT3JnYW5pemF0aW9u MREwDwYDVQQLDAhZb3VyVW5pdDEXMBUGA1UEAwwOeW91cmRvbWFpbi5jb20wggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMDg2nLepMRS6o3F5oSiP/U4yh 5lOWSE24VQE4R0EMbTiQ1lATIA0AbYU0MbVfu2EU+6rcyml7wSwekVBdRq/KLcvH 5mJjmQ25qHzJIFzxqNtUygY790job51zpOsIaFfg+MZkCdCWQK5G4qUr5bkfCKCN VCiFcTi1nJo/PIP5Cx+/NCq3iFUL//Dt4+UxADUhD9mdXODIFUYGAP0IDD5hL58g 0IPNAAECky1fx4oSP1G0I8IYEnZ7V3RXvO82WZOlthJTtyysVTlIt6vy2cyG6WIg iuBYOyl3Uf1S//TAMQwDF6oBO43EkqJqEODe4HTdMODd+72LY/4HSbikyBvRAgMB AAGjggEnMIIBIzCB5gYDVR0RBIHeMIHboC4GCCsGAQUFBwgHoCIWIF94bXBwLXNl cnZlci5zZXJ2aWNlLmV4YW1wbGUuY29toCYGCCsGAQUFBwgHoBoWGF9kbnMuc2Vy dmljZS5leGFtcGxlLm5ldKAeBggrBgEFBQcIBaASDBB1c2VyQGV4YW1wbGUuY29t oCQGCCsGAQUFBwgFoBgMFm5vdC1hLXVzZXIuZXhhbXBsZS5jb22GEnhtcHA6dGhp cmQtb25lLm5ldIIOeW91cmRvbWFpbi5jb22CEWFub3RoZXJkb21haW4uY29thwTA qAEBMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUF BwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEADrekbzSNviLTvI8DXqBD JnNPPS98nzWgABscB5Xups+G7Jrj4aibNHonePXW8B6rOqEYeBBbIzCYRRRPbuGl kqksCmGa0/CWYX0uf4RoLaGy5BzZndJWYNPe/Hj5GbyLbFCFNyBOMDz0NyrwfVoH Yq0W2rkve2SWKp7iiiUc80qKj4tcTX25x5h8oLgv7Lh4OAGKXFr6TYk23wdDPjiC zZlXLN8TFw+RT7LQQc/Xi8XC/1ULbLalTEwh/xIaKju5P5CBTZO9xnVDc9LJ3hww TN04BDlf3U02OCoSr0SxiLmmDRJOLbzGJK2AEQPpHUM5URcd98Tf2GzyUvxfhHUc 7A== -----END CERTIFICATE-----"""); final SANCertificateIdentityMapping mapper = new SANCertificateIdentityMapping(); // Execute system under test. final List result = mapper.mapIdentity(chain.iterator().next()); // Verify results assertTrue(result.contains("service.example.com"), "Expected the to contain 'service.example.com', as the certificate contains an id-on-dnsSRV OtherName entry with value '_xmpp-server.service.example.com'"); assertFalse(result.contains("service.example.net"), "Didn't expect the result to contain 'service.example.net. Although the certificate contains an id-on-dnsSRV OtherName entry with value '_dns.service.example.net', it service is not of an XMPP-type (but rather, DNS)."); assertTrue(result.contains("user@example.com"), "Expected the result to contain 'user@example.com', as the certificate contains that value in an id-on-xmppAddr OtherName entry."); assertTrue(result.contains("not-a-user.example.com"), "Expected the result to contain 'not-a-user.example.com', as the certificate contains that value in an id-on-xmppAddr OtherName entry."); assertFalse(result.contains("third-one.net"), "Didn't expect the result to contain 'third-one.net, which is provided in an URI entry in the certificate. URI entries are not defined as a valid source for JIDs in a certificate by RFC6120."); assertTrue(result.contains("yourdomain.com"), "Expected the result to contain 'yourdomain.com', as the certificate contains that value in DNS entry."); assertTrue(result.contains("anotherdomain.com"), "Expected the result to contain 'anotherdomain.com', as the certificate contains that value in DNS entry."); assertFalse(result.contains("192.168.1.1"), "Didn't expect the result to contain '192.168.1.1, which is provided in an IP entry in the certificate. IP entries are not defined as a valid source for JIDs in a certificate by RFC6120."); } /** * Asserts that {@link CertificateManager#parsePrivateKey(InputStream, String)} can parse a password-less private * key PEM file. */ @Test public void testParsePrivateKey() throws Exception { // Setup fixture. try ( final InputStream stream = getClass().getResourceAsStream( "/privatekey.pem" ) ) { // Execute system under test. final PrivateKey result = CertificateManager.parsePrivateKey( stream, "" ); // Verify result. assertNotNull( result ); } } /** * Asserts that {@link CertificateManager#parseCertificates(InputStream)} can parse a PEM file that contains a * certificate chain */ @Test public void testParseFullChain() throws Exception { // Setup fixture. try ( final InputStream stream = getClass().getResourceAsStream( "/fullchain.pem" ) ) { // Execute system under test. final Collection result = CertificateManager.parseCertificates( stream ); // Verify result. assertNotNull( result ); assertEquals( 2, result.size() ); } } /** * Asserts the date-based validity constraints in a certificate that is generated by {@link org.jivesoftware.util.CertificateManager#createX509V3Certificate(KeyPair, int, X500NameBuilder, X500NameBuilder, String, String, Set)} */ @Test public void testGenerateCertificateDateValidity() throws Exception { // Setup fixture. final KeyPair keyPair = subjectKeyPair; final int days = 2; final String issuerCommonName = "issuer common name"; final String subjectCommonName = "subject common name"; final String domain = "domain.example.org"; final Set sanDnsNames = Stream.of( "alternative-a.example.org", "alternative-b.example.org" ).collect( Collectors.toSet() ); // Execute system under test. final X509Certificate result = CertificateManager.createX509V3Certificate( keyPair, days, issuerCommonName, subjectCommonName, domain, SIGNATURE_ALGORITHM, sanDnsNames ); // Verify results. assertNotNull( result ); assertCertificateDateValid( "The generated certificate is expected to be valid immediately (but is not).", result, new Date() ); assertCertificateDateValid( "The generated certificate is expected to be valid half was during its maximum validity period (but is not).", result, addDays( days / 2 ) ); assertCertificateDateNotValid( "The generated certificate is not expected to be valid on a date before it was created (but is).", result, addDays( -1 ) ); assertCertificateDateNotValid( "The generated certificate is not expected to be valid after its maximum validity period has ended (but is).", result, addDays( days * 2 ) ); } /** * Asserts that the provided common name of the issuer is returned as part of the issuer distinguished name in a certificate that is generated by {@link org.jivesoftware.util.CertificateManager#createX509V3Certificate(KeyPair, int, X500NameBuilder, X500NameBuilder, String, String, Set)} */ @Test public void testGenerateCertificateIssuer() throws Exception { // Setup fixture. final KeyPair keyPair = subjectKeyPair; final int days = 2; final String issuerCommonName = "issuer common name"; final String subjectCommonName = "subject common name"; final String domain = "domain.example.org"; final Set sanDnsNames = Stream.of( "alternative-a.example.org", "alternative-b.example.org" ).collect( Collectors.toSet() ); // Execute system under test. final X509Certificate result = CertificateManager.createX509V3Certificate( keyPair, days, issuerCommonName, subjectCommonName, domain, SIGNATURE_ALGORITHM, sanDnsNames ); // Verify results. assertNotNull( result ); final Set foundIssuerCNs = parse( result.getIssuerX500Principal().getName(), "CN" ); assertEquals( 1, foundIssuerCNs.size() ); assertEquals( issuerCommonName, foundIssuerCNs.iterator().next() ); } /** * Asserts that the provided common name of the subject is returned as part of the subject distinguished name in a certificate that is generated by {@link org.jivesoftware.util.CertificateManager#createX509V3Certificate(KeyPair, int, X500NameBuilder, X500NameBuilder, String, String, Set)} */ @Test public void testGenerateCertificateSubject() throws Exception { // Setup fixture. final KeyPair keyPair = subjectKeyPair; final int days = 2; final String issuerCommonName = "issuer common name"; final String subjectCommonName = "subject common name"; final String domain = "domain.example.org"; final Set sanDnsNames = Stream.of( "alternative-a.example.org", "alternative-b.example.org" ).collect( Collectors.toSet() ); // Execute system under test. final X509Certificate result = CertificateManager.createX509V3Certificate( keyPair, days, issuerCommonName, subjectCommonName, domain, SIGNATURE_ALGORITHM, sanDnsNames ); // Verify results. assertNotNull( result ); final Set foundSubjectCNs = parse( result.getSubjectX500Principal().getName(), "CN" ); assertEquals( 1, foundSubjectCNs.size() ); assertEquals( subjectCommonName, foundSubjectCNs.iterator().next() ); } /** * Asserts that the provided subject alternative DNS names are returned as part of the subject alternative names in a certificate that is generated by {@link org.jivesoftware.util.CertificateManager#createX509V3Certificate(KeyPair, int, X500NameBuilder, X500NameBuilder, String, String, Set)} */ @Test public void testGenerateCertificateSubjectAlternativeNames() throws Exception { // Setup fixture. final KeyPair keyPair = subjectKeyPair; final int days = 2; final String issuerCommonName = "issuer common name"; final String subjectCommonName = "subject common name"; final String domain = "domain.example.org"; final Set sanDnsNames = Stream.of( "alternative-a.example.org", "alternative-b.example.org" ).collect( Collectors.toSet() ); // Execute system under test. final X509Certificate result = CertificateManager.createX509V3Certificate( keyPair, days, issuerCommonName, subjectCommonName, domain, SIGNATURE_ALGORITHM, sanDnsNames ); // Verify results. assertNotNull( result ); final ArrayList> expectedNames = new ArrayList<>(); for ( final String sanDnsName : sanDnsNames ) { expectedNames.add( Arrays.asList( 2 , sanDnsName ) ); } assertThat( "Expected to find all 'alternative names' as DNS entries in the subject alternative names (but does not).", result.getSubjectAlternativeNames(), both( hasSize( sanDnsNames.size() ) ) .and( containsInAnyOrder( expectedNames.toArray() ) ) ); } public static Date addDays( int amount ) { final Calendar instance = Calendar.getInstance(); instance.add( Calendar.DATE, amount ); return instance.getTime(); } /** * * @see