Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
.gradle
.idea
.vscode
build
out
bin
**/bin/
*.class

# macOS
.DS_Store
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
Expand All @@ -28,8 +27,6 @@
*/
public class X509Svid {

private static final int URI_SAN_TYPE = 6;

SpiffeId spiffeId;

/**
Expand Down Expand Up @@ -242,37 +239,14 @@ private static SpiffeId getSpiffeId(final List<X509Certificate> x509Certificates
final SpiffeId spiffeId;
try {
X509Certificate leaf = x509Certificates.get(0);
validateLeafHasSingleUriSan(leaf);
X509SvidProfile.validateLeafHasSingleUriSan(leaf);
spiffeId = CertificateUtils.getSpiffeId(leaf);
} catch (CertificateException e) {
throw new X509SvidException(e.getMessage(), e);
}
return spiffeId;
}

private static void validateLeafHasSingleUriSan(final X509Certificate leaf)
throws CertificateException, X509SvidException {
final Collection<List<?>> subjectAlternativeNames = leaf.getSubjectAlternativeNames();

int uriSanCount = 0;
if (subjectAlternativeNames != null) {
for (List<?> sanEntry : subjectAlternativeNames) {
if (sanEntry == null || sanEntry.isEmpty()) {
continue;
}

Object sanType = sanEntry.get(0);
if (sanType instanceof Integer && (Integer) sanType == URI_SAN_TYPE) {
uriSanCount++;
}
}
}

if (uriSanCount != 1) {
throw new X509SvidException("Leaf certificate must contain exactly one URI SAN");
}
}

private static PrivateKey generatePrivateKey(final byte[] privateKeyBytes,
final KeyFileFormat keyFileFormat,
final List<X509Certificate> x509Certificates)
Expand Down Expand Up @@ -315,21 +289,6 @@ private static void verifyCaCert(final X509Certificate cert) throws X509SvidExce
}

private static void validateLeafCertificate(final X509Certificate leaf) throws X509SvidException {
if (CertificateUtils.isCA(leaf)) {
throw new X509SvidException("Leaf certificate must not have CA flag set to true");
}
validateKeyUsageOfLeafCertificate(leaf);
}

private static void validateKeyUsageOfLeafCertificate(final X509Certificate leaf) throws X509SvidException {
if (!CertificateUtils.hasKeyUsageDigitalSignature(leaf)) {
throw new X509SvidException("Leaf certificate must have 'digitalSignature' as key usage");
}
if (CertificateUtils.hasKeyUsageCertSign(leaf)) {
throw new X509SvidException("Leaf certificate must not have 'keyCertSign' as key usage");
}
if (CertificateUtils.hasKeyUsageCRLSign(leaf)) {
throw new X509SvidException("Leaf certificate must not have 'cRLSign' as key usage");
}
X509SvidProfile.validateLeafCertificate(leaf);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.spiffe.svid.x509svid;

import io.spiffe.exception.X509SvidException;
import io.spiffe.internal.CertificateUtils;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;

final class X509SvidProfile {

private static final int URI_SAN_TYPE = 6;

private X509SvidProfile() {
}

static void validateLeaf(final X509Certificate leaf) throws CertificateException, X509SvidException {
validateLeafHasSingleUriSan(leaf);
validateLeafCertificate(leaf);
}

static void validateLeafHasSingleUriSan(final X509Certificate leaf)
throws CertificateException, X509SvidException {
final Collection<List<?>> subjectAlternativeNames = leaf.getSubjectAlternativeNames();

int uriSanCount = 0;
if (subjectAlternativeNames != null) {
for (List<?> sanEntry : subjectAlternativeNames) {
if (sanEntry == null || sanEntry.isEmpty()) {
continue;
}

Object sanType = sanEntry.get(0);
if (sanType instanceof Integer && (Integer) sanType == URI_SAN_TYPE) {
uriSanCount++;
}
}
}

if (uriSanCount != 1) {
throw new X509SvidException("Leaf certificate must contain exactly one URI SAN");
}
}

static void validateLeafCertificate(final X509Certificate leaf) throws X509SvidException {
if (CertificateUtils.isCA(leaf)) {
throw new X509SvidException("Leaf certificate must not have CA flag set to true");
}
validateKeyUsageOfLeafCertificate(leaf);
}

private static void validateKeyUsageOfLeafCertificate(final X509Certificate leaf) throws X509SvidException {
if (!CertificateUtils.hasKeyUsageDigitalSignature(leaf)) {
throw new X509SvidException("Leaf certificate must have 'digitalSignature' as key usage");
}
if (CertificateUtils.hasKeyUsageCertSign(leaf)) {
throw new X509SvidException("Leaf certificate must not have 'keyCertSign' as key usage");
}
if (CertificateUtils.hasKeyUsageCRLSign(leaf)) {
throw new X509SvidException("Leaf certificate must not have 'cRLSign' as key usage");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.spiffe.bundle.BundleSource;
import io.spiffe.bundle.x509bundle.X509Bundle;
import io.spiffe.exception.BundleNotFoundException;
import io.spiffe.exception.X509SvidException;
import io.spiffe.internal.CertificateUtils;
import io.spiffe.spiffeid.SpiffeId;
import io.spiffe.spiffeid.TrustDomain;
Expand Down Expand Up @@ -43,6 +44,12 @@ public static void verifyChain(
Objects.requireNonNull(chain, "chain must not be null");
Objects.requireNonNull(x509BundleSource, "x509BundleSource must not be null");

try {
X509SvidProfile.validateLeaf(chain.get(0));
} catch (X509SvidException e) {
throw new CertificateException(e.getMessage(), e);
}

TrustDomain trustDomain = CertificateUtils.getTrustDomain(chain);
X509Bundle x509Bundle = x509BundleSource.getBundleForTrustDomain(trustDomain);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ public static CertAndKeyPair createCertificateWithoutKeyUsage(
return new CertAndKeyPair(cert, certKeyPair);
}

/**
* Creates a non-CA certificate with a custom KeyUsage extension.
*/
public static CertAndKeyPair createCertificateWithKeyUsage(
String subject,
String issuerSubject,
String spiffeId,
CertAndKeyPair issuer,
boolean digitalSignature,
boolean keyCertSign,
boolean crlSign
) throws Exception {
KeyPair certKeyPair = generateKeyPair();
PrivateKey issuerKey = issuer.keyPair.getPrivate();
JcaX509v3CertificateBuilder builder = getCertificateBuilder(certKeyPair, subject, issuerSubject);
addCertExtensions(builder, Collections.singletonList(spiffeId), false, true);
builder.replaceExtension(Extension.keyUsage, true,
new KeyUsage(keyUsage(digitalSignature, keyCertSign, crlSign)));
X509Certificate cert = getSignedX509Certificate(issuerKey, builder);
return new CertAndKeyPair(cert, certKeyPair);
}

private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
return keyGen.generateKeyPair();
Expand Down Expand Up @@ -151,6 +173,20 @@ private static void addCAExtensions(JcaX509v3CertificateBuilder builder, KeyPair
builder.addExtension(Extension.subjectKeyIdentifier, false, new SubjectKeyIdentifier(certKeyPair.getPublic().getEncoded()));
}

private static int keyUsage(boolean digitalSignature, boolean keyCertSign, boolean crlSign) {
int usage = 0;
if (digitalSignature) {
usage |= KeyUsage.digitalSignature;
}
if (keyCertSign) {
usage |= KeyUsage.keyCertSign;
}
if (crlSign) {
usage |= KeyUsage.cRLSign;
}
return usage;
}

private static JcaX509v3CertificateBuilder getCertificateBuilder(KeyPair certKeyPair, String subject, String issuerSubject) {
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
Instant validFrom = Instant.now().minus(5, ChronoUnit.DAYS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@
import java.nio.ByteBuffer;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

import static io.spiffe.utils.X509CertificateTestUtils.createCertificate;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificateWithKeyUsage;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificateWithUriSans;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificateWithoutKeyUsage;
import static io.spiffe.utils.X509CertificateTestUtils.createRootCA;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -333,6 +338,67 @@ void checkServerTrusted_passCertificateWithNonAcceptedSpiffeId_ThrowCertificateE
}
}

@Test
void checkTrusted_leafCertificateWithCaTrue_throwsCertificateException() throws Exception {
PeerChain peerChain = createPeerChain((subject, issuerSubject, spiffeId, issuer) ->
createCertificate(subject, issuerSubject, spiffeId, issuer, true));

assertPeerChainRejected(
peerChain,
"Leaf certificate must not have CA flag set to true"
);
}

@Test
void checkTrusted_leafCertificateWithoutDigitalSignature_throwsCertificateException() throws Exception {
PeerChain peerChain = createPeerChain((subject, issuerSubject, spiffeId, issuer) ->
createCertificateWithoutKeyUsage(subject, issuerSubject, spiffeId, issuer, false));

assertPeerChainRejected(
peerChain,
"Leaf certificate must have 'digitalSignature' as key usage"
);
}

@Test
void checkTrusted_leafCertificateWithKeyCertSign_throwsCertificateException() throws Exception {
PeerChain peerChain = createPeerChain((subject, issuerSubject, spiffeId, issuer) ->
createCertificateWithKeyUsage(subject, issuerSubject, spiffeId, issuer, true, true, false));

assertPeerChainRejected(
peerChain,
"Leaf certificate must not have 'keyCertSign' as key usage"
);
}

@Test
void checkTrusted_leafCertificateWithCrlSign_throwsCertificateException() throws Exception {
PeerChain peerChain = createPeerChain((subject, issuerSubject, spiffeId, issuer) ->
createCertificateWithKeyUsage(subject, issuerSubject, spiffeId, issuer, true, false, true));

assertPeerChainRejected(
peerChain,
"Leaf certificate must not have 'cRLSign' as key usage"
);
}

@Test
void checkTrusted_leafCertificateWithAdditionalNonSpiffeUriSan_throwsCertificateException() throws Exception {
PeerChain peerChain = createPeerChain((subject, issuerSubject, spiffeId, issuer) ->
createCertificateWithUriSans(
subject,
issuerSubject,
Arrays.asList(spiffeId, "https://example.org/workload"),
issuer,
false
));

assertPeerChainRejected(
peerChain,
"Leaf certificate must contain exactly one URI SAN"
);
}

@Test
void checkServerTrusted_verifierResult_ThrowCertificateException() throws BundleNotFoundException {
when(bundleSource.getBundleForTrustDomain(TrustDomain.parse("example.org"))).thenReturn(bundleKnown);
Expand Down Expand Up @@ -372,6 +438,71 @@ void getAcceptedIssuers() {
assertEquals(0, acceptedIssuers.length);
}

private PeerChain createPeerChain(LeafCertificateFactory leafCertificateFactory) throws Exception {
final String subject = "C = US, O = SPIRE";
final String issuerSubject = "C = US, O = SPIFFE";

final TrustDomain trustDomain = TrustDomain.parse("spiffe://example.org");
final SpiffeId spiffeIdRoot = trustDomain.newSpiffeId();
final SpiffeId spiffeIdHost1 = SpiffeId.fromSegments(trustDomain, "host1");
final SpiffeId spiffeIdHost2 = SpiffeId.fromSegments(trustDomain, "host2");
final SpiffeId spiffeIdTest = SpiffeId.fromSegments(trustDomain, "test");

final CertAndKeyPair rootCa = createRootCA(issuerSubject, spiffeIdRoot.toString());
final CertAndKeyPair intermediate1 = createCertificate(subject, issuerSubject, spiffeIdHost1.toString(), rootCa, true);
final CertAndKeyPair intermediate2 = createCertificate(subject, subject, spiffeIdHost2.toString(), intermediate1, true);
final CertAndKeyPair leaf = leafCertificateFactory.create(
subject,
subject,
spiffeIdTest.toString(),
intermediate2
);

X509Certificate[] chain = new X509Certificate[]{
leaf.getCertificate(),
intermediate2.getCertificate(),
intermediate1.getCertificate()
};
X509Bundle bundle = X509Bundle.parse(trustDomain, rootCa.getCertificate().getEncoded());
return new PeerChain(trustDomain, spiffeIdTest, chain, bundle);
}

private void assertPeerChainRejected(PeerChain peerChain, String expectedError) throws BundleNotFoundException {
acceptedSpiffeIds = Collections.singleton(peerChain.spiffeId);
when(bundleSource.getBundleForTrustDomain(peerChain.trustDomain)).thenReturn(peerChain.bundle);

CertificateException clientException = assertThrows(
CertificateException.class,
() -> spiffeTrustManager.checkClientTrusted(peerChain.chain, "")
);
assertEquals(expectedError, clientException.getMessage());

CertificateException serverException = assertThrows(
CertificateException.class,
() -> spiffeTrustManager.checkServerTrusted(peerChain.chain, "")
);
assertEquals(expectedError, serverException.getMessage());
}

private interface LeafCertificateFactory {
CertAndKeyPair create(String subject, String issuerSubject, String spiffeId, CertAndKeyPair issuer)
throws Exception;
}

private static class PeerChain {
TrustDomain trustDomain;
SpiffeId spiffeId;
X509Certificate[] chain;
X509Bundle bundle;

PeerChain(TrustDomain trustDomain, SpiffeId spiffeId, X509Certificate[] chain, X509Bundle bundle) {
this.trustDomain = trustDomain;
this.spiffeId = spiffeId;
this.chain = chain;
this.bundle = bundle;
}
}

private SSLEngine getSslEngineStub() {
return new SSLEngine() {
@Override
Expand Down