Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public static SpiffeId getSpiffeId(final X509Certificate certificate) throws Cer
throw new CertificateException("Certificate contains multiple SPIFFE IDs");
}

if (spiffeIds.size() < 1) {
if (spiffeIds.isEmpty()) {
throw new CertificateException("Certificate does not contain SPIFFE ID in the URI SAN");
}

Expand All @@ -143,16 +143,25 @@ public static boolean isCA(final X509Certificate cert) {

public static boolean hasKeyUsageCertSign(final X509Certificate cert) {
boolean[] keyUsage = cert.getKeyUsage();
if (keyUsage == null) {
return false;
}
return keyUsage[KEY_CERT_SIGN.index()];
}

public static boolean hasKeyUsageDigitalSignature(final X509Certificate cert) {
boolean[] keyUsage = cert.getKeyUsage();
if (keyUsage == null) {
return false;
}
return keyUsage[DIGITAL_SIGNATURE.index()];
}

public static boolean hasKeyUsageCRLSign(final X509Certificate cert) {
boolean[] keyUsage = cert.getKeyUsage();
if (keyUsage == null) {
return false;
}
return keyUsage[CRL_SIGN.index()];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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 @@ -27,6 +28,8 @@
*/
public class X509Svid {

private static final int URI_SAN_TYPE = 6;

SpiffeId spiffeId;

/**
Expand Down Expand Up @@ -238,13 +241,38 @@ private static void validateLeafSpiffeId(final SpiffeId spiffeId) throws X509Svi
private static SpiffeId getSpiffeId(final List<X509Certificate> x509Certificates) throws X509SvidException {
final SpiffeId spiffeId;
try {
spiffeId = CertificateUtils.getSpiffeId(x509Certificates.get(0));
X509Certificate leaf = x509Certificates.get(0);
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
import static io.spiffe.internal.AsymmetricKeyAlgorithm.RSA;
import static io.spiffe.utils.TestUtils.toUri;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificate;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificateWithoutKeyUsage;
import static io.spiffe.utils.X509CertificateTestUtils.createRootCA;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
Expand Down Expand Up @@ -196,4 +198,21 @@ void testGetTrustDomain() throws Exception {
fail(e);
}
}

@Test
void keyUsageChecks_noKeyUsageExtension() throws Exception {
final CertAndKeyPair rootCa = createRootCA("C = US, O = SPIFFE", "spiffe://domain.test");
final CertAndKeyPair leaf = createCertificateWithoutKeyUsage(
"C = US, O = SPIRE",
"C = US, O = SPIRE",
"spiffe://domain.test/workload",
rootCa,
false
);

X509Certificate certificate = leaf.getCertificate();
assertFalse(CertificateUtils.hasKeyUsageDigitalSignature(certificate));
assertFalse(CertificateUtils.hasKeyUsageCertSign(certificate));
assertFalse(CertificateUtils.hasKeyUsageCRLSign(certificate));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.stream.Stream;

import static io.spiffe.utils.TestUtils.toUri;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificate;
import static io.spiffe.utils.X509CertificateTestUtils.createCertificateWithUriSans;
import static io.spiffe.utils.X509CertificateTestUtils.createRootCA;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand Down Expand Up @@ -113,7 +115,7 @@ static Stream<Arguments> provideX509SvidScenarios() {
.certsPath(leafEmptyID)
.keyPath(keyRSA)
.hint("")
.expectedError("Certificate does not contain SPIFFE ID in the URI SAN")
.expectedError("Leaf certificate must contain exactly one URI SAN")
.build()
),
Arguments.of(TestCase
Expand Down Expand Up @@ -352,6 +354,28 @@ void parseRaw_leafSpiffeIdWithRootOnlyPath_isRejected() throws Exception {
assertEquals("Path cannot have a trailing slash", exception.getMessage());
}

@Test
void parseRaw_leafWithMultipleUriSans_isRejected() throws Exception {
CertAndKeyPair rootCa = createRootCA("C = US, O = SPIFFE", "spiffe://example.org");
CertAndKeyPair leaf = createCertificateWithUriSans(
"C = US, O = SPIRE",
"C = US, O = SPIFFE",
Arrays.asList("spiffe://example.org/workload", "https://example.org/workload"),
rootCa,
false
);

byte[] certBytes = leaf.getCertificate().getEncoded();
byte[] keyBytes = leaf.getKeyPair().getPrivate().getEncoded();

X509SvidException exception = assertThrows(
X509SvidException.class,
() -> X509Svid.parseRaw(certBytes, keyBytes)
);

assertEquals("Leaf certificate must contain exactly one URI SAN", exception.getMessage());
}

@ParameterizedTest
@MethodSource("provideX509SvidScenarios")
void parseX509Svid(TestCase testCase) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

public class X509CertificateTestUtils {

Expand All @@ -53,7 +56,43 @@ public static CertAndKeyPair createCertificate(String subject, String issuerSubj
KeyPair certKeyPair = generateKeyPair();
PrivateKey issuerKey = issuer.keyPair.getPrivate();
JcaX509v3CertificateBuilder builder = getCertificateBuilder(certKeyPair, subject, issuerSubject);
addCertExtensions(builder, spiffeId, isCa);
addCertExtensions(builder, Collections.singletonList(spiffeId), isCa, true);
X509Certificate cert = getSignedX509Certificate(issuerKey, builder);
return new CertAndKeyPair(cert, certKeyPair);
}

/**
* Creates a certificate with a custom list of URI SAN values.
*/
public static CertAndKeyPair createCertificateWithUriSans(
String subject,
String issuerSubject,
List<String> uriSans,
CertAndKeyPair issuer,
boolean isCa
) throws Exception {
KeyPair certKeyPair = generateKeyPair();
PrivateKey issuerKey = issuer.keyPair.getPrivate();
JcaX509v3CertificateBuilder builder = getCertificateBuilder(certKeyPair, subject, issuerSubject);
addCertExtensions(builder, uriSans, isCa, true);
X509Certificate cert = getSignedX509Certificate(issuerKey, builder);
return new CertAndKeyPair(cert, certKeyPair);
}

/**
* Creates a certificate without a KeyUsage extension.
*/
public static CertAndKeyPair createCertificateWithoutKeyUsage(
String subject,
String issuerSubject,
String spiffeId,
CertAndKeyPair issuer,
boolean isCa
) throws Exception {
KeyPair certKeyPair = generateKeyPair();
PrivateKey issuerKey = issuer.keyPair.getPrivate();
JcaX509v3CertificateBuilder builder = getCertificateBuilder(certKeyPair, subject, issuerSubject);
addCertExtensions(builder, Collections.singletonList(spiffeId), isCa, false);
X509Certificate cert = getSignedX509Certificate(issuerKey, builder);
return new CertAndKeyPair(cert, certKeyPair);
}
Expand All @@ -63,25 +102,41 @@ private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
return keyGen.generateKeyPair();
}

private static void addCertExtensions(JcaX509v3CertificateBuilder builder, String spiffeId, boolean isCa) throws CertIOException {
private static void addCertExtensions(
JcaX509v3CertificateBuilder builder,
List<String> uriSans,
boolean isCa,
boolean includeKeyUsage
) throws CertIOException {
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(isCa));

if (isCa) {
KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.digitalSignature | KeyUsage.cRLSign);
builder.addExtension(Extension.keyUsage, true, usage);
if (includeKeyUsage) {
KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.digitalSignature | KeyUsage.cRLSign);
builder.addExtension(Extension.keyUsage, true, usage);
}
} else {
KeyUsage usage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.keyAgreement);
builder.addExtension(Extension.keyUsage, true, usage);
if (includeKeyUsage) {
KeyUsage usage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.keyAgreement);
builder.addExtension(Extension.keyUsage, true, usage);
}

ASN1EncodableVector purposes = new ASN1EncodableVector();
purposes.add(KeyPurposeId.id_kp_serverAuth);
purposes.add(KeyPurposeId.id_kp_clientAuth);
builder.addExtension(Extension.extendedKeyUsage, false, new DERSequence(purposes));
}

if (StringUtils.isNotBlank(spiffeId)) {
List<GeneralName> uriGeneralNames = new ArrayList<>();
for (String uriSan : uriSans) {
if (StringUtils.isNotBlank(uriSan)) {
uriGeneralNames.add(new GeneralName(GeneralName.uniformResourceIdentifier, uriSan));
}
}

if (!uriGeneralNames.isEmpty()) {
builder.addExtension(Extension.subjectAlternativeName, false,
new GeneralNames(new GeneralName(GeneralName.uniformResourceIdentifier, spiffeId )));
new GeneralNames(uriGeneralNames.toArray(new GeneralName[0])));
}
}

Expand Down