Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ELY-1446] Add utility methods for converting an unordered array of certificates into an ordered X.509 certificate chain and for creating an X.509 certificate chain from a map of certificates #1035

Merged
merged 1 commit into from
Nov 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/java/org/wildfly/security/_private/ElytronMessages.java
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,18 @@ public interface ElytronMessages extends BasicLogger {
@Message(id = 10024, value = "Invalid X.509 certificate extension string value")
IllegalArgumentException invalidCertificateExtensionStringValue();

@Message(id = 10025, value = "Non-X.509 certificate found in certificate array")
IllegalArgumentException nonX509CertificateInCertificateArray();

@Message(id = 10026, value = "Starting public key not found in certificate array")
IllegalArgumentException startingPublicKeyNotFoundInCertificateArray();

@Message(id = 10027, value = "Incomplete certificate array")
IllegalArgumentException incompleteCertificateArray();

@Message(id = 10028, value = "Unable to create X.509 certificate chain from map of certificates")
IllegalArgumentException unableToCreateCertificateChainFromCertificateMap();

/* Audit Exceptions */

// 11000 - Unused in any Final release
Expand Down
120 changes: 120 additions & 0 deletions src/main/java/org/wildfly/security/x500/X500.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@

package org.wildfly.security.x500;

import static org.wildfly.security._private.ElytronMessages.log;

import java.security.Principal;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;

import org.wildfly.common.Assert;

/**
* Useful X500 constants and utilities.
Expand Down Expand Up @@ -137,4 +148,113 @@ public static X509Certificate[] asX509CertificateArray(Object[] certificates) th
return Arrays.copyOf(certificates, certificates.length, X509Certificate[].class);
}
}

/**
* Convert an unordered array of certificates into an ordered X.509 certificate chain.
*
* @param firstPublicKey the public key that should be in the first certificate in the ordered X.509 certificate
* chain (may not be {@code null})
* @param certificates the unordered array of certificates (may not be {@code null})
* @return the ordered X.509 certificate chain, as an array
* @throws IllegalArgumentException if the given unordered array of certificates cannot be converted into an ordered X.509 certificate chain
*/
public static X509Certificate[] asOrderedX509CertificateChain(PublicKey firstPublicKey, Certificate[] certificates) throws IllegalArgumentException {
Assert.checkNotNullParam("firstPublicKey", firstPublicKey);
Assert.checkNotNullParam("certificates", certificates);
X509Certificate[] x509Certificates;
try {
x509Certificates = asX509CertificateArray(certificates);
} catch (ArrayStoreException e) {
throw log.nonX509CertificateInCertificateArray();
}
boolean foundFirstCertificate = false;
for (int i = 0; i < x509Certificates.length; i++) {
if (x509Certificates[i].getPublicKey().equals(firstPublicKey)) {
foundFirstCertificate = true;
swapCertificates(x509Certificates, 0, i);
break;
}
}
if (! foundFirstCertificate) {
throw log.startingPublicKeyNotFoundInCertificateArray();
}
X509Certificate currentCertificate = x509Certificates[0];
for (int i = 1; i < x509Certificates.length - 1; i++) {
boolean issuerCertificateFound = false;
for (int j = i; j < x509Certificates.length; j++) {
if (issuedBy(currentCertificate, x509Certificates[j])) {
swapCertificates(x509Certificates, i, j);
issuerCertificateFound = true;
currentCertificate = x509Certificates[i];
break;
}
}
if (! issuerCertificateFound) {
throw log.incompleteCertificateArray();
}
}
return x509Certificates;
}

/**
* Create an X.509 certificate chain given the first certificate that should be in the chain and a map of certificates.
*
* @param firstCertificate the certificate that should be first in the newly created X.509 certificate chain
* @param certificatesMap a map of distinguished names to certificates to use to create the X.509 certificate chain
* @return the newly created X.509 certificate chain, as an array
* @throws IllegalArgumentException if the X.509 certificate chain could not be created
*/
public static X509Certificate[] createX509CertificateChain(final X509Certificate firstCertificate,
final HashMap<Principal, HashSet<X509Certificate>> certificatesMap) throws IllegalArgumentException {
Assert.checkNotNullParam("firstCertificate", firstCertificate);
Assert.checkNotNullParam("certificatesMap", certificatesMap);
final ArrayList<X509Certificate> certificateChain = new ArrayList<>();
if (createX509CertificateChain(firstCertificate, certificateChain, certificatesMap)) {
Collections.reverse(certificateChain);
return certificateChain.toArray(new X509Certificate[certificateChain.size()]);
}
throw log.unableToCreateCertificateChainFromCertificateMap();
}

private static void swapCertificates(Certificate[] certificates, int i, int j) {
Certificate tempCertificate = certificates[i];
certificates[i] = certificates[j];
certificates[j] = tempCertificate;
}

private static boolean issuedBy(final X509Certificate certificate, X509Certificate issuer) {
if (issuer.getSubjectDN().equals(certificate.getIssuerDN())) {
try {
certificate.verify(issuer.getPublicKey());
return true;
} catch (Exception e) {
return false;
}
}
return false;
}

private static boolean createX509CertificateChain(final X509Certificate firstCertificate, final ArrayList<X509Certificate> certificateChain,
final HashMap<Principal, HashSet<X509Certificate>> certificatesMap) {
if (issuedBy(firstCertificate, firstCertificate)) {
// self-signed
certificateChain.add(firstCertificate);
return true;
}
final HashSet<X509Certificate> issuerCertificates = certificatesMap.get(firstCertificate.getIssuerDN());
if (issuerCertificates == null || issuerCertificates.isEmpty()) {
return false;
}
for (X509Certificate issuerCertificate : issuerCertificates) {
if (issuedBy(firstCertificate, issuerCertificate)) {
// recurse
if (createX509CertificateChain(issuerCertificate, certificateChain, certificatesMap)) {
certificateChain.add(firstCertificate);
return true;
}
}
}
return false;
}

}
159 changes: 159 additions & 0 deletions src/test/java/org/wildfly/security/x500/X500Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2017 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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.wildfly.security.x500;

import static org.junit.Assert.*;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;

import javax.security.auth.x500.X500Principal;

import org.junit.Test;
import org.wildfly.security.asn1.ASN1Encodable;
import org.wildfly.security.x500.cert.X509CertificateBuilder;

/**
* Tests for X500 utilities.
*
* @author <a href="mailto:fjuma@redhat.com">Farah Juma</a>
*/
public class X500Test {

private static X509Certificate[] populateCertificateChain() throws Exception {
KeyPairGenerator keyPairGenerator;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new Error(e);
}
final KeyPair[] keyPairs = new KeyPair[5];
for (int i = 0; i < keyPairs.length; i++) {
keyPairs[i] = keyPairGenerator.generateKeyPair();
}
final X509Certificate[] orderedCertificates = new X509Certificate[5];
for (int i = 0; i < orderedCertificates.length; i++) {
X509CertificateBuilder builder = new X509CertificateBuilder();
X500PrincipalBuilder principalBuilder = new X500PrincipalBuilder();
principalBuilder.addItem(X500AttributeTypeAndValue.create(X500.OID_AT_COMMON_NAME,
ASN1Encodable.ofUtf8String("bob" + i)));
X500Principal dn = principalBuilder.build();
builder.setSubjectDn(dn);
if (i == orderedCertificates.length - 1) {
// self-signed
builder.setIssuerDn(dn);
builder.setSigningKey(keyPairs[i].getPrivate());
} else {
principalBuilder = new X500PrincipalBuilder();
principalBuilder.addItem(X500AttributeTypeAndValue.create(X500.OID_AT_COMMON_NAME,
ASN1Encodable.ofUtf8String("bob" + (i + 1))));
X500Principal issuerDn = principalBuilder.build();
builder.setIssuerDn(issuerDn);
builder.setSigningKey(keyPairs[i + 1].getPrivate());
}
builder.setSignatureAlgorithmName("SHA256withRSA");
builder.setPublicKey(keyPairs[i].getPublic());
orderedCertificates[i] = builder.build();
}
return orderedCertificates;
}

@Test
public void testAsOrderedX509CertificateChain() throws Exception {
final X509Certificate[] orderedCertificates = populateCertificateChain();

X509Certificate[] unorderedCertificates = new X509Certificate[1];
unorderedCertificates[0] = orderedCertificates[4];
assertArrayEquals(new X509Certificate[] { orderedCertificates[4] }, X500.asOrderedX509CertificateChain(orderedCertificates[4].getPublicKey(), unorderedCertificates));

unorderedCertificates = new X509Certificate[5];
unorderedCertificates[0] = orderedCertificates[3];
unorderedCertificates[1] = orderedCertificates[0];
unorderedCertificates[2] = orderedCertificates[4];
unorderedCertificates[3] = orderedCertificates[1];
unorderedCertificates[4] = orderedCertificates[2];
assertArrayEquals(orderedCertificates, X500.asOrderedX509CertificateChain(orderedCertificates[0].getPublicKey(), unorderedCertificates));
}

@Test
public void testAsOrderedX509CertificateChainInvalidValues() throws Exception {
final X509Certificate[] orderedCertificates = populateCertificateChain();
try {
// without starting public key
X500.asOrderedX509CertificateChain(orderedCertificates[0].getPublicKey(), new X509Certificate[] { orderedCertificates[3], orderedCertificates[1], orderedCertificates[2] });
fail("Expected IllegalArgumentException not thrown");
} catch (IllegalArgumentException expected) {
}

try {
// incomplete array
X500.asOrderedX509CertificateChain(orderedCertificates[0].getPublicKey(), new X509Certificate[] { orderedCertificates[4], orderedCertificates[0], orderedCertificates[3], orderedCertificates[1] });
fail("Expected IllegalArgumentException not thrown");
} catch (IllegalArgumentException expected) {
}
}

@Test
public void testCreateX509CertificateChain() throws Exception {
final X509Certificate[] orderedCertificates = populateCertificateChain();
HashMap<Principal, HashSet<X509Certificate>> certificatesMap = new HashMap<>();

certificatesMap = new HashMap<>();
certificatesMap.put(orderedCertificates[4].getSubjectDN(), new HashSet<> (Arrays.asList(orderedCertificates[4])));
assertArrayEquals(new X509Certificate[] { orderedCertificates[4] }, X500.createX509CertificateChain(orderedCertificates[4], certificatesMap));

certificatesMap = new HashMap<>();
for (int i = 0; i < orderedCertificates.length; i++) {
certificatesMap.put(orderedCertificates[i].getSubjectDN(), new HashSet<> (Arrays.asList(orderedCertificates[i])));
}
assertArrayEquals(orderedCertificates, X500.createX509CertificateChain(orderedCertificates[0], certificatesMap));
}

@Test
public void testCreateX509CertificateChainInvalidValues() throws Exception {
final X509Certificate[] orderedCertificates = populateCertificateChain();
HashMap<Principal, HashSet<X509Certificate>> certificatesMap = new HashMap<>();

// incomplete map
for (int i = 0; i < orderedCertificates.length; i+=2) {
certificatesMap.put(orderedCertificates[i].getSubjectDN(), new HashSet<> (Arrays.asList(orderedCertificates[i])));
}
try {
X500.createX509CertificateChain(orderedCertificates[0], certificatesMap);
fail("Expected IllegalArgumentException not thrown");
} catch (IllegalArgumentException expected) {
}

// invalid starting certificate
for (int i = 2; i < orderedCertificates.length; i++) {
certificatesMap.put(orderedCertificates[i].getSubjectDN(), new HashSet<> (Arrays.asList(orderedCertificates[i])));
}
try {
X500.createX509CertificateChain(orderedCertificates[0], certificatesMap);
fail("Expected IllegalArgumentException not thrown");
} catch (IllegalArgumentException expected) {
}
}
}