diff --git a/.gitignore b/.gitignore index b30a8d8b..3eb41ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ .gradle .idea +.vscode build out +bin +**/bin/ +*.class + +# macOS +.DS_Store diff --git a/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509Svid.java b/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509Svid.java index f79c1750..5c349af9 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509Svid.java +++ b/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509Svid.java @@ -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; @@ -28,8 +27,6 @@ */ public class X509Svid { - private static final int URI_SAN_TYPE = 6; - SpiffeId spiffeId; /** @@ -242,7 +239,7 @@ private static SpiffeId getSpiffeId(final List 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); @@ -250,29 +247,6 @@ private static SpiffeId getSpiffeId(final List x509Certificates return spiffeId; } - private static void validateLeafHasSingleUriSan(final X509Certificate leaf) - throws CertificateException, X509SvidException { - final Collection> 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 x509Certificates) @@ -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); } } diff --git a/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509SvidProfile.java b/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509SvidProfile.java new file mode 100644 index 00000000..c4a909ec --- /dev/null +++ b/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509SvidProfile.java @@ -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> 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"); + } + } +} diff --git a/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509SvidValidator.java b/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509SvidValidator.java index d1aebb28..c7a8e49b 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509SvidValidator.java +++ b/java-spiffe-core/src/main/java/io/spiffe/svid/x509svid/X509SvidValidator.java @@ -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; @@ -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); diff --git a/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/X509CertificateTestUtils.java b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/X509CertificateTestUtils.java index 39206609..81ebabb1 100644 --- a/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/X509CertificateTestUtils.java +++ b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/X509CertificateTestUtils.java @@ -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(); @@ -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); diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerTest.java index e13ddacd..7ed747b9 100644 --- a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerTest.java +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerTest.java @@ -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; @@ -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); @@ -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