From e9df15e44bbfebef6b94540a577cb9c20d6c5aaf Mon Sep 17 00:00:00 2001 From: Max Lambrecht Date: Thu, 16 Jul 2020 15:08:09 -0300 Subject: [PATCH] Refactoring to improve testability. Add X509Source interface. Add tests to cover provider module. Signed-off-by: Max Lambrecht --- build.gradle | 8 +- java-spiffe-core/README.md | 4 +- java-spiffe-core/build.gradle | 7 +- .../internal/GrpcManagedChannelFactory.java | 1 - .../internal/GrpcManagedChannelFactory.java | 1 - .../spiffe/bundle/x509bundle/X509Bundle.java | 36 +- .../spiffe/exception/X509BundleException.java | 15 + .../internal/AsymmetricKeyAlgorithm.java | 2 +- .../io/spiffe/internal/CertificateUtils.java | 19 +- .../spiffe/workloadapi/DefaultX509Source.java | 270 +++++++++++ .../workloadapi/GrpcConversionUtils.java | 5 +- .../io/spiffe/workloadapi/X509Source.java | 261 +---------- .../workloadapi/internal/ThreadUtils.java | 1 - .../bundle/x509bundle/X509BundleTest.java | 25 +- .../internal/AsymmetricKeyAlgorithmTest.java | 29 ++ .../spiffe/internal/CertificateUtilsTest.java | 64 ++- .../svid/x509svid/X509SvidValidatorTest.java | 2 +- .../io/spiffe/workloadapi/AddressTest.java | 5 +- ...ceTest.java => DefaultX509SourceTest.java} | 27 +- .../workloadapi/GrpcConversionUtilsTest.java | 15 - .../workloadapi/WorkloadApiClientStub.java | 4 +- .../java/io/spiffe/utils/CertAndKeyPair.java | 22 + .../java/io/spiffe/utils/TestUtils.java | 1 - .../utils/X509CertificateTestUtils.java | 12 - java-spiffe-helper/build.gradle | 2 + .../java/io/spiffe/helper/cli/ConfigTest.java | 2 +- .../java/io/spiffe/helper/cli/RunnerTest.java | 2 +- .../helper/keystore/KeyStoreHelperTest.java | 4 +- .../spiffe/helper/keystore/KeyStoreTest.java | 5 +- .../keystore/WorkloadApiClientStub.java | 4 +- .../io/spiffe/helper/utils/TestUtils.java | 53 --- java-spiffe-provider/build.gradle | 2 + .../io/spiffe/provider/EnvironmentUtils.java | 5 +- .../provider/SpiffeKeyManagerFactory.java | 12 +- .../provider/SpiffeSslContextFactory.java | 9 +- .../provider/SpiffeSslSocketFactory.java | 6 +- .../provider/SpiffeTrustManagerFactory.java | 8 +- .../io/spiffe/provider/X509SourceManager.java | 9 +- .../SpiffeProviderException.java | 2 +- .../spiffe/provider/EnvironmentUtilsTest.java | 44 ++ .../provider/SpiffeKeyManagerFactoryTest.java | 67 +++ .../spiffe/provider/SpiffeKeyManagerTest.java | 102 ++++- .../spiffe/provider/SpiffeKeyStoreTest.java | 109 +++++ .../spiffe/provider/SpiffeProviderTest.java | 24 + .../provider/SpiffeSslContextFactoryTest.java | 86 ++++ .../provider/SpiffeSslSocketFactoryTest.java | 118 +++++ .../SpiffeTrustManagerFactoryTest.java | 130 ++++++ .../provider/SpiffeTrustManagerTest.java | 422 ++++++++++++++---- .../provider/X509SourceManagerTest.java | 34 ++ .../io/spiffe/provider/X509SourceStub.java | 52 +++ .../provider/examples/mtls/HttpsClient.java | 6 +- .../provider/examples/mtls/HttpsServer.java | 2 +- 52 files changed, 1616 insertions(+), 541 deletions(-) create mode 100644 java-spiffe-core/src/main/java/io/spiffe/exception/X509BundleException.java create mode 100644 java-spiffe-core/src/main/java/io/spiffe/workloadapi/DefaultX509Source.java create mode 100644 java-spiffe-core/src/test/java/io/spiffe/internal/AsymmetricKeyAlgorithmTest.java rename java-spiffe-core/src/test/java/io/spiffe/workloadapi/{X509SourceTest.java => DefaultX509SourceTest.java} (86%) create mode 100644 java-spiffe-core/src/testFixtures/java/io/spiffe/utils/CertAndKeyPair.java rename java-spiffe-core/src/{test => testFixtures}/java/io/spiffe/utils/TestUtils.java (98%) rename java-spiffe-core/src/{test => testFixtures}/java/io/spiffe/utils/X509CertificateTestUtils.java (94%) delete mode 100644 java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java rename java-spiffe-provider/src/main/java/io/spiffe/provider/{ => exception}/SpiffeProviderException.java (90%) create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/EnvironmentUtilsTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerFactoryTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyStoreTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeProviderTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslContextFactoryTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslSocketFactoryTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerFactoryTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceManagerTest.java create mode 100644 java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceStub.java diff --git a/build.gradle b/build.gradle index fc90455..dfb19dc 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,8 @@ subprojects { jupiterVersion = '5.6.2' mockitoVersion = '3.3.3' lombokVersion = '1.18.12' + nimbusVersion = '8.19' + if (gradle.ext.isMacOs) { osClassifier = "osx-x86_64" } else { @@ -87,7 +89,7 @@ task jacocoTestReport(type: JacocoReport) { // Filter out autogenerated or internal code afterEvaluate { classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: ['**/grpc/**', '**/exception/**', '**/provider/**', '**/internal/**']) + fileTree(dir: it, exclude: ['**/grpc/**', '**/exception/**', '**/internal/**']) })) } @@ -99,7 +101,9 @@ task jacocoTestReport(type: JacocoReport) { coveralls { jacocoReportPath 'build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml' - sourceDirs = ['java-spiffe-core/src/main/java', 'java-spiffe-helper/src/main/java'] + sourceDirs = ['java-spiffe-core/src/main/java', + 'java-spiffe-helper/src/main/java', + 'java-spiffe-provider/src/main/java'] } // always run the tests before generating the report diff --git a/java-spiffe-core/README.md b/java-spiffe-core/README.md index 851acdf..a5df447 100644 --- a/java-spiffe-core/README.md +++ b/java-spiffe-core/README.md @@ -14,7 +14,7 @@ To create a new X.509 Source: ``` X509Source x509Source; try { - x509Source = X509Source.newSource(); + x509Source = DefaultX509Source.newSource(); } catch (SocketEndpointAddressException | X509SourceException e) { // handle exception } @@ -38,7 +38,7 @@ configure it is by providing an `X509SourceOptions` instance to the `newSource` .picker(list -> list.get(list.size()-1)) .build(); - X509Source x509Source = X509Source.newSource(x509SourceOptions); + X509Source x509Source = DefaultX509Source.newSource(x509SourceOptions); ``` It allows to configure another SVID picker. By default, the first SVID is used. diff --git a/java-spiffe-core/build.gradle b/java-spiffe-core/build.gradle index 467bc3b..ba8d87c 100644 --- a/java-spiffe-core/build.gradle +++ b/java-spiffe-core/build.gradle @@ -9,6 +9,7 @@ buildscript { } apply plugin: 'com.google.protobuf' +apply plugin: 'java-test-fixtures' sourceSets { main { @@ -48,9 +49,11 @@ dependencies { compileOnly group: 'org.apache.tomcat', name:'annotations-api', version: '6.0.53' // necessary for Java 9+ // library for processing JWT tokens and JOSE JWK bundles - implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '8.19' + implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: "${nimbusVersion}" + testFixturesImplementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: "${nimbusVersion}" // using bouncy castle for generating X.509 certs for testing purposes - testImplementation group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.65' + testFixturesImplementation group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.65' + testFixturesImplementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.10' } diff --git a/java-spiffe-core/grpc-netty-linux/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java b/java-spiffe-core/grpc-netty-linux/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java index 5bf7855..e47a446 100644 --- a/java-spiffe-core/grpc-netty-linux/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java +++ b/java-spiffe-core/grpc-netty-linux/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java @@ -24,7 +24,6 @@ public final class GrpcManagedChannelFactory { private static final String TCP_SCHEME = "tcp"; private GrpcManagedChannelFactory() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } /** diff --git a/java-spiffe-core/grpc-netty-macos/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java b/java-spiffe-core/grpc-netty-macos/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java index 6093737..1b03357 100644 --- a/java-spiffe-core/grpc-netty-macos/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java +++ b/java-spiffe-core/grpc-netty-macos/src/main/java/io/spiffe/workloadapi/internal/GrpcManagedChannelFactory.java @@ -24,7 +24,6 @@ public final class GrpcManagedChannelFactory { private static final String TCP_SCHEME = "tcp"; private GrpcManagedChannelFactory() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } /** diff --git a/java-spiffe-core/src/main/java/io/spiffe/bundle/x509bundle/X509Bundle.java b/java-spiffe-core/src/main/java/io/spiffe/bundle/x509bundle/X509Bundle.java index 2072174..b73fb0b 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/bundle/x509bundle/X509Bundle.java +++ b/java-spiffe-core/src/main/java/io/spiffe/bundle/x509bundle/X509Bundle.java @@ -2,6 +2,7 @@ package io.spiffe.bundle.x509bundle; import io.spiffe.bundle.BundleSource; import io.spiffe.exception.BundleNotFoundException; +import io.spiffe.exception.X509BundleException; import io.spiffe.internal.CertificateUtils; import io.spiffe.spiffeid.TrustDomain; import lombok.NonNull; @@ -10,12 +11,12 @@ import lombok.val; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -58,20 +59,17 @@ public class X509Bundle implements BundleSource { * @return an instance of {@link X509Bundle} with the X.509 authorities * associated to the trust domain. * - * @throws IOException in case of failure accessing the given bundle path - * @throws CertificateException if the bundle cannot be parsed + * @throws X509BundleException in case of failure accessing the given bundle path or the bundle cannot be parsed */ - public static X509Bundle load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath) - throws IOException, CertificateException { - + public static X509Bundle load(@NonNull final TrustDomain trustDomain, @NonNull final Path bundlePath) throws X509BundleException { final byte[] bundleBytes; try { bundleBytes = Files.readAllBytes(bundlePath); - } catch (NoSuchFileException e) { - throw new IOException("Unable to load X.509 bundle file", e); + } catch (IOException e) { + throw new X509BundleException("Unable to load X.509 bundle file", e); } - val x509Certificates = CertificateUtils.generateCertificates(bundleBytes); + val x509Certificates = generateX509Certificates(bundleBytes); val x509CertificateSet = new HashSet<>(x509Certificates); return new X509Bundle(trustDomain, x509CertificateSet); } @@ -85,11 +83,10 @@ public class X509Bundle implements BundleSource { * @return an instance of {@link X509Bundle} with the X.509 authorities * associated to the given trust domain * - * @throws CertificateException if the bundle cannot be parsed + * @throws X509BundleException if the bundle cannot be parsed */ - public static X509Bundle parse(@NonNull final TrustDomain trustDomain, @NonNull final byte[] bundleBytes) - throws CertificateException { - val x509Certificates = CertificateUtils.generateCertificates(bundleBytes); + public static X509Bundle parse(@NonNull final TrustDomain trustDomain, @NonNull final byte[] bundleBytes) throws X509BundleException { + val x509Certificates = generateX509Certificates(bundleBytes); val x509CertificateSet = new HashSet<>(x509Certificates); return new X509Bundle(trustDomain, x509CertificateSet); } @@ -146,4 +143,15 @@ public class X509Bundle implements BundleSource { public void removeX509Authority(@NonNull final X509Certificate x509Authority) { x509Authorities.remove(x509Authority); } + + private static List generateX509Certificates(byte[] bundleBytes) throws X509BundleException { + List x509Certificates; + try { + x509Certificates = CertificateUtils.generateCertificates(bundleBytes); + } catch (CertificateParsingException e) { + throw new X509BundleException("Bundle certificates could not be parsed from bundle path", e); + } + return x509Certificates; + } + } diff --git a/java-spiffe-core/src/main/java/io/spiffe/exception/X509BundleException.java b/java-spiffe-core/src/main/java/io/spiffe/exception/X509BundleException.java new file mode 100644 index 0000000..b70d4f6 --- /dev/null +++ b/java-spiffe-core/src/main/java/io/spiffe/exception/X509BundleException.java @@ -0,0 +1,15 @@ +package io.spiffe.exception; + +/** + * Checked exception thrown when there is an error parsing + * the components of an X.509 Bundle. + */ +public class X509BundleException extends Exception { + public X509BundleException(final String message) { + super(message); + } + + public X509BundleException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/java-spiffe-core/src/main/java/io/spiffe/internal/AsymmetricKeyAlgorithm.java b/java-spiffe-core/src/main/java/io/spiffe/internal/AsymmetricKeyAlgorithm.java index adfbc44..ab275a7 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/internal/AsymmetricKeyAlgorithm.java +++ b/java-spiffe-core/src/main/java/io/spiffe/internal/AsymmetricKeyAlgorithm.java @@ -21,7 +21,7 @@ public enum AsymmetricKeyAlgorithm { } else if ("EC".equalsIgnoreCase(a)) { return EC; } else { - throw new IllegalArgumentException(String.format("Algorithm not recognized: %s", a)); + throw new IllegalArgumentException(String.format("Algorithm not supported: %s", a)); } } } diff --git a/java-spiffe-core/src/main/java/io/spiffe/internal/CertificateUtils.java b/java-spiffe-core/src/main/java/io/spiffe/internal/CertificateUtils.java index 03644ce..b3d032e 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/internal/CertificateUtils.java +++ b/java-spiffe-core/src/main/java/io/spiffe/internal/CertificateUtils.java @@ -2,7 +2,6 @@ package io.spiffe.internal; import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.TrustDomain; -import lombok.NonNull; import lombok.val; import java.io.ByteArrayInputStream; @@ -33,11 +32,11 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import static io.spiffe.internal.AsymmetricKeyAlgorithm.EC; +import static io.spiffe.internal.AsymmetricKeyAlgorithm.RSA; import static io.spiffe.internal.KeyUsage.CRL_SIGN; import static io.spiffe.internal.KeyUsage.DIGITAL_SIGNATURE; import static io.spiffe.internal.KeyUsage.KEY_CERT_SIGN; -import static io.spiffe.internal.AsymmetricKeyAlgorithm.EC; -import static io.spiffe.internal.AsymmetricKeyAlgorithm.RSA; import static org.apache.commons.lang3.StringUtils.startsWith; /** @@ -55,7 +54,6 @@ public class CertificateUtils { private static final String X509_CERTIFICATE_TYPE = "X.509"; private CertificateUtils() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } /** @@ -64,7 +62,7 @@ public class CertificateUtils { * @param input as byte array representing a list of X.509 certificates, as a DER or PEM * @return a List of {@link X509Certificate} */ - public static List generateCertificates(@NonNull final byte[] input) throws CertificateParsingException { + public static List generateCertificates(final byte[] input) throws CertificateParsingException { if (input.length == 0) { throw new CertificateParsingException("No certificates found"); } @@ -105,6 +103,9 @@ public class CertificateUtils { * @throws CertPathValidatorException */ public static void validate(final List chain, final Collection trustedCerts) throws CertificateException, CertPathValidatorException { + if (chain == null || chain.size() == 0) { + throw new IllegalArgumentException("Chain of certificates is empty"); + } val certificateFactory = getCertificateFactory(); PKIXParameters pkixParameters; try { @@ -168,9 +169,6 @@ public class CertificateUtils { break; case EC: verifyKeys(privateKey, x509Certificate.getPublicKey(), SHA_512_WITH_ECDSA); - break; - default: - throw new InvalidKeyException(String.format("Private Key algorithm not supported: %s", algorithm)); } } @@ -233,16 +231,13 @@ public class CertificateUtils { } private static PrivateKey generatePrivateKeyWithSpec(final EncodedKeySpec keySpec, AsymmetricKeyAlgorithm algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException { - PrivateKey privateKey; + PrivateKey privateKey = null; switch (algorithm) { case EC: privateKey = KeyFactory.getInstance(EC.value()).generatePrivate(keySpec); break; case RSA: privateKey = KeyFactory.getInstance(RSA.value()).generatePrivate(keySpec); - break; - default: - throw new NoSuchAlgorithmException(String.format("Private Key algorithm is not supported: %s", algorithm)); } return privateKey; } diff --git a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/DefaultX509Source.java b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/DefaultX509Source.java new file mode 100644 index 0000000..4448925 --- /dev/null +++ b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/DefaultX509Source.java @@ -0,0 +1,270 @@ +package io.spiffe.workloadapi; + +import io.spiffe.bundle.BundleSource; +import io.spiffe.bundle.x509bundle.X509Bundle; +import io.spiffe.bundle.x509bundle.X509BundleSet; +import io.spiffe.exception.BundleNotFoundException; +import io.spiffe.exception.SocketEndpointAddressException; +import io.spiffe.exception.WatcherException; +import io.spiffe.exception.X509SourceException; +import io.spiffe.spiffeid.TrustDomain; +import io.spiffe.svid.x509svid.X509Svid; +import io.spiffe.svid.x509svid.X509SvidSource; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.java.Log; +import lombok.val; + +import java.io.Closeable; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.logging.Level; + +import static io.spiffe.workloadapi.internal.ThreadUtils.await; + +/** + * Represents a source of X.509 SVIDs and X.509 bundles maintained via the Workload API. + *

+ * It handles a {@link X509Svid} and a {@link X509BundleSet} that are updated automatically + * whenever there is an update from the Workload API. + *

+ * Implements {@link X509SvidSource} and {@link BundleSource}. + *

+ * Implements the {@link Closeable} interface. The {@link #close()} method closes the source, + * dropping the connection to the Workload API. Other source methods will return an error + * after close has been called. + */ +@Log +public final class DefaultX509Source implements X509Source { + + private static final String TIMEOUT_SYSTEM_PROPERTY = "spiffe.newX509Source.timeout"; + private static final Duration DEFAULT_TIMEOUT = Duration.parse(System.getProperty(TIMEOUT_SYSTEM_PROPERTY, "PT0S")); + + private X509Svid svid; + private X509BundleSet bundles; + + private final Function, X509Svid> picker; + private final WorkloadApiClient workloadApiClient; + + private volatile boolean closed; + + // private constructor + private DefaultX509Source(final Function, X509Svid> svidPicker, final WorkloadApiClient workloadApiClient) { + this.picker = svidPicker; + this.workloadApiClient = workloadApiClient; + } + + /** + * Creates a new X.509 source. It blocks until the initial update with the X.509 materials + * has been received from the Workload API or until the timeout configured + * through the system property `spiffe.newX509Source.timeout` expires. + * If no timeout is configured, it blocks until it gets an X.509 update from the Workload API. + *

+ * It uses the default address socket endpoint from the environment variable to get the Workload API address. + *

+ * It uses the default X.509 SVID (picks the first SVID that comes in the Workload API response). + * + * @return an instance of {@link DefaultX509Source}, with the SVID and bundles initialized + * @throws SocketEndpointAddressException if the address to the Workload API is not valid + * @throws X509SourceException if the source could not be initialized + */ + public static DefaultX509Source newSource() throws SocketEndpointAddressException, X509SourceException { + val x509SourceOptions = X509SourceOptions.builder().initTimeout(DEFAULT_TIMEOUT).build(); + return newSource(x509SourceOptions); + } + + /** + * Creates a new X.509 source. It blocks until the initial update with the X.509 materials + * has been received from the Workload API, doing retries with a backoff exponential policy, + * or until the timeout has expired. + *

+ * If the timeout is not provided in the options, the default timeout is read from the + * system property `spiffe.newX509Source.timeout`. If none is configured, this method will + * block until the X.509 materials can be retrieved from the Workload API. + *

+ * The {@link WorkloadApiClient} can be provided in the options, if it is not, + * a new client is created. + *

+ * If no SVID Picker is provided in the options, it uses the default X.509 SVID (picks the first SVID that comes + * in the Workload API response). + * + * @param options {@link X509SourceOptions} + * @return an instance of {@link DefaultX509Source}, with the SVID and bundles initialized + * @throws SocketEndpointAddressException if the address to the Workload API is not valid + * @throws X509SourceException if the source could not be initialized + */ + public static DefaultX509Source newSource(@NonNull final X509SourceOptions options) + throws SocketEndpointAddressException, X509SourceException { + if (options.workloadApiClient == null) { + options.workloadApiClient = createClient(options); + } + + if (options.initTimeout == null) { + options.initTimeout = DEFAULT_TIMEOUT; + } + + val x509Source = new DefaultX509Source(options.svidPicker, options.workloadApiClient); + + try { + x509Source.init(options.initTimeout); + } catch (Exception e) { + x509Source.close(); + throw new X509SourceException("Error creating X.509 source", e); + } + + return x509Source; + } + + /** + * Returns the X.509 SVID handled by this source. + * + * @return a {@link X509Svid} + * @throws IllegalStateException if the source is closed + */ + @Override + public X509Svid getX509Svid() { + if (isClosed()) { + throw new IllegalStateException("X.509 SVID source is closed"); + } + return svid; + } + + /** + * Returns the X.509 bundle for a given trust domain. + * + * @return an instance of a {@link X509Bundle} + * + * @throws BundleNotFoundException is there is no bundle for the trust domain provided + * @throws IllegalStateException if the source is closed + */ + @Override + public X509Bundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException { + if (isClosed()) { + throw new IllegalStateException("X.509 bundle source is closed"); + } + return bundles.getBundleForTrustDomain(trustDomain); + } + + /** + * Closes this source, dropping the connection to the Workload API. + * Other source methods will return an error after close has been called. + *

+ * It is marked with {@link SneakyThrows} because it is not expected to throw + * the checked exception defined on the {@link Closeable} interface. + */ + @SneakyThrows + @Override + public void close() { + if (!closed) { + synchronized (this) { + if (!closed) { + workloadApiClient.close(); + closed = true; + } + } + } + } + + private static WorkloadApiClient createClient(final X509SourceOptions options) + throws SocketEndpointAddressException { + val clientOptions = DefaultWorkloadApiClient.ClientOptions + .builder() + .spiffeSocketPath(options.spiffeSocketPath) + .build(); + return DefaultWorkloadApiClient.newClient(clientOptions); + } + + private void init(final Duration timeout) throws TimeoutException { + val done = new CountDownLatch(1); + setX509ContextWatcher(done); + + final boolean success; + if (timeout.isZero()) { + await(done); + success = true; + } else { + success = await(done, timeout.getSeconds(), TimeUnit.SECONDS); + } + if (!success) { + throw new TimeoutException("Timeout waiting for X.509 Context update"); + } + } + + private void setX509ContextWatcher(final CountDownLatch done) { + workloadApiClient.watchX509Context(new Watcher() { + @Override + public void onUpdate(final X509Context update) { + log.log(Level.INFO, "Received X509Context update"); + setX509Context(update); + done.countDown(); + } + + @Override + public void onError(final Throwable error) { + log.log(Level.SEVERE, "Error in X509Context watcher", error); + done.countDown(); + throw new WatcherException("Error in X509Context watcher", error); + } + }); + } + + private void setX509Context(final X509Context update) { + final X509Svid svidUpdate; + if (picker == null) { + svidUpdate = update.getDefaultSvid(); + } else { + svidUpdate = picker.apply(update.getX509Svids()); + } + synchronized (this) { + this.svid = svidUpdate; + this.bundles = update.getX509BundleSet(); + } + } + + private boolean isClosed() { + synchronized (this) { + return closed; + } + } + + /** + * Options for creating a new {@link DefaultX509Source} + *

+ * spiffeSocketPath Address to the Workload API, if it is not set, the default address will be used. + *

+ * initTimeout Timeout for initializing the instance. If it is not defined, the timeout is read + * from the System property `spiffe.newX509Source.timeout'. If this is also not defined, no default timeout is applied. + *

+ * svidPicker Function to choose the X.509 SVID from the list returned by the Workload API. + * If it is not set, the default SVID is picked. + *

+ * workloadApiClient A custom instance of a {@link WorkloadApiClient}, if it is not set, a new client + * will be created. + */ + @Data + public static class X509SourceOptions { + + private String spiffeSocketPath; + private Duration initTimeout; + private Function, X509Svid> svidPicker; + private WorkloadApiClient workloadApiClient; + + @Builder + public X509SourceOptions(final String spiffeSocketPath, + final Duration initTimeout, + final Function, X509Svid> svidPicker, + final WorkloadApiClient workloadApiClient) { + this.spiffeSocketPath = spiffeSocketPath; + this.initTimeout = initTimeout; + this.svidPicker = svidPicker; + this.workloadApiClient = workloadApiClient; + } + } + +} diff --git a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/GrpcConversionUtils.java b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/GrpcConversionUtils.java index 15d151f..a94566d 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/GrpcConversionUtils.java +++ b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/GrpcConversionUtils.java @@ -6,6 +6,7 @@ import io.spiffe.bundle.jwtbundle.JwtBundleSet; import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.bundle.x509bundle.X509BundleSet; import io.spiffe.exception.JwtBundleException; +import io.spiffe.exception.X509BundleException; import io.spiffe.exception.X509ContextException; import io.spiffe.exception.X509SvidException; import io.spiffe.spiffeid.SpiffeId; @@ -14,7 +15,6 @@ import io.spiffe.svid.x509svid.X509Svid; import io.spiffe.workloadapi.grpc.Workload; import lombok.val; -import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -27,7 +27,6 @@ import java.util.Set; final class GrpcConversionUtils { private GrpcConversionUtils() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } static X509Context toX509Context(final Iterator x509SvidResponseIterator) throws X509ContextException { @@ -75,7 +74,7 @@ final class GrpcConversionUtils { static X509Bundle parseX509Bundle(TrustDomain trustDomain, byte[] bundleBytes) throws X509ContextException { try { return X509Bundle.parse(trustDomain, bundleBytes); - } catch (CertificateException e) { + } catch (X509BundleException e) { throw new X509ContextException("X.509 Bundles could not be processed", e); } } diff --git a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/X509Source.java b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/X509Source.java index 3cb050f..2db08a9 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/X509Source.java +++ b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/X509Source.java @@ -2,269 +2,12 @@ package io.spiffe.workloadapi; import io.spiffe.bundle.BundleSource; import io.spiffe.bundle.x509bundle.X509Bundle; -import io.spiffe.bundle.x509bundle.X509BundleSet; -import io.spiffe.exception.BundleNotFoundException; -import io.spiffe.exception.SocketEndpointAddressException; -import io.spiffe.exception.WatcherException; -import io.spiffe.exception.X509SourceException; -import io.spiffe.spiffeid.TrustDomain; -import io.spiffe.svid.x509svid.X509Svid; import io.spiffe.svid.x509svid.X509SvidSource; -import lombok.Builder; -import lombok.Data; -import lombok.NonNull; -import lombok.SneakyThrows; -import lombok.extern.java.Log; -import lombok.val; import java.io.Closeable; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; -import java.util.logging.Level; - -import static io.spiffe.workloadapi.internal.ThreadUtils.await; /** - * Represents a source of X.509 SVIDs and X.509 bundles maintained via the Workload API. - *

- * It handles a {@link X509Svid} and a {@link X509BundleSet} that are updated automatically - * whenever there is an update from the Workload API. - *

- * Implements {@link X509SvidSource} and {@link BundleSource}. - *

- * Implements the {@link Closeable} interface. The {@link #close()} method closes the source, - * dropping the connection to the Workload API. Other source methods will return an error - * after close has been called. + * Source of X.509 SVIDs and Bundles. */ -@Log -public final class X509Source implements X509SvidSource, BundleSource, Closeable { - - private static final String TIMEOUT_SYSTEM_PROPERTY = "spiffe.newX509Source.timeout"; - private static final Duration DEFAULT_TIMEOUT = Duration.parse(System.getProperty(TIMEOUT_SYSTEM_PROPERTY, "PT0S")); - - private X509Svid svid; - private X509BundleSet bundles; - - private final Function, X509Svid> picker; - private final WorkloadApiClient workloadApiClient; - - private volatile boolean closed; - - // private constructor - private X509Source(final Function, X509Svid> svidPicker, final WorkloadApiClient workloadApiClient) { - this.picker = svidPicker; - this.workloadApiClient = workloadApiClient; - } - - /** - * Creates a new X.509 source. It blocks until the initial update with the X.509 materials - * has been received from the Workload API or until the timeout configured - * through the system property `spiffe.newX509Source.timeout` expires. - * If no timeout is configured, it blocks until it gets an X.509 update from the Workload API. - *

- * It uses the default address socket endpoint from the environment variable to get the Workload API address. - *

- * It uses the default X.509 SVID (picks the first SVID that comes in the Workload API response). - * - * @return an instance of {@link X509Source}, with the SVID and bundles initialized - * @throws SocketEndpointAddressException if the address to the Workload API is not valid - * @throws X509SourceException if the source could not be initialized - */ - public static X509Source newSource() throws SocketEndpointAddressException, X509SourceException { - val x509SourceOptions = X509SourceOptions.builder().initTimeout(DEFAULT_TIMEOUT).build(); - return newSource(x509SourceOptions); - } - - /** - * Creates a new X.509 source. It blocks until the initial update with the X.509 materials - * has been received from the Workload API, doing retries with a backoff exponential policy, - * or until the timeout has expired. - *

- * If the timeout is not provided in the options, the default timeout is read from the - * system property `spiffe.newX509Source.timeout`. If none is configured, this method will - * block until the X.509 materials can be retrieved from the Workload API. - *

- * The {@link WorkloadApiClient} can be provided in the options, if it is not, - * a new client is created. - *

- * If no SVID Picker is provided in the options, it uses the default X.509 SVID (picks the first SVID that comes - * in the Workload API response). - * - * @param options {@link X509SourceOptions} - * @return an instance of {@link X509Source}, with the SVID and bundles initialized - * @throws SocketEndpointAddressException if the address to the Workload API is not valid - * @throws X509SourceException if the source could not be initialized - */ - public static X509Source newSource(@NonNull final X509SourceOptions options) - throws SocketEndpointAddressException, X509SourceException { - if (options.workloadApiClient == null) { - options.workloadApiClient = createClient(options); - } - - if (options.initTimeout == null) { - options.initTimeout = DEFAULT_TIMEOUT; - } - - val x509Source = new X509Source(options.svidPicker, options.workloadApiClient); - - try { - x509Source.init(options.initTimeout); - } catch (Exception e) { - x509Source.close(); - throw new X509SourceException("Error creating X.509 source", e); - } - - return x509Source; - } - - /** - * Returns the X.509 SVID handled by this source. - * - * @return a {@link X509Svid} - * @throws IllegalStateException if the source is closed - */ - @Override - public X509Svid getX509Svid() { - if (isClosed()) { - throw new IllegalStateException("X.509 SVID source is closed"); - } - return svid; - } - - /** - * Returns the X.509 bundle for a given trust domain. - * - * @return an instance of a {@link X509Bundle} - * - * @throws BundleNotFoundException is there is no bundle for the trust domain provided - * @throws IllegalStateException if the source is closed - */ - @Override - public X509Bundle getBundleForTrustDomain(@NonNull final TrustDomain trustDomain) throws BundleNotFoundException { - if (isClosed()) { - throw new IllegalStateException("X.509 bundle source is closed"); - } - return bundles.getBundleForTrustDomain(trustDomain); - } - - /** - * Closes this source, dropping the connection to the Workload API. - * Other source methods will return an error after close has been called. - *

- * It is marked with {@link SneakyThrows} because it is not expected to throw - * the checked exception defined on the {@link Closeable} interface. - */ - @SneakyThrows - @Override - public void close() { - if (!closed) { - synchronized (this) { - if (!closed) { - workloadApiClient.close(); - closed = true; - } - } - } - } - - private static WorkloadApiClient createClient(final X509SourceOptions options) - throws SocketEndpointAddressException { - val clientOptions = DefaultWorkloadApiClient.ClientOptions - .builder() - .spiffeSocketPath(options.spiffeSocketPath) - .build(); - return DefaultWorkloadApiClient.newClient(clientOptions); - } - - private void init(final Duration timeout) throws TimeoutException { - val done = new CountDownLatch(1); - setX509ContextWatcher(done); - - final boolean success; - if (timeout.isZero()) { - await(done); - success = true; - } else { - success = await(done, timeout.getSeconds(), TimeUnit.SECONDS); - } - if (!success) { - throw new TimeoutException("Timeout waiting for X.509 Context update"); - } - } - - private void setX509ContextWatcher(final CountDownLatch done) { - workloadApiClient.watchX509Context(new Watcher() { - @Override - public void onUpdate(final X509Context update) { - log.log(Level.INFO, "Received X509Context update"); - setX509Context(update); - done.countDown(); - } - - @Override - public void onError(final Throwable error) { - log.log(Level.SEVERE, "Error in X509Context watcher", error); - done.countDown(); - throw new WatcherException("Error in X509Context watcher", error); - } - }); - } - - private void setX509Context(final X509Context update) { - final X509Svid svidUpdate; - if (picker == null) { - svidUpdate = update.getDefaultSvid(); - } else { - svidUpdate = picker.apply(update.getX509Svids()); - } - synchronized (this) { - this.svid = svidUpdate; - this.bundles = update.getX509BundleSet(); - } - } - - private boolean isClosed() { - synchronized (this) { - return closed; - } - } - - /** - * Options for creating a new {@link X509Source} - *

- * spiffeSocketPath Address to the Workload API, if it is not set, the default address will be used. - *

- * initTimeout Timeout for initializing the instance. If it is not defined, the timeout is read - * from the System property `spiffe.newX509Source.timeout'. If this is also not defined, no default timeout is applied. - *

- * svidPicker Function to choose the X.509 SVID from the list returned by the Workload API. - * If it is not set, the default SVID is picked. - *

- * workloadApiClient A custom instance of a {@link WorkloadApiClient}, if it is not set, a new client - * will be created. - */ - @Data - public static class X509SourceOptions { - - private String spiffeSocketPath; - private Duration initTimeout; - private Function, X509Svid> svidPicker; - private WorkloadApiClient workloadApiClient; - - @Builder - public X509SourceOptions(final String spiffeSocketPath, - final Duration initTimeout, - final Function, X509Svid> svidPicker, - final WorkloadApiClient workloadApiClient) { - this.spiffeSocketPath = spiffeSocketPath; - this.initTimeout = initTimeout; - this.svidPicker = svidPicker; - this.workloadApiClient = workloadApiClient; - } - } - +public interface X509Source extends X509SvidSource, BundleSource, Closeable { } diff --git a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/internal/ThreadUtils.java b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/internal/ThreadUtils.java index 8123f0d..10c12d7 100644 --- a/java-spiffe-core/src/main/java/io/spiffe/workloadapi/internal/ThreadUtils.java +++ b/java-spiffe-core/src/main/java/io/spiffe/workloadapi/internal/ThreadUtils.java @@ -6,7 +6,6 @@ import java.util.concurrent.TimeUnit; public final class ThreadUtils { private ThreadUtils() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } public static void await(CountDownLatch latch) { diff --git a/java-spiffe-core/src/test/java/io/spiffe/bundle/x509bundle/X509BundleTest.java b/java-spiffe-core/src/test/java/io/spiffe/bundle/x509bundle/X509BundleTest.java index da06708..88940b3 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/bundle/x509bundle/X509BundleTest.java +++ b/java-spiffe-core/src/test/java/io/spiffe/bundle/x509bundle/X509BundleTest.java @@ -1,6 +1,7 @@ package io.spiffe.bundle.x509bundle; import io.spiffe.exception.BundleNotFoundException; +import io.spiffe.exception.X509BundleException; import io.spiffe.internal.DummyX509Certificate; import io.spiffe.spiffeid.TrustDomain; import lombok.Builder; @@ -11,12 +12,10 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.platform.commons.util.StringUtils; -import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashSet; import java.util.stream.Stream; @@ -116,7 +115,7 @@ public class X509BundleTest { try { X509Bundle x509Bundle = X509Bundle.load(TrustDomain.of("example.org"), Paths.get(toUri("testdata/x509bundle/certs.pem"))); assertEquals(2, x509Bundle.getX509Authorities().size()); - } catch (IOException | CertificateException | URISyntaxException e) { + } catch (URISyntaxException | X509BundleException e) { fail(e); } } @@ -126,13 +125,13 @@ public class X509BundleTest { try { X509Bundle.load(TrustDomain.of("example.org"), Paths.get("testdata/x509bundle/non-existent.pem")); fail("should have thrown exception"); - } catch (IOException | CertificateException e) { + } catch (X509BundleException e) { assertEquals("Unable to load X.509 bundle file", e.getMessage()); } } @Test - void testLoad_nullTrustDomain_throwsNullPointerException() throws IOException, CertificateException { + void testLoad_nullTrustDomain_throwsNullPointerException() throws X509BundleException { try { X509Bundle.load(null,Paths.get("testdata/x509bundle/non-existent.pem")); fail("should have thrown exception"); @@ -142,7 +141,7 @@ public class X509BundleTest { } @Test - void testLoad_nullBundlePath_throwsNullPointerException() throws IOException, CertificateException { + void testLoad_nullBundlePath_throwsNullPointerException() throws X509BundleException { try { X509Bundle.load(TrustDomain.of("example.org"), null); fail("should have thrown exception"); @@ -152,7 +151,7 @@ public class X509BundleTest { } @Test - void testParse_nullTrustDomain_throwsNullPointerException() throws IOException, CertificateException { + void testParse_nullTrustDomain_throwsNullPointerException() throws X509BundleException { try { X509Bundle.parse(null, "bytes".getBytes()); fail("should have thrown exception"); @@ -162,7 +161,7 @@ public class X509BundleTest { } @Test - void testParse_nullBundlePath_throwsNullPointerException() throws IOException, CertificateException { + void testParse_nullBundlePath_throwsNullPointerException() throws X509BundleException { try { X509Bundle.parse(TrustDomain.of("example.org"), null); fail("should have thrown exception"); @@ -215,7 +214,7 @@ public class X509BundleTest { // Load bundle2, which contains 2 certificates // The first certificate is the same than the one used in bundle1 bundle2 = X509Bundle.load(TrustDomain.of("example.org"), Paths.get(toUri("testdata/x509bundle/certs.pem"))); - } catch (IOException | CertificateException | URISyntaxException e) { + } catch (URISyntaxException | X509BundleException e) { fail(e); } @@ -295,7 +294,7 @@ public class X509BundleTest { .name("Parse empty bytes should fail") .path("testdata/x509bundle/empty.pem") .trustDomain(TrustDomain.of("example.org")) - .expectedError("No certificates found") + .expectedError("Bundle certificates could not be parsed from bundle path") .build() ), Arguments.of(TestCase @@ -303,7 +302,7 @@ public class X509BundleTest { .name("Parse non-PEM bytes should fail") .path("testdata/x509bundle/not-pem.pem") .trustDomain(TrustDomain.of("example.org")) - .expectedError("Certificate could not be parsed from cert bytes") + .expectedError("Bundle certificates could not be parsed from bundle path") .build() ), Arguments.of(TestCase @@ -311,7 +310,7 @@ public class X509BundleTest { .name("Parse should fail if no certificate block is is found") .path("testdata/x509bundle/key.pem") .trustDomain(TrustDomain.of("example.org")) - .expectedError("Certificate could not be parsed from cert bytes") + .expectedError("Bundle certificates could not be parsed from bundle path") .build() ), Arguments.of(TestCase @@ -319,7 +318,7 @@ public class X509BundleTest { .name("Parse a corrupted certificate should fail") .path("testdata/x509bundle/corrupted.pem") .trustDomain(TrustDomain.of("example.org")) - .expectedError("Certificate could not be parsed from cert bytes") + .expectedError("Bundle certificates could not be parsed from bundle path") .build() ) ); diff --git a/java-spiffe-core/src/test/java/io/spiffe/internal/AsymmetricKeyAlgorithmTest.java b/java-spiffe-core/src/test/java/io/spiffe/internal/AsymmetricKeyAlgorithmTest.java new file mode 100644 index 0000000..6848c14 --- /dev/null +++ b/java-spiffe-core/src/test/java/io/spiffe/internal/AsymmetricKeyAlgorithmTest.java @@ -0,0 +1,29 @@ +package io.spiffe.internal; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AsymmetricKeyAlgorithmTest { + + @Test + void parseRSA() { + AsymmetricKeyAlgorithm algorithm = AsymmetricKeyAlgorithm.parse("RSA"); + assertEquals(AsymmetricKeyAlgorithm.RSA, algorithm); + } + + @Test + void parseEC() { + AsymmetricKeyAlgorithm algorithm = AsymmetricKeyAlgorithm.parse("EC"); + assertEquals(AsymmetricKeyAlgorithm.EC, algorithm); + } + + @Test + void parseUnknown() { + try { + AsymmetricKeyAlgorithm.parse("unknown"); + } catch (IllegalArgumentException e) { + assertEquals("Algorithm not supported: unknown", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/java-spiffe-core/src/test/java/io/spiffe/internal/CertificateUtilsTest.java b/java-spiffe-core/src/test/java/io/spiffe/internal/CertificateUtilsTest.java index c4088db..2226ee3 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/internal/CertificateUtilsTest.java +++ b/java-spiffe-core/src/test/java/io/spiffe/internal/CertificateUtilsTest.java @@ -17,9 +17,11 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static io.spiffe.internal.AsymmetricKeyAlgorithm.RSA; @@ -69,7 +71,67 @@ public class CertificateUtilsTest { } @Test - void testGenerateiRsaPrivateKeyFromBytes() throws URISyntaxException, IOException { + void validateCerts_nullTrustedCerts() throws URISyntaxException, IOException, CertificateParsingException { + val certPath = Paths.get(toUri("testdata/internal/cert2.pem")); + val certBytes = Files.readAllBytes(certPath); + val chain = CertificateUtils.generateCertificates(certBytes); + + try { + CertificateUtils.validate(chain, null); + } catch (CertificateException e) { + assertEquals("No trusted Certs", e.getMessage()); + } catch (CertPathValidatorException e) { + fail(e); + } + } + + @Test + void validateCerts_emptyTrustedCerts() throws URISyntaxException, IOException, CertificateParsingException { + val certPath = Paths.get(toUri("testdata/internal/cert2.pem")); + val certBytes = Files.readAllBytes(certPath); + val chain = CertificateUtils.generateCertificates(certBytes); + + try { + CertificateUtils.validate(chain, Collections.emptyList()); + } catch (CertificateException e) { + assertEquals("No trusted Certs", e.getMessage()); + } catch (CertPathValidatorException e) { + fail(e); + } + } + + @Test + void validateCerts_nullChain() throws URISyntaxException, IOException, CertificateParsingException { + val certPath = Paths.get(toUri("testdata/internal/cert2.pem")); + val certBytes = Files.readAllBytes(certPath); + val certificates = CertificateUtils.generateCertificates(certBytes); + + try { + CertificateUtils.validate(null, certificates); + } catch (CertificateException | CertPathValidatorException e) { + fail(e); + } catch (IllegalArgumentException e) { + assertEquals("Chain of certificates is empty", e.getMessage()); + } + } + + @Test + void validateCerts_emptyChain() throws URISyntaxException, IOException, CertificateParsingException { + val certPath = Paths.get(toUri("testdata/internal/cert2.pem")); + val certBytes = Files.readAllBytes(certPath); + val certificates = CertificateUtils.generateCertificates(certBytes); + + try { + CertificateUtils.validate(Collections.emptyList(), certificates); + } catch (CertificateException | CertPathValidatorException e) { + fail(e); + } catch (IllegalArgumentException e) { + assertEquals("Chain of certificates is empty", e.getMessage()); + } + } + + @Test + void testGenerateRsaPrivateKeyFromBytes() throws URISyntaxException, IOException { val keyPath = Paths.get(toUri("testdata/internal/privateKeyRsa.pem")); val keyBytes = Files.readAllBytes(keyPath); diff --git a/java-spiffe-core/src/test/java/io/spiffe/svid/x509svid/X509SvidValidatorTest.java b/java-spiffe-core/src/test/java/io/spiffe/svid/x509svid/X509SvidValidatorTest.java index d70dce6..ecbc5f6 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/svid/x509svid/X509SvidValidatorTest.java +++ b/java-spiffe-core/src/test/java/io/spiffe/svid/x509svid/X509SvidValidatorTest.java @@ -5,7 +5,7 @@ import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.exception.BundleNotFoundException; import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.TrustDomain; -import io.spiffe.utils.X509CertificateTestUtils.CertAndKeyPair; +import io.spiffe.utils.CertAndKeyPair; import lombok.val; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/AddressTest.java b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/AddressTest.java index d149d9c..c860333 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/AddressTest.java +++ b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/AddressTest.java @@ -74,15 +74,14 @@ public class AddressTest { @Test void getDefaultAddress() throws Exception { - TestUtils.setEnvironmentVariable(Address.SOCKET_ENV_VARIABLE, "unix:/tmp/agent.sock" ); + TestUtils.setEnvironmentVariable(Address.SOCKET_ENV_VARIABLE, "unix:/tmp/test" ); String defaultAddress = Address.getDefaultAddress(); - assertEquals("unix:/tmp/agent.sock", defaultAddress); + assertEquals("unix:/tmp/test", defaultAddress); } @Test void getDefaultAddress_isBlankThrowsException() throws Exception { TestUtils.setEnvironmentVariable(Address.SOCKET_ENV_VARIABLE, ""); - String defaultAddress = null; try { Address.getDefaultAddress(); fail(); diff --git a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/X509SourceTest.java b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/DefaultX509SourceTest.java similarity index 86% rename from java-spiffe-core/src/test/java/io/spiffe/workloadapi/X509SourceTest.java rename to java-spiffe-core/src/test/java/io/spiffe/workloadapi/DefaultX509SourceTest.java index a559541..d29d7a1 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/X509SourceTest.java +++ b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/DefaultX509SourceTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.time.Duration; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -21,18 +20,18 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -class X509SourceTest { +class DefaultX509SourceTest { - private X509Source x509Source; + private DefaultX509Source x509Source; private WorkloadApiClientStub workloadApiClient; private WorkloadApiClientErrorStub workloadApiClientErrorStub; @BeforeEach - void setUp() throws IOException, X509SourceException, SocketEndpointAddressException { + void setUp() throws X509SourceException, SocketEndpointAddressException { workloadApiClient = new WorkloadApiClientStub(); - X509Source.X509SourceOptions options = X509Source.X509SourceOptions.builder().workloadApiClient(workloadApiClient).build(); + DefaultX509Source.X509SourceOptions options = DefaultX509Source.X509SourceOptions.builder().workloadApiClient(workloadApiClient).build(); System.setProperty(JwtSource.TIMEOUT_SYSTEM_PROPERTY, "PT1S"); - x509Source = X509Source.newSource(options); + x509Source = DefaultX509Source.newSource(options); workloadApiClientErrorStub = new WorkloadApiClientErrorStub(); } @@ -99,14 +98,14 @@ class X509SourceTest { @Test void newSource_success() { - val options = X509Source.X509SourceOptions + val options = DefaultX509Source.X509SourceOptions .builder() .workloadApiClient(workloadApiClient) .svidPicker((list) -> list.get(0)) .initTimeout(Duration.ofSeconds(0)) .build(); try { - X509Source jwtSource = X509Source.newSource(options); + DefaultX509Source jwtSource = DefaultX509Source.newSource(options); assertNotNull(jwtSource); } catch (SocketEndpointAddressException | X509SourceException e) { fail(e); @@ -116,7 +115,7 @@ class X509SourceTest { @Test void newSource_nullParam() { try { - X509Source.newSource(null); + DefaultX509Source.newSource(null); fail(); } catch (NullPointerException e) { assertEquals("options is marked non-null but is null", e.getMessage()); @@ -127,12 +126,12 @@ class X509SourceTest { @Test void newSource_timeout() throws Exception { try { - val options = X509Source.X509SourceOptions + val options = DefaultX509Source.X509SourceOptions .builder() .initTimeout(Duration.ofSeconds(1)) .spiffeSocketPath("unix:/tmp/test") .build(); - X509Source.newSource(options); + DefaultX509Source.newSource(options); fail(); } catch (X509SourceException e) { assertEquals("Error creating X.509 source", e.getMessage()); @@ -143,13 +142,13 @@ class X509SourceTest { @Test void newSource_errorFetchingJwtBundles() { - val options = X509Source.X509SourceOptions + val options = DefaultX509Source.X509SourceOptions .builder() .workloadApiClient(workloadApiClientErrorStub) .spiffeSocketPath("unix:/tmp/test") .build(); try { - X509Source.newSource(options); + DefaultX509Source.newSource(options); fail(); } catch (X509SourceException e) { assertEquals("Error creating X.509 source", e.getMessage()); @@ -164,7 +163,7 @@ class X509SourceTest { try { // just in case the variable is defined in the environment TestUtils.setEnvironmentVariable(Address.SOCKET_ENV_VARIABLE, ""); - X509Source.newSource(); + DefaultX509Source.newSource(); fail(); } catch (X509SourceException | SocketEndpointAddressException e) { fail(); diff --git a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/GrpcConversionUtilsTest.java b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/GrpcConversionUtilsTest.java index 5e37b4e..c25fcfa 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/GrpcConversionUtilsTest.java +++ b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/GrpcConversionUtilsTest.java @@ -4,15 +4,12 @@ import io.spiffe.exception.JwtBundleException; import io.spiffe.exception.X509ContextException; import io.spiffe.spiffeid.TrustDomain; import io.spiffe.workloadapi.grpc.Workload; -import lombok.val; import org.junit.jupiter.api.Test; -import java.lang.reflect.InvocationTargetException; import java.util.Collections; import java.util.Iterator; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; class GrpcConversionUtilsTest { @@ -44,16 +41,4 @@ class GrpcConversionUtilsTest { assertEquals("X.509 Bundles could not be processed", e.getMessage()); } } - - @Test - void testPrivateConstructor_InstanceCannotBeCreated() throws IllegalAccessException, InstantiationException { - val constructor = GrpcConversionUtils.class.getDeclaredConstructors()[0]; - constructor.setAccessible(true); - try { - constructor.newInstance(); - fail(); - } catch (InvocationTargetException e) { - assertEquals("This is a utility class and cannot be instantiated", e.getCause().getMessage()); - } - } } \ No newline at end of file diff --git a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/WorkloadApiClientStub.java b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/WorkloadApiClientStub.java index 081cd99..0f3e70f 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/workloadapi/WorkloadApiClientStub.java +++ b/java-spiffe-core/src/test/java/io/spiffe/workloadapi/WorkloadApiClientStub.java @@ -7,6 +7,7 @@ import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.bundle.x509bundle.X509BundleSet; import io.spiffe.exception.JwtBundleException; import io.spiffe.exception.JwtSvidException; +import io.spiffe.exception.X509BundleException; import io.spiffe.exception.X509SvidException; import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.TrustDomain; @@ -22,7 +23,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyPair; -import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -127,7 +127,7 @@ public class WorkloadApiClientStub implements WorkloadApiClient { Path pathBundle = Paths.get(toUri(x509Bundle)); byte[] bundleBytes = Files.readAllBytes(pathBundle); return X509Bundle.parse(TrustDomain.of("example.org"), bundleBytes); - } catch (IOException | CertificateException | URISyntaxException e) { + } catch (IOException | URISyntaxException | X509BundleException e) { throw new RuntimeException(e); } } diff --git a/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/CertAndKeyPair.java b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/CertAndKeyPair.java new file mode 100644 index 0000000..3bdacd2 --- /dev/null +++ b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/CertAndKeyPair.java @@ -0,0 +1,22 @@ +package io.spiffe.utils; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +public class CertAndKeyPair { + KeyPair keyPair; + X509Certificate certificate; + + public CertAndKeyPair(X509Certificate certificate, KeyPair keyPair) { + this.keyPair = keyPair; + this.certificate = certificate; + } + + public KeyPair getKeyPair() { + return keyPair; + } + + public X509Certificate getCertificate() { + return certificate; + } +} diff --git a/java-spiffe-core/src/test/java/io/spiffe/utils/TestUtils.java b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/TestUtils.java similarity index 98% rename from java-spiffe-core/src/test/java/io/spiffe/utils/TestUtils.java rename to java-spiffe-core/src/testFixtures/java/io/spiffe/utils/TestUtils.java index 60effbe..f78fa34 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/utils/TestUtils.java +++ b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/TestUtils.java @@ -33,7 +33,6 @@ import java.util.Set; public class TestUtils { private TestUtils() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } public static KeyPair generateECKeyPair(Curve curve) { diff --git a/java-spiffe-core/src/test/java/io/spiffe/utils/X509CertificateTestUtils.java b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/X509CertificateTestUtils.java similarity index 94% rename from java-spiffe-core/src/test/java/io/spiffe/utils/X509CertificateTestUtils.java rename to java-spiffe-core/src/testFixtures/java/io/spiffe/utils/X509CertificateTestUtils.java index ce356b7..6158a3c 100644 --- a/java-spiffe-core/src/test/java/io/spiffe/utils/X509CertificateTestUtils.java +++ b/java-spiffe-core/src/testFixtures/java/io/spiffe/utils/X509CertificateTestUtils.java @@ -1,6 +1,5 @@ package io.spiffe.utils; -import lombok.Value; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.DERSequence; @@ -59,17 +58,6 @@ public class X509CertificateTestUtils { return new CertAndKeyPair(cert, certKeyPair); } - @Value - public static class CertAndKeyPair { - KeyPair keyPair; - X509Certificate certificate; - - public CertAndKeyPair(X509Certificate certificate, KeyPair keyPair) { - this.keyPair = keyPair; - this.certificate = certificate; - } - } - private static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); return keyGen.generateKeyPair(); diff --git a/java-spiffe-helper/build.gradle b/java-spiffe-helper/build.gradle index b1c2637..1dab6f5 100644 --- a/java-spiffe-helper/build.gradle +++ b/java-spiffe-helper/build.gradle @@ -16,4 +16,6 @@ shadowJar { dependencies { api (project(':java-spiffe-core')) implementation group: 'commons-cli', name: 'commons-cli', version: '1.4' + + testImplementation(testFixtures(project(":java-spiffe-core"))) } diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/ConfigTest.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/ConfigTest.java index 3580d12..9c6ceee 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/ConfigTest.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/ConfigTest.java @@ -11,7 +11,7 @@ import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.Properties; -import static io.spiffe.helper.utils.TestUtils.toUri; +import static io.spiffe.utils.TestUtils.toUri; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java index 9be7ac3..b60b761 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/cli/RunnerTest.java @@ -10,7 +10,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; -import static io.spiffe.helper.utils.TestUtils.toUri; +import static io.spiffe.utils.TestUtils.toUri; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java index d85370a..310de1f 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreHelperTest.java @@ -2,9 +2,9 @@ package io.spiffe.helper.keystore; import io.spiffe.exception.SocketEndpointAddressException; import io.spiffe.helper.exception.KeyStoreHelperException; -import io.spiffe.helper.utils.TestUtils; import io.spiffe.internal.CertificateUtils; import io.spiffe.spiffeid.SpiffeId; +import io.spiffe.utils.TestUtils; import io.spiffe.workloadapi.Address; import io.spiffe.workloadapi.WorkloadApiClient; import lombok.SneakyThrows; @@ -80,7 +80,7 @@ class KeyStoreHelperTest { try (val keystoreHelper = KeyStoreHelper.create(options)) { keystoreHelper.run(false); } catch (KeyStoreHelperException e) { - fail(); + fail(e); } checkPrivateKeyEntry(keyStoreFilePath, keyStorePass, keyPass, keyStoreType, alias); diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java index 84c5149..35a0695 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/KeyStoreTest.java @@ -1,6 +1,7 @@ package io.spiffe.helper.keystore; import io.spiffe.bundle.x509bundle.X509Bundle; +import io.spiffe.exception.X509BundleException; import io.spiffe.exception.X509SvidException; import io.spiffe.internal.CertificateUtils; import io.spiffe.spiffeid.SpiffeId; @@ -25,7 +26,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import static io.spiffe.helper.utils.TestUtils.toUri; +import static io.spiffe.utils.TestUtils.toUri; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -41,7 +42,7 @@ public class KeyStoreTest { private Path keyStoreFilePath; @BeforeEach - void setup() throws X509SvidException, URISyntaxException, IOException, CertificateException { + void setup() throws X509SvidException, URISyntaxException, X509BundleException { x509Svid = X509Svid.load( Paths.get(toUri("testdata/svid.pem")), Paths.get(toUri("testdata/svid.key"))); diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientStub.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientStub.java index bb1a397..6e7a876 100644 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientStub.java +++ b/java-spiffe-helper/src/test/java/io/spiffe/helper/keystore/WorkloadApiClientStub.java @@ -5,6 +5,7 @@ import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.bundle.x509bundle.X509BundleSet; import io.spiffe.exception.JwtBundleException; import io.spiffe.exception.JwtSvidException; +import io.spiffe.exception.X509BundleException; import io.spiffe.exception.X509SvidException; import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.TrustDomain; @@ -22,7 +23,6 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.cert.CertificateException; import java.util.Collections; public class WorkloadApiClientStub implements WorkloadApiClient { @@ -83,7 +83,7 @@ public class WorkloadApiClientStub implements WorkloadApiClient { Path pathBundle = Paths.get(toUri(x509Bundle)); byte[] bundleBytes = Files.readAllBytes(pathBundle); return X509Bundle.parse(TrustDomain.of("example.org"), bundleBytes); - } catch (IOException | CertificateException e) { + } catch (IOException | X509BundleException e) { throw new RuntimeException(e); } } diff --git a/java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java b/java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java deleted file mode 100644 index 20529bc..0000000 --- a/java-spiffe-helper/src/test/java/io/spiffe/helper/utils/TestUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -package io.spiffe.helper.utils; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Map; - -/** - * Util methods for testing. - */ -public class TestUtils { - - private TestUtils() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); - } - - public static void setEnvironmentVariable(String variableName, String value) throws Exception { - Class processEnvironment = Class.forName("java.lang.ProcessEnvironment"); - - Field unmodifiableMapField = getField(processEnvironment, "theUnmodifiableEnvironment"); - Object unmodifiableMap = unmodifiableMapField.get(null); - injectIntoUnmodifiableMap(variableName, value, unmodifiableMap); - - Field mapField = getField(processEnvironment, "theEnvironment"); - Map map = (Map) mapField.get(null); - map.put(variableName, value); - } - - public static Object invokeMethod(Class clazz, String methodName, Object... args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - Method method = clazz.getDeclaredMethod(methodName); - method.setAccessible(true); - return method.invoke(args); - } - - public static Field getField(Class clazz, String fieldName) throws NoSuchFieldException { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return field; - } - - private static void injectIntoUnmodifiableMap(String key, String value, Object map) throws ReflectiveOperationException { - Class unmodifiableMap = Class.forName("java.util.Collections$UnmodifiableMap"); - Field field = getField(unmodifiableMap, "m"); - Object obj = field.get(map); - ((Map) obj).put(key, value); - } - - public static URI toUri(String path) throws URISyntaxException { - return Thread.currentThread().getContextClassLoader().getResource(path).toURI(); - } -} diff --git a/java-spiffe-provider/build.gradle b/java-spiffe-provider/build.gradle index 04a5283..6c46e47 100644 --- a/java-spiffe-provider/build.gradle +++ b/java-spiffe-provider/build.gradle @@ -13,4 +13,6 @@ shadowJar { dependencies { api(project(":java-spiffe-core")) + + testImplementation(testFixtures(project(":java-spiffe-core"))) } diff --git a/java-spiffe-provider/src/main/java/io/spiffe/provider/EnvironmentUtils.java b/java-spiffe-provider/src/main/java/io/spiffe/provider/EnvironmentUtils.java index fc1a67b..f5cce11 100644 --- a/java-spiffe-provider/src/main/java/io/spiffe/provider/EnvironmentUtils.java +++ b/java-spiffe-provider/src/main/java/io/spiffe/provider/EnvironmentUtils.java @@ -11,7 +11,6 @@ import java.security.Security; final class EnvironmentUtils { private EnvironmentUtils() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } /** @@ -41,11 +40,11 @@ final class EnvironmentUtils { */ static String getProperty(final String variableName) { String value; - value = Security.getProperty(variableName); + value = System.getProperty(variableName); if (StringUtils.isNotBlank(value)) { return value; } - value = System.getProperty(variableName); + value = Security.getProperty(variableName); if (StringUtils.isNotBlank(value)) { return value; } diff --git a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeKeyManagerFactory.java b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeKeyManagerFactory.java index 485def9..fa0df14 100644 --- a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeKeyManagerFactory.java +++ b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeKeyManagerFactory.java @@ -2,10 +2,12 @@ package io.spiffe.provider; import io.spiffe.exception.SocketEndpointAddressException; import io.spiffe.exception.X509SourceException; +import io.spiffe.provider.exception.SpiffeProviderException; +import io.spiffe.svid.x509svid.X509SvidSource; +import io.spiffe.workloadapi.DefaultX509Source; import io.spiffe.workloadapi.X509Source; import lombok.NonNull; import lombok.val; -import io.spiffe.svid.x509svid.X509SvidSource; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactorySpi; @@ -16,7 +18,7 @@ import java.security.KeyStore; * Implementation of a {@link KeyManagerFactorySpi} to create a {@link KeyManager} that is backed by the Workload API. *

* The Java Security API will call engineGetKeyManagers() to get an instance of a KeyManager. - * This KeyManager instance is injected with an {@link X509Source} to obtain the latest X.509 SVIDs updates + * This KeyManager instance is injected with an {@link DefaultX509Source} to obtain the latest X.509 SVIDs updates * from the Workload API. * * @see SpiffeSslContextFactory @@ -27,14 +29,14 @@ import java.security.KeyStore; public final class SpiffeKeyManagerFactory extends KeyManagerFactorySpi { /** - * Default method for creating the KeyManager, uses an {@link X509Source} instance + * Default method for creating the KeyManager, uses an {@link DefaultX509Source} instance * that is handled by the Singleton {@link X509SourceManager} * * @throws SpiffeProviderException in case there is an error setting up the X.509 source */ @Override protected KeyManager[] engineGetKeyManagers() { - val x509Source = createX509Source(); + val x509Source = getX509Source(); val spiffeKeyManager = new SpiffeKeyManager(x509Source); return new KeyManager[]{spiffeKeyManager}; } @@ -60,7 +62,7 @@ public final class SpiffeKeyManagerFactory extends KeyManagerFactorySpi { //no implementation needed } - private X509Source createX509Source() { + private X509Source getX509Source() { try { return X509SourceManager.getX509Source(); } catch (X509SourceException e) { diff --git a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslContextFactory.java b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslContextFactory.java index 21e7336..91aadf6 100644 --- a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslContextFactory.java +++ b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslContextFactory.java @@ -1,6 +1,7 @@ package io.spiffe.provider; import io.spiffe.spiffeid.SpiffeId; +import io.spiffe.workloadapi.DefaultX509Source; import io.spiffe.workloadapi.X509Source; import lombok.AccessLevel; import lombok.Builder; @@ -30,14 +31,14 @@ public final class SpiffeSslContextFactory { /** * Creates an {@link SSLContext} initialized with a {@link SpiffeKeyManager} and {@link SpiffeTrustManager} - * that are backed by the Workload API via an {@link X509Source}. + * that are backed by the Workload API via an {@link DefaultX509Source}. * - * @param options {@link SslContextOptions}. The option {@link X509Source} must be not null. + * @param options {@link SslContextOptions}. The option {@link DefaultX509Source} must be not null. * If the option acceptedSpiffeIdsSupplier is not provided, the Set of accepted SPIFFE IDs * is read from the Security or System Property ssl.spiffe.accept. * If the sslProtocol is not provided, the default TLSv1.2 is used. * @return an initialized {@link SSLContext} - * @throws IllegalArgumentException if the {@link X509Source} is not provided in the options + * @throws IllegalArgumentException if the {@link DefaultX509Source} is not provided in the options * @throws NoSuchAlgorithmException if there is a problem creating the SSL context * @throws KeyManagementException if there is a problem initializing the SSL context */ @@ -85,7 +86,7 @@ public final class SpiffeSslContextFactory { *

* sslProtocol The SSL Protocol. Default: TLSv1.2 *

- * x509Source An {@link X509Source} that provides the X.509 materials. + * x509Source An {@link DefaultX509Source} that provides the X.509 materials. *

* acceptedSpiffeIdsSupplier A supplier of a set of {@link SpiffeId} that will be accepted * for a secure socket connection. diff --git a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslSocketFactory.java b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslSocketFactory.java index 5bcce8e..d33f84d 100644 --- a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslSocketFactory.java +++ b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeSslSocketFactory.java @@ -1,7 +1,7 @@ package io.spiffe.provider; -import lombok.val; import io.spiffe.provider.SpiffeSslContextFactory.SslContextOptions; +import lombok.val; import javax.net.ssl.SSLSocketFactory; import java.io.IOException; @@ -31,6 +31,10 @@ public class SpiffeSslSocketFactory extends SSLSocketFactory { delegate = sslContext.getSocketFactory(); } + SpiffeSslSocketFactory(final SSLSocketFactory delegate) { + this.delegate = delegate; + } + @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); diff --git a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeTrustManagerFactory.java b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeTrustManagerFactory.java index e5ebf48..87c1ac9 100644 --- a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeTrustManagerFactory.java +++ b/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeTrustManagerFactory.java @@ -4,8 +4,10 @@ import io.spiffe.bundle.BundleSource; import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.exception.SocketEndpointAddressException; import io.spiffe.exception.X509SourceException; +import io.spiffe.provider.exception.SpiffeProviderException; import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.SpiffeIdUtils; +import io.spiffe.workloadapi.DefaultX509Source; import io.spiffe.workloadapi.X509Source; import lombok.NonNull; import lombok.val; @@ -22,10 +24,10 @@ import static io.spiffe.provider.SpiffeProviderConstants.SSL_SPIFFE_ACCEPT_PROPE /** * Implementation of a {@link javax.net.ssl.TrustManagerFactory} to create a {@link SpiffeTrustManager} backed by a - * {@link X509Source} that is maintained via the Workload API. + * {@link DefaultX509Source} that is maintained via the Workload API. *

* The Java Security API will call engineGetTrustManagers() to get an instance of a {@link TrustManager}. - * This TrustManager instance gets injected an {@link X509Source}, which implements {@link BundleSource} and + * This TrustManager instance gets injected an {@link DefaultX509Source}, which implements {@link BundleSource} and * keeps bundles updated. * The TrustManager also gets a Supplier of a Set of accepted SPIFFE IDs used to validate the SPIFFE ID from the SVIDs * presented by a peer during the secure socket handshake. @@ -43,7 +45,7 @@ public class SpiffeTrustManagerFactory extends TrustManagerFactorySpi { () -> SpiffeIdUtils.toSetOfSpiffeIds(EnvironmentUtils.getProperty(SSL_SPIFFE_ACCEPT_PROPERTY)); /** - * Creates a {@link TrustManager} initialized with the {@link X509Source} instance + * Creates a {@link TrustManager} initialized with the {@link DefaultX509Source} instance * that is handled by the {@link X509SourceManager}, and with and a supplier of accepted SPIFFE IDs. that reads * the Set of {@link SpiffeId} from the System Property 'ssl.spiffe.accept'. *

diff --git a/java-spiffe-provider/src/main/java/io/spiffe/provider/X509SourceManager.java b/java-spiffe-provider/src/main/java/io/spiffe/provider/X509SourceManager.java index 77b8a52..6b0e648 100644 --- a/java-spiffe-provider/src/main/java/io/spiffe/provider/X509SourceManager.java +++ b/java-spiffe-provider/src/main/java/io/spiffe/provider/X509SourceManager.java @@ -2,10 +2,11 @@ package io.spiffe.provider; import io.spiffe.exception.SocketEndpointAddressException; import io.spiffe.exception.X509SourceException; +import io.spiffe.workloadapi.DefaultX509Source; import io.spiffe.workloadapi.X509Source; /** - * Singleton that handles an instance of an {@link X509Source}. + * Singleton that handles an instance of a {@link DefaultX509Source} that implements an {@link X509Source}. *

* The default SPIFFE socket endpoint address is used to create an X.509 Source backed by the * Workload API. @@ -13,7 +14,7 @@ import io.spiffe.workloadapi.X509Source; * If the environment variable is not defined, it will throw an IllegalStateException. * If the X509Source cannot be initialized, it will throw a RuntimeException. *

- * This Singleton needed to be able to handle a single {@link X509Source} instance + * This Singleton needed to be able to handle a single {@link DefaultX509Source} instance * to be used by the {@link SpiffeKeyManagerFactory} and {@link SpiffeTrustManagerFactory} to inject it * in the {@link SpiffeKeyManager} and {@link SpiffeTrustManager} instances. */ @@ -28,13 +29,13 @@ public final class X509SourceManager { * Returns the single instance handled by this singleton. If the instance has not been * created yet, it creates a new X509Source and initializes the singleton in a thread safe way. * - * @return the single instance of {@link X509Source} + * @return the single instance of {@link DefaultX509Source} * @throws X509SourceException if the X.509 source could not be initialized * @throws SocketEndpointAddressException is the socket endpoint address is not valid */ public static synchronized X509Source getX509Source() throws X509SourceException, SocketEndpointAddressException { if (x509Source == null) { - x509Source = X509Source.newSource(); + x509Source = DefaultX509Source.newSource(); } return x509Source; } diff --git a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeProviderException.java b/java-spiffe-provider/src/main/java/io/spiffe/provider/exception/SpiffeProviderException.java similarity index 90% rename from java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeProviderException.java rename to java-spiffe-provider/src/main/java/io/spiffe/provider/exception/SpiffeProviderException.java index e686048..985d75d 100644 --- a/java-spiffe-provider/src/main/java/io/spiffe/provider/SpiffeProviderException.java +++ b/java-spiffe-provider/src/main/java/io/spiffe/provider/exception/SpiffeProviderException.java @@ -1,4 +1,4 @@ -package io.spiffe.provider; +package io.spiffe.provider.exception; /** * Unchecked exception thrown when there is an error setting up the source of SVIDs and bundles. diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/EnvironmentUtilsTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/EnvironmentUtilsTest.java new file mode 100644 index 0000000..ed3b1fe --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/EnvironmentUtilsTest.java @@ -0,0 +1,44 @@ +package io.spiffe.provider; + +import org.junit.jupiter.api.Test; + +import java.security.Security; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EnvironmentUtilsTest { + + @Test + void getPropertyFromSystem() { + System.setProperty("testVariable", "example"); + String value = EnvironmentUtils.getProperty("testVariable"); + assertEquals("example", value); + } + + @Test + void getPropertyFromSecurity() { + Security.setProperty("testVariable", "example"); + String value = EnvironmentUtils.getProperty("testVariable"); + assertEquals("example", value); + } + + @Test + void getSecurityPropertyWithDefaultValue() { + Security.setProperty("testVariable", "example"); + String value = EnvironmentUtils.getProperty("otherVariable", "default"); + assertEquals("default", value); + } + + @Test + void getSystemPropertyWithDefaultValue() { + System.setProperty("testVariable", "example"); + String value = EnvironmentUtils.getProperty("testVariable", "default"); + assertEquals("example", value); + } + + @Test + void getPropertyReturnBlankForNotFoundVariable() { + String value = EnvironmentUtils.getProperty("unknown"); + assertEquals("", value); + } +} \ No newline at end of file diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerFactoryTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerFactoryTest.java new file mode 100644 index 0000000..7eb5173 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerFactoryTest.java @@ -0,0 +1,67 @@ +package io.spiffe.provider; + +import io.spiffe.exception.X509SvidException; +import io.spiffe.svid.x509svid.X509Svid; +import io.spiffe.svid.x509svid.X509SvidSource; +import io.spiffe.workloadapi.X509Source; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.KeyManager; +import java.lang.reflect.Field; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.cert.X509Certificate; + +import static io.spiffe.provider.SpiffeProviderConstants.DEFAULT_ALIAS; +import static io.spiffe.utils.TestUtils.toUri; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SpiffeKeyManagerFactoryTest { + + @Test + void engineGetKeyManagers_usingX509SourceManager() throws NoSuchFieldException, IllegalAccessException { + // init singleton with an instance + Field field = X509SourceManager.class.getDeclaredField("x509Source"); + field.setAccessible(true); + X509Source source = new X509SourceStub(); + field.set(null, source); + + KeyManager[] keyManagers = new SpiffeKeyManagerFactory().engineGetKeyManagers(); + SpiffeKeyManager keyManager = (SpiffeKeyManager) keyManagers[0]; + + X509Certificate[] chain = keyManager.getCertificateChain(DEFAULT_ALIAS); + X509Certificate certificate = chain[0]; + + assertEquals(source.getX509Svid().getChain().get(0), certificate); + } + + @Test + void engineGetKeyManagers_passingAX509SvidSource() throws URISyntaxException, X509SvidException { + Path cert = Paths.get(toUri("testdata/cert.pem")); + Path key = Paths.get(toUri("testdata/key.pem")); + X509Svid svid = X509Svid.load(cert, key); + X509SvidSource x509SvidSource = () -> svid; + + KeyManager[] keyManagers = new SpiffeKeyManagerFactory().engineGetKeyManagers(x509SvidSource); + SpiffeKeyManager keyManager = (SpiffeKeyManager) keyManagers[0]; + + assertEquals(svid.getChainArray()[0], keyManager.getCertificateChain(DEFAULT_ALIAS)[0]); + } + + @Test + void engineGetKeyManagers_nullParameter() { + try { + new SpiffeKeyManagerFactory().engineGetKeyManagers(null); + } catch (NullPointerException e) { + assertEquals("x509SvidSource is marked non-null but is null", e.getMessage()); + } + } + + @Test + void engineInit() { + new SpiffeKeyManagerFactory().engineInit(null); + new SpiffeKeyManagerFactory().engineInit(null, null); + + } +} \ No newline at end of file diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerTest.java index 52afe04..3520da0 100644 --- a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerTest.java +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyManagerTest.java @@ -1,6 +1,5 @@ package io.spiffe.provider; -import io.spiffe.exception.X509SvidException; import io.spiffe.internal.CertificateUtils; import io.spiffe.svid.x509svid.X509Svid; import io.spiffe.svid.x509svid.X509SvidSource; @@ -10,16 +9,21 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import javax.net.ssl.X509KeyManager; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Paths; +import java.security.PrivateKey; import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import static io.spiffe.provider.SpiffeProviderConstants.DEFAULT_ALIAS; +import static io.spiffe.utils.X509CertificateTestUtils.createCertificate; +import static io.spiffe.utils.X509CertificateTestUtils.createRootCA; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; public class SpiffeKeyManagerTest { @@ -27,24 +31,39 @@ public class SpiffeKeyManagerTest { @Mock X509SvidSource x509SvidSource; - X509KeyManager keyManager; + SpiffeKeyManager spiffeKeyManager; X509Svid x509Svid; @BeforeEach - void setup() throws X509SvidException, URISyntaxException { + void setup() throws Exception { MockitoAnnotations.initMocks(this); - keyManager = (X509KeyManager) new SpiffeKeyManagerFactory().engineGetKeyManagers(x509SvidSource)[0]; - x509Svid = X509Svid - .load( - Paths.get(toUri("testdata/cert.pem")), - Paths.get(toUri("testdata/key.pem"))); + + val rootCa = createRootCA("C = US, O = SPIFFE", "spiffe://domain.test"); + val leaf = createCertificate("C = US, O = SPIRE", "C = US, O = SPIRE", "spiffe://domain.test/workload", rootCa, false); + + X509Svid svid = X509Svid.parseRaw(leaf.getCertificate().getEncoded(), leaf.getKeyPair().getPrivate().getEncoded()); + + x509Svid = X509Svid.load( + Paths.get(toUri("testdata/cert.pem")), + Paths.get(toUri("testdata/key.pem"))); + when(x509SvidSource.getX509Svid()).thenReturn(x509Svid); + + spiffeKeyManager = new SpiffeKeyManager(x509SvidSource); } @Test - void getCertificateChain_returnsAnArrayOfX509Certificates() throws CertificateException { - when(x509SvidSource.getX509Svid()).thenReturn(x509Svid); + void testCreateNewSpiffeKeyManager_nullSource() { + try { + new SpiffeKeyManager(null); + fail(); + } catch (Exception e) { + assertEquals("x509SvidSource is marked non-null but is null", e.getMessage()); + } + } - val certificateChain = keyManager.getCertificateChain(DEFAULT_ALIAS); + @Test + void getCertificateChain() throws CertificateException { + val certificateChain = spiffeKeyManager.getCertificateChain(DEFAULT_ALIAS); val spiffeId = CertificateUtils.getSpiffeId(certificateChain[0]); assertAll( @@ -53,15 +72,66 @@ public class SpiffeKeyManagerTest { ); } + @Test + void getCertificateChain_aliasNotSupported() { + X509Certificate[] chain = spiffeKeyManager.getCertificateChain("other_alias"); + assertEquals(0, chain.length); + } + @Test void getPrivateKey_aliasIsSpiffe_returnAPrivateKey() { - when(x509SvidSource.getX509Svid()).thenReturn(x509Svid); - - val privateKey = keyManager.getPrivateKey(DEFAULT_ALIAS); - + val privateKey = spiffeKeyManager.getPrivateKey(DEFAULT_ALIAS); assertNotNull(privateKey); } + @Test + void getPrivateKey_aliasNotSupported() { + PrivateKey privateKey = spiffeKeyManager.getPrivateKey("other_alias"); + assertNull(privateKey); + } + + @Test + void getClientAliases() { + String[] aliases = spiffeKeyManager.getClientAliases("EC", null); + assertEquals(DEFAULT_ALIAS, aliases[0]); + } + + @Test + void chooseClientAlias() { + String alias = spiffeKeyManager.chooseClientAlias(new String[]{"EC"}, null, null); + assertEquals(DEFAULT_ALIAS, alias); + } + + @Test + void chooseEngineClientAlias() { + String alias = spiffeKeyManager.chooseEngineClientAlias(new String[]{"EC"}, null, null); + assertEquals(DEFAULT_ALIAS, alias); + } + + @Test + void getServerAliases() { + String[] aliases = spiffeKeyManager.getServerAliases("EC", null); + assertEquals(DEFAULT_ALIAS, aliases[0]); + } + + @Test + void chooseEngineServerAlias() { + String alias = spiffeKeyManager.chooseEngineServerAlias("EC", null, null); + assertEquals(DEFAULT_ALIAS, alias); + } + + @Test + void chooseServerAlias() { + String alias = spiffeKeyManager.chooseServerAlias("EC", null, null); + assertEquals(DEFAULT_ALIAS, alias); + } + + @Test + void chooseServerAlias_keyTypeNotSupported() { + String alias = spiffeKeyManager.chooseServerAlias("not-supported", null, null); + assertNull(alias); + } + private URI toUri(String path) throws URISyntaxException { return Thread.currentThread().getContextClassLoader().getResource(path).toURI(); } diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyStoreTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyStoreTest.java new file mode 100644 index 0000000..c132ac5 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeKeyStoreTest.java @@ -0,0 +1,109 @@ +package io.spiffe.provider; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.security.cert.Certificate; +import java.util.Enumeration; + +import static io.spiffe.provider.SpiffeProviderConstants.DEFAULT_ALIAS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SpiffeKeyStoreTest { + + private static SpiffeKeyStore spiffeKeyStore; + + @BeforeAll + static void setup() { + spiffeKeyStore = new SpiffeKeyStore(); + } + @Test + void engineGetKey() { + assertNull(spiffeKeyStore.engineGetKey("alias", "pass".toCharArray())); + } + + @Test + void engineGetCertificateChain() { + Certificate[] chain = spiffeKeyStore.engineGetCertificateChain("alias"); + assertEquals(0, chain.length); + } + + @Test + void engineGetCertificate() { + assertNull(spiffeKeyStore.engineGetCertificate("alias")); + } + + @Test + void engineGetCreationDate() { + assertNotNull(spiffeKeyStore.engineGetCreationDate("alias")); + } + + @Test + void engineSetKeyEntry() { + spiffeKeyStore.engineSetKeyEntry("alias", null, null); + spiffeKeyStore.engineSetKeyEntry("alias", null, null, null); + } + + @Test + void testEngineSetKeyEntry() { + spiffeKeyStore.engineSetKeyEntry("alias", null, null); + spiffeKeyStore.engineSetKeyEntry("alias", null, null, null); + } + + @Test + void engineSetCertificateEntry() { + spiffeKeyStore.engineSetCertificateEntry("alias", null); + } + + @Test + void engineDeleteEntry() { + spiffeKeyStore.engineDeleteEntry("alias"); + } + + @Test + void engineAliases() { + Enumeration enumeration = spiffeKeyStore.engineAliases(); + assertEquals(DEFAULT_ALIAS, enumeration.nextElement()); + } + + @Test + void engineContainsAlias() { + assertTrue(spiffeKeyStore.engineContainsAlias(DEFAULT_ALIAS)); + } + + @Test + void engineSize() { + assertEquals(1, spiffeKeyStore.engineSize()); + } + + @Test + void engineIsKeyEntry() { + assertTrue(spiffeKeyStore.engineIsKeyEntry(DEFAULT_ALIAS)); + assertFalse(spiffeKeyStore.engineIsKeyEntry("alias")); + } + + @Test + void engineIsCertificateEntry() { + assertTrue(spiffeKeyStore.engineIsCertificateEntry(DEFAULT_ALIAS)); + assertFalse(spiffeKeyStore.engineIsCertificateEntry("alias")); + } + + @Test + void engineGetCertificateAlias() { + assertEquals(DEFAULT_ALIAS, spiffeKeyStore.engineGetCertificateAlias(null)); + } + + @Test + void engineStore() { + spiffeKeyStore.engineStore(null, null); + } + + @Test + void engineLoad() { + spiffeKeyStore.engineLoad(null, null); + } +} \ No newline at end of file diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeProviderTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeProviderTest.java new file mode 100644 index 0000000..0734c44 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeProviderTest.java @@ -0,0 +1,24 @@ +package io.spiffe.provider; + +import org.junit.jupiter.api.Test; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class SpiffeProviderTest { + + @Test + void install() throws NoSuchAlgorithmException { + SpiffeProvider.install(); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(SpiffeProviderConstants.ALGORITHM); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(SpiffeProviderConstants.ALGORITHM); + assertNotNull(keyManagerFactory); + assertNotNull(trustManagerFactory); + + // should do nothing + SpiffeProvider.install(); + } +} \ No newline at end of file diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslContextFactoryTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslContextFactoryTest.java new file mode 100644 index 0000000..741d309 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslContextFactoryTest.java @@ -0,0 +1,86 @@ +package io.spiffe.provider; + +import io.spiffe.workloadapi.X509Source; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +class SpiffeSslContextFactoryTest { + + X509Source x509Source; + + @BeforeEach + void setup() { + x509Source = new X509SourceStub(); + } + + @Test + void getSslContext_withX509Source() { + SpiffeSslContextFactory.SslContextOptions options = SpiffeSslContextFactory.SslContextOptions + .builder().x509Source(x509Source).build(); + try { + assertNotNull(SpiffeSslContextFactory.getSslContext(options)); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + fail(e); + } + } + + @Test + void getSslContext_withSupplierOfSpiffeIds() { + SpiffeSslContextFactory.SslContextOptions options = SpiffeSslContextFactory.SslContextOptions + .builder().x509Source(x509Source).acceptedSpiffeIdsSupplier(Collections::emptySet).build(); + try { + assertNotNull(SpiffeSslContextFactory.getSslContext(options)); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + fail(e); + } + } + + @Test + void getSslContext_withAcceptAny() { + SpiffeSslContextFactory.SslContextOptions options = SpiffeSslContextFactory.SslContextOptions + .builder().x509Source(x509Source).acceptAnySpiffeId(true).build(); + try { + assertNotNull(SpiffeSslContextFactory.getSslContext(options)); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + fail(e); + } + } + + @Test + void getSslContext_withOtherSslProtocol() { + SpiffeSslContextFactory.SslContextOptions options = SpiffeSslContextFactory.SslContextOptions + .builder().x509Source(x509Source).sslProtocol("TLSv1.1").build(); + try { + assertNotNull(SpiffeSslContextFactory.getSslContext(options)); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + fail(e); + } + } + + @Test + void getSslContext_nullOptions() throws KeyManagementException, NoSuchAlgorithmException { + try { + SpiffeSslContextFactory.getSslContext(null); + } catch (NullPointerException e) { + assertEquals("options is marked non-null but is null", e.getMessage()); + } + } + + @Test + void getSslContext_nullX509Source() throws KeyManagementException, NoSuchAlgorithmException { + SpiffeSslContextFactory.SslContextOptions options = SpiffeSslContextFactory.SslContextOptions.builder().build(); + try { + SpiffeSslContextFactory.getSslContext(options); + } catch (IllegalArgumentException e) { + assertEquals("x509Source option cannot be null, an X.509 Source must be provided", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslSocketFactoryTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslSocketFactoryTest.java new file mode 100644 index 0000000..75d7da8 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeSslSocketFactoryTest.java @@ -0,0 +1,118 @@ +package io.spiffe.provider; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +class SpiffeSslSocketFactoryTest { + + @Mock + SSLSocketFactory sslSocketFactoryMock; + + private SpiffeSslSocketFactory spiffeSslSocketFactory; + private SSLSocketFactory socketFactory; + + @BeforeEach + void setup() throws NoSuchAlgorithmException, KeyManagementException { + X509SourceStub x509Source = new X509SourceStub(); + SpiffeSslContextFactory.SslContextOptions options = SpiffeSslContextFactory.SslContextOptions.builder().x509Source(x509Source).build(); + spiffeSslSocketFactory = new SpiffeSslSocketFactory(options); + SSLContext sslContext = SpiffeSslContextFactory.getSslContext(options); + socketFactory = sslContext.getSocketFactory(); + MockitoAnnotations.initMocks(this); + } + + @Test + void getDefaultCipherSuites() { + String[] defaultCipherSuites = spiffeSslSocketFactory.getDefaultCipherSuites(); + String[] expected = socketFactory.getDefaultCipherSuites(); + assertArrayEquals(expected, defaultCipherSuites); + } + + @Test + void getSupportedCipherSuites() { + String[] supportedCipherSuites = spiffeSslSocketFactory.getSupportedCipherSuites(); + String[] expected = socketFactory.getSupportedCipherSuites(); + assertArrayEquals(expected, supportedCipherSuites); + } + + @Test + void createSocket() throws IOException { + SpiffeSslSocketFactory socketFactory = new SpiffeSslSocketFactory(sslSocketFactoryMock); + + Socket expected = new Socket(); + when(sslSocketFactoryMock.createSocket()).thenReturn(expected); + Socket socket = socketFactory.createSocket(); + + assertEquals(expected, socket); + } + + @Test + void testCreateSocket_HostParameter() throws IOException { + SpiffeSslSocketFactory socketFactory = new SpiffeSslSocketFactory(sslSocketFactoryMock); + + Socket expected = new Socket(); + when(sslSocketFactoryMock.createSocket("string", 1)).thenReturn(expected); + Socket socket = socketFactory.createSocket("string", 1); + + assertEquals(expected, socket); + } + + @Test + void testCreateSocket_InetAddressParameter() throws IOException { + SpiffeSslSocketFactory socketFactory = new SpiffeSslSocketFactory(sslSocketFactoryMock); + + Socket expected = new Socket(); + when(sslSocketFactoryMock.createSocket(InetAddress.getLocalHost(), 1)).thenReturn(expected); + Socket socket = socketFactory.createSocket(InetAddress.getLocalHost(), 1); + + assertEquals(expected, socket); + } + + @Test + void testCreateSocket_StringInetAddressParameter() throws IOException { + SpiffeSslSocketFactory socketFactory = new SpiffeSslSocketFactory(sslSocketFactoryMock); + + Socket expected = new Socket(); + when(sslSocketFactoryMock.createSocket("string", 1, InetAddress.getLocalHost(), 2)).thenReturn(expected); + Socket socket = socketFactory.createSocket("string", 1, InetAddress.getLocalHost(), 2); + + assertEquals(expected, socket); + } + + @Test + void testCreateSocket_InetAddressPortInetAddressPortParameters() throws IOException { + SpiffeSslSocketFactory socketFactory = new SpiffeSslSocketFactory(sslSocketFactoryMock); + + Socket expected = new Socket(); + when(sslSocketFactoryMock.createSocket(InetAddress.getLocalHost(), 1, InetAddress.getLocalHost(), 2)).thenReturn(expected); + Socket socket = socketFactory.createSocket(InetAddress.getLocalHost(), 1, InetAddress.getLocalHost(), 2); + + assertEquals(expected, socket); + } + + @Test + void testCreateSocket_parametersSocketStringPortAutoClose() throws IOException { + SpiffeSslSocketFactory socketFactory = new SpiffeSslSocketFactory(sslSocketFactoryMock); + + Socket expected = new Socket(); + Socket s = new Socket(); + when(sslSocketFactoryMock.createSocket(s, "string", 1, true)).thenReturn(expected); + Socket socket = socketFactory.createSocket(s, "string", 1, true); + + assertEquals(expected, socket); + } +} \ No newline at end of file diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerFactoryTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerFactoryTest.java new file mode 100644 index 0000000..f996395 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/SpiffeTrustManagerFactoryTest.java @@ -0,0 +1,130 @@ +package io.spiffe.provider; + +import io.spiffe.bundle.BundleSource; +import io.spiffe.bundle.x509bundle.X509Bundle; +import io.spiffe.exception.BundleNotFoundException; +import io.spiffe.spiffeid.SpiffeId; +import io.spiffe.spiffeid.TrustDomain; +import io.spiffe.workloadapi.X509Source; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.TrustManager; +import java.lang.reflect.Field; +import java.util.Set; +import java.util.function.Supplier; + +import static io.spiffe.provider.SpiffeProviderConstants.SSL_SPIFFE_ACCEPT_PROPERTY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SpiffeTrustManagerFactoryTest { + + @Test + void engineGetTrustManagers() throws Exception { + System.setProperty(SSL_SPIFFE_ACCEPT_PROPERTY, "spiffe://example.org/test1 | spiffe://example.org/test2" ); + + // init singleton with an instance + Field field = X509SourceManager.class.getDeclaredField("x509Source"); + field.setAccessible(true); + X509Source source = new X509SourceStub(); + field.set(null, source); + + TrustManager[] trustManagers = new SpiffeTrustManagerFactory().engineGetTrustManagers(); + SpiffeTrustManager trustManager = (SpiffeTrustManager) trustManagers[0]; + + BundleSource bundleSource = getX509BundleBundleSource(trustManager); + Supplier> supplier = getSetSupplier(trustManager); + boolean acceptAny = isAcceptAny(trustManager); + + TrustDomain trustDomain = TrustDomain.of("example.org"); + assertEquals(source.getBundleForTrustDomain(trustDomain), bundleSource.getBundleForTrustDomain(trustDomain)); + assertEquals(2, supplier.get().size()); + assertTrue(supplier.get().contains(SpiffeId.parse("spiffe://example.org/test1"))); + assertFalse(acceptAny); + } + + + @Test + void testEngineGetTrustManagers_withCustomSource() throws NoSuchFieldException, IllegalAccessException, BundleNotFoundException { + System.setProperty(SSL_SPIFFE_ACCEPT_PROPERTY, "spiffe://example.org/test1 | spiffe://example.org/test2" ); + + X509Source source = new X509SourceStub(); + TrustManager[] trustManagers = new SpiffeTrustManagerFactory().engineGetTrustManagers(source); + SpiffeTrustManager trustManager = (SpiffeTrustManager) trustManagers[0]; + + BundleSource bundleSource = getX509BundleBundleSource(trustManager); + Supplier> supplier = getSetSupplier(trustManager); + boolean acceptAny = isAcceptAny(trustManager); + + TrustDomain trustDomain = TrustDomain.of("example.org"); + assertEquals(source.getBundleForTrustDomain(trustDomain), bundleSource.getBundleForTrustDomain(trustDomain)); + assertEquals(2, supplier.get().size()); + assertTrue(supplier.get().contains(SpiffeId.parse("spiffe://example.org/test1"))); + assertFalse(acceptAny); + } + + @Test + void engineGetTrustManagersAcceptAnySpiffeId() throws NoSuchFieldException, IllegalAccessException { + X509Source source = new X509SourceStub(); + TrustManager[] trustManagers = new SpiffeTrustManagerFactory().engineGetTrustManagersAcceptAnySpiffeId(source); + SpiffeTrustManager trustManager = (SpiffeTrustManager) trustManagers[0]; + boolean acceptAny = isAcceptAny(trustManager); + assertTrue(acceptAny); + } + + @Test + void engineGetTrustManagersAcceptAnySpiffeId_nullParameter() { + try { + new SpiffeTrustManagerFactory().engineGetTrustManagersAcceptAnySpiffeId(null); + } catch (NullPointerException e) { + assertEquals("x509BundleSource is marked non-null but is null", e.getMessage()); + } + } + + @Test + void engineGetTrustManagers_nullParameter() { + try { + new SpiffeTrustManagerFactory().engineGetTrustManagers(null); + } catch (NullPointerException e) { + assertEquals("x509BundleSource is marked non-null but is null", e.getMessage()); + } + } + + @Test + void engineGetTrustManagers_nullParameters() { + try { + new SpiffeTrustManagerFactory().engineGetTrustManagers(null, null); + } catch (NullPointerException e) { + assertEquals("x509BundleSource is marked non-null but is null", e.getMessage()); + } + } + + @Test + void engineGetTrustManagers_nullSupplier() { + X509Source source = new X509SourceStub(); + try { + new SpiffeTrustManagerFactory().engineGetTrustManagers(source, null); + } catch (NullPointerException e) { + assertEquals("acceptedSpiffeIdsSupplier is marked non-null but is null", e.getMessage()); + } + } + + private BundleSource getX509BundleBundleSource(SpiffeTrustManager trustManager) throws NoSuchFieldException, IllegalAccessException { + Field bundleField = SpiffeTrustManager.class.getDeclaredField("x509BundleSource"); + bundleField.setAccessible(true); + return (BundleSource) bundleField.get(trustManager); + } + + private Supplier> getSetSupplier(SpiffeTrustManager trustManager) throws NoSuchFieldException, IllegalAccessException { + Field supplierField = SpiffeTrustManager.class.getDeclaredField("acceptedSpiffeIdsSupplier"); + supplierField.setAccessible(true); + return (Supplier>) supplierField.get(trustManager); + } + + private boolean isAcceptAny(SpiffeTrustManager trustManager) throws NoSuchFieldException, IllegalAccessException { + Field acceptAnyField = SpiffeTrustManager.class.getDeclaredField("acceptAnySpiffeId"); + acceptAnyField.setAccessible(true); + return (boolean) acceptAnyField.get(trustManager); + } +} \ No newline at end of file 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 82d51e3..6968f8a 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 @@ -3,10 +3,8 @@ package io.spiffe.provider; import io.spiffe.bundle.BundleSource; import io.spiffe.bundle.x509bundle.X509Bundle; import io.spiffe.exception.BundleNotFoundException; -import io.spiffe.exception.X509SvidException; import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.TrustDomain; -import io.spiffe.svid.x509svid.X509Svid; import lombok.val; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -14,16 +12,19 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import javax.net.ssl.X509TrustManager; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Paths; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import java.net.Socket; +import java.nio.ByteBuffer; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.Set; +import static io.spiffe.utils.X509CertificateTestUtils.createCertificate; +import static io.spiffe.utils.X509CertificateTestUtils.createRootCA; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; @@ -33,62 +34,226 @@ public class SpiffeTrustManagerTest { @Mock BundleSource bundleSource; - static X509Bundle x509Bundle; - static X509Svid x509Svid; - static X509Svid otherX509Svid; + static X509Certificate[] chain; + static X509Bundle bundleKnown; + static X509Bundle bundleUnknown; + Set acceptedSpiffeIds; - X509TrustManager trustManager; + + SpiffeTrustManager spiffeTrustManager; @BeforeAll - static void setupClass() throws IOException, CertificateException, X509SvidException, URISyntaxException { - x509Svid = X509Svid - .load( - Paths.get(toUri("testdata/cert.pem")), - Paths.get(toUri("testdata/key.pem"))); - otherX509Svid = X509Svid - .load( - Paths.get(toUri("testdata/cert2.pem")), - Paths.get(toUri("testdata/key2.pem"))); - x509Bundle = X509Bundle - .load( - TrustDomain.of("example.org"), - Paths.get(toUri("testdata/bundle.pem"))); + static void setupClass() throws Exception { + val subject = "C = US, O = SPIRE"; + val issuerSubject = "C = US, O = SPIFFE"; + + val trustDomain = TrustDomain.of("spiffe://example.org"); + val spiffeIdRoot = trustDomain.newSpiffeId(); + val spiffeIdHost1 = SpiffeId.of(trustDomain, "host1"); + val spiffeIdHost2 = SpiffeId.of(trustDomain, "host2"); + val spiffeIdTest = SpiffeId.of(trustDomain, "test"); + + val rootCa = createRootCA(issuerSubject, spiffeIdRoot.toString() ); + val otherRootCa = createRootCA(issuerSubject, spiffeIdRoot.toString()); + + val intermediate1 = createCertificate(subject, issuerSubject, spiffeIdHost1.toString(), rootCa, true); + val intermediate2 = createCertificate(subject, subject, spiffeIdHost2.toString(), intermediate1, true); + val leaf = createCertificate(subject, subject, spiffeIdTest.toString(), intermediate2, false); + + chain = new X509Certificate[]{leaf.getCertificate(), intermediate2.getCertificate(), intermediate1.getCertificate()}; + + bundleKnown = X509Bundle.parse(trustDomain, rootCa.getCertificate().getEncoded()); + bundleUnknown = X509Bundle.parse(trustDomain, otherRootCa.getCertificate().getEncoded()); } @BeforeEach void setupMocks() { MockitoAnnotations.initMocks(this); - trustManager = (X509TrustManager) - new SpiffeTrustManagerFactory().engineGetTrustManagers(bundleSource, () -> acceptedSpiffeIds)[0]; + spiffeTrustManager = new SpiffeTrustManager(bundleSource, () -> acceptedSpiffeIds); } @Test - void checkClientTrusted_passAExpiredCertificate_throwsException() throws BundleNotFoundException { + void testCreateSpiffeTrustManager_nullSource() { + try { + new SpiffeTrustManager(null, true); + fail(); + } catch (Exception e) { + assertEquals("x509BundleSource is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testCreateSpiffeTrustManager_nullSupplier() { + try { + new SpiffeTrustManager(bundleSource, null); + fail(); + } catch (Exception e) { + assertEquals("acceptedSpiffeIdsSupplier is marked non-null but is null", e.getMessage()); + } + } + + @Test + void testCreateSpiffeTrustManager_nullParameters() { + try { + new SpiffeTrustManager(null, null); + fail(); + } catch (Exception e) { + assertEquals("x509BundleSource is marked non-null but is null", e.getMessage()); + } + } + + @Test + void test_checkClientTrustedMethods_Success() throws BundleNotFoundException { acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/test")); - - val chain = x509Svid.getChainArray(); - - when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(x509Bundle); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleKnown); try { - trustManager.checkClientTrusted(chain, ""); - fail("CertificateException was expected"); + spiffeTrustManager.checkClientTrusted(chain, ""); + spiffeTrustManager.checkClientTrusted(chain, "", new Socket()); + spiffeTrustManager.checkClientTrusted(chain, "", getSslEngineStub()); + } catch (CertificateException e) { + fail(e); + } + } + + @Test + void test_checkClientTrustedMethods_ChainCannotVerify() throws BundleNotFoundException { + acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/test")); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleUnknown); + + try { + spiffeTrustManager.checkClientTrusted(chain, ""); + fail(); } catch (CertificateException e) { assertEquals("Cert chain cannot be verified", e.getMessage()); } + + try { + spiffeTrustManager.checkClientTrusted(chain, "", new Socket()); + fail(); + } catch (CertificateException e) { + assertEquals("Cert chain cannot be verified", e.getMessage()); + } + + try { + spiffeTrustManager.checkClientTrusted(chain, "", getSslEngineStub()); + fail(); + } catch (CertificateException e) { + assertEquals("Cert chain cannot be verified", e.getMessage()); + } + } + + @Test + void test_checkClientTrustedMethods_ChainIsNull() throws CertificateException { + try { + spiffeTrustManager.checkClientTrusted(null, ""); + fail(); + } catch (NullPointerException e) { + assertEquals("chain is marked non-null but is null", e.getMessage()); + } + + try { + spiffeTrustManager.checkClientTrusted(null, "", new Socket()); + fail(); + } catch (NullPointerException e) { + assertEquals("chain is marked non-null but is null", e.getMessage()); + } + + try { + spiffeTrustManager.checkClientTrusted(null, "", getSslEngineStub()); + fail(); + } catch (NullPointerException e) { + assertEquals("chain is marked non-null but is null", e.getMessage()); + } + } + + @Test + void test_checkServerTrustedMethods_Success() throws BundleNotFoundException { + acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/test")); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleKnown); + + try { + spiffeTrustManager.checkServerTrusted(chain, ""); + spiffeTrustManager.checkServerTrusted(chain, "", new Socket()); + spiffeTrustManager.checkServerTrusted(chain, "", getSslEngineStub()); + } catch (CertificateException e) { + fail(e); + } + } + + @Test + void test_checkServerTrustedMethods_ChainCannotVerify() throws BundleNotFoundException { + acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/test")); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleUnknown); + + try { + spiffeTrustManager.checkServerTrusted(chain, ""); + fail(); + } catch (CertificateException e) { + assertEquals("Cert chain cannot be verified", e.getMessage()); + } + + try { + spiffeTrustManager.checkServerTrusted(chain, "", new Socket()); + fail(); + } catch (CertificateException e) { + assertEquals("Cert chain cannot be verified", e.getMessage()); + } + + try { + spiffeTrustManager.checkServerTrusted(chain, "", getSslEngineStub()); + fail(); + } catch (CertificateException e) { + assertEquals("Cert chain cannot be verified", e.getMessage()); + } + } + + @Test + void test_checkServerTrustedMethods_ChainIsNull() throws CertificateException { + try { + spiffeTrustManager.checkServerTrusted(null, ""); + fail(); + } catch (NullPointerException e) { + assertEquals("chain is marked non-null but is null", e.getMessage()); + } + + try { + spiffeTrustManager.checkServerTrusted(null, "", new Socket()); + fail(); + } catch (NullPointerException e) { + assertEquals("chain is marked non-null but is null", e.getMessage()); + } + + try { + spiffeTrustManager.checkServerTrusted(null, "", getSslEngineStub()); + fail(); + } catch (NullPointerException e) { + assertEquals("chain is marked non-null but is null", e.getMessage()); + } } @Test void checkClientTrusted_noBundleForTrustDomain_ThrowCertificateException() throws BundleNotFoundException { acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/test")); - val chain = x509Svid.getChainArray(); - when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenThrow(new BundleNotFoundException("Bundle not found")); try { - trustManager.checkClientTrusted(chain, ""); - fail("CertificateException was expected"); + spiffeTrustManager.checkClientTrusted(chain, ""); + fail(); + } catch (CertificateException e) { + assertEquals("Bundle not found", e.getMessage()); + } + } + + @Test + void checkServerTrusted_noBundleForTrustDomain_ThrowCertificateException() throws BundleNotFoundException { + acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/test")); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenThrow(new BundleNotFoundException("Bundle not found")); + + try { + spiffeTrustManager.checkServerTrusted(chain, ""); + fail(); } catch (CertificateException e) { assertEquals("Bundle not found", e.getMessage()); } @@ -97,14 +262,10 @@ public class SpiffeTrustManagerTest { @Test void checkClientTrusted_passCertificateWithNonAcceptedSpiffeId_ThrowCertificateException() throws BundleNotFoundException { acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/other")); - - X509Certificate[] chain = x509Svid.getChainArray(); - - when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))) - .thenReturn(x509Bundle); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleKnown); try { - trustManager.checkClientTrusted(chain, ""); + spiffeTrustManager.checkClientTrusted(chain, ""); fail("CertificateException was expected"); } catch (CertificateException e) { assertEquals("SPIFFE ID spiffe://example.org/test in X.509 certificate is not accepted", e.getMessage()); @@ -112,47 +273,40 @@ public class SpiffeTrustManagerTest { } @Test - void checkClientTrusted_passCertificateThatDoesntChainToBundle_ThrowCertificateException() throws BundleNotFoundException { - acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://other.org/test")); + void checkClientTrusted_acceptyAnySpiffeId() throws BundleNotFoundException { + acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/other")); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleKnown); - val chain = otherX509Svid.getChainArray(); - - when(bundleSource.getBundleForTrustDomain(TrustDomain.of("other.org"))).thenReturn(x509Bundle); + spiffeTrustManager = new SpiffeTrustManager(bundleSource, true); try { - trustManager.checkClientTrusted(chain, ""); - fail("CertificateException was expected"); + spiffeTrustManager.checkClientTrusted(chain, ""); } catch (CertificateException e) { - assertEquals("Cert chain cannot be verified", e.getMessage()); + fail(e); } } @Test - void checkServerTrusted_passAnExpiredCertificate_ThrowsException() throws BundleNotFoundException { - acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/test")); + void checkServerTrusted_acceptyAnySpiffeId() throws BundleNotFoundException { + acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/other")); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleKnown); - val chain = x509Svid.getChainArray(); - - when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(x509Bundle); + spiffeTrustManager = new SpiffeTrustManager(bundleSource, true); try { - trustManager.checkServerTrusted(chain, ""); - fail("CertificateException was expected"); + spiffeTrustManager.checkClientTrusted(chain, ""); } catch (CertificateException e) { - assertEquals("Cert chain cannot be verified", e.getMessage()); + fail(e); } } @Test void checkServerTrusted_passCertificateWithNonAcceptedSpiffeId_ThrowCertificateException() throws BundleNotFoundException { acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://example.org/other")); - - val chain = x509Svid.getChainArray(); - - when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(x509Bundle); + when(bundleSource.getBundleForTrustDomain(TrustDomain.of("example.org"))).thenReturn(bundleKnown); try { - trustManager.checkServerTrusted(chain, ""); + spiffeTrustManager.checkServerTrusted(chain, ""); fail("CertificateException was expected"); } catch (CertificateException e) { assertEquals("SPIFFE ID spiffe://example.org/test in X.509 certificate is not accepted", e.getMessage()); @@ -160,22 +314,132 @@ public class SpiffeTrustManagerTest { } @Test - void checkServerTrusted_passCertificateThatDoesntChainToBundle_ThrowCertificateException() throws BundleNotFoundException { - acceptedSpiffeIds = Collections.singleton(SpiffeId.parse("spiffe://other.org/test")); - - val chain = otherX509Svid.getChainArray(); - - when(bundleSource.getBundleForTrustDomain(TrustDomain.of("other.org"))).thenReturn(x509Bundle); - - try { - trustManager.checkServerTrusted(chain, ""); - fail("CertificateException was expected"); - } catch (CertificateException e) { - assertEquals("Cert chain cannot be verified", e.getMessage()); - } + void getAcceptedIssuers() { + X509Certificate[] acceptedIssuers = spiffeTrustManager.getAcceptedIssuers(); + assertEquals(0, acceptedIssuers.length); } - private static URI toUri(String path) throws URISyntaxException { - return Thread.currentThread().getContextClassLoader().getResource(path).toURI(); + private SSLEngine getSslEngineStub() { + return new SSLEngine() { + @Override + public SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst) throws SSLException { + return null; + } + + @Override + public SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) throws SSLException { + return null; + } + + @Override + public Runnable getDelegatedTask() { + return null; + } + + @Override + public void closeInbound() throws SSLException { + + } + + @Override + public boolean isInboundDone() { + return false; + } + + @Override + public void closeOutbound() { + + } + + @Override + public boolean isOutboundDone() { + return false; + } + + @Override + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + @Override + public String[] getEnabledCipherSuites() { + return new String[0]; + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + + } + + @Override + public String[] getSupportedProtocols() { + return new String[0]; + } + + @Override + public String[] getEnabledProtocols() { + return new String[0]; + } + + @Override + public void setEnabledProtocols(String[] protocols) { + + } + + @Override + public SSLSession getSession() { + return null; + } + + @Override + public void beginHandshake() throws SSLException { + + } + + @Override + public SSLEngineResult.HandshakeStatus getHandshakeStatus() { + return null; + } + + @Override + public void setUseClientMode(boolean mode) { + + } + + @Override + public boolean getUseClientMode() { + return false; + } + + @Override + public void setNeedClientAuth(boolean need) { + + } + + @Override + public boolean getNeedClientAuth() { + return false; + } + + @Override + public void setWantClientAuth(boolean want) { + + } + + @Override + public boolean getWantClientAuth() { + return false; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + + } + + @Override + public boolean getEnableSessionCreation() { + return false; + } + }; } } diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceManagerTest.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceManagerTest.java new file mode 100644 index 0000000..9c84d72 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceManagerTest.java @@ -0,0 +1,34 @@ +package io.spiffe.provider; + +import io.spiffe.utils.TestUtils; +import io.spiffe.workloadapi.Address; +import io.spiffe.workloadapi.X509Source; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class X509SourceManagerTest { + + @Test + void getX509Source_returnTheX509SourceInstance() throws Exception { + Field field = X509SourceManager.class.getDeclaredField("x509Source"); + field.setAccessible(true); + X509Source source = new X509SourceStub(); + field.set(null, source); + + X509Source x509Source = X509SourceManager.getX509Source(); + assertEquals(source, x509Source); + } + + @Test + void getX509Source_defaultAddressNotSet() throws Exception { + TestUtils.setEnvironmentVariable(Address.SOCKET_ENV_VARIABLE, "" ); + try { + X509SourceManager.getX509Source(); + } catch (IllegalStateException e) { + assertEquals("Endpoint Socket Address Environment Variable is not set: SPIFFE_ENDPOINT_SOCKET", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceStub.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceStub.java new file mode 100644 index 0000000..419dbf8 --- /dev/null +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/X509SourceStub.java @@ -0,0 +1,52 @@ +package io.spiffe.provider; + +import io.spiffe.bundle.x509bundle.X509Bundle; +import io.spiffe.exception.BundleNotFoundException; +import io.spiffe.exception.X509BundleException; +import io.spiffe.exception.X509SvidException; +import io.spiffe.spiffeid.TrustDomain; +import io.spiffe.svid.x509svid.X509Svid; +import io.spiffe.workloadapi.X509Source; +import lombok.NonNull; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static io.spiffe.utils.TestUtils.toUri; + +public class X509SourceStub implements X509Source { + + private final X509Svid svid; + private final X509Bundle bundle; + + public X509SourceStub() { + try { + Path cert = Paths.get(toUri("testdata/cert.pem")); + Path key = Paths.get(toUri("testdata/key.pem")); + svid = X509Svid.load(cert, key); + bundle = X509Bundle.load( + TrustDomain.of("spiffe://example.org"), + Paths.get(toUri("testdata/bundle.pem"))); + } catch (X509SvidException | URISyntaxException | X509BundleException e) { + throw new RuntimeException(e); + } + } + + @Override + public X509Bundle getBundleForTrustDomain(@NonNull TrustDomain trustDomain) throws BundleNotFoundException { + if (TrustDomain.of("example.org").equals(trustDomain)) { + return bundle; + } + throw new BundleNotFoundException("trustDomain not found"); + } + + @Override + public X509Svid getX509Svid() { + return svid; + } + + @Override + public void close() { + } +} diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsClient.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsClient.java index 8a5b92d..7fd3921 100644 --- a/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsClient.java +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsClient.java @@ -8,7 +8,7 @@ import io.spiffe.provider.SpiffeSslContextFactory.SslContextOptions; import io.spiffe.provider.SpiffeTrustManager; import io.spiffe.spiffeid.SpiffeId; import io.spiffe.spiffeid.SpiffeIdUtils; -import io.spiffe.workloadapi.X509Source; +import io.spiffe.workloadapi.DefaultX509Source; import lombok.val; import javax.net.ssl.SSLContext; @@ -56,11 +56,11 @@ public class HttpsClient { void run() throws IOException, SocketEndpointAddressException, KeyManagementException, NoSuchAlgorithmException, X509SourceException { - val sourceOptions = X509Source.X509SourceOptions + val sourceOptions = DefaultX509Source.X509SourceOptions .builder() .spiffeSocketPath(spiffeSocket) .build(); - val x509Source = X509Source.newSource(sourceOptions); + val x509Source = DefaultX509Source.newSource(sourceOptions); SslContextOptions sslContextOptions = SslContextOptions .builder() diff --git a/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsServer.java b/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsServer.java index 547f160..e896413 100644 --- a/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsServer.java +++ b/java-spiffe-provider/src/test/java/io/spiffe/provider/examples/mtls/HttpsServer.java @@ -3,11 +3,11 @@ package io.spiffe.provider.examples.mtls; import io.spiffe.exception.SocketEndpointAddressException; import io.spiffe.exception.X509SourceException; import io.spiffe.provider.SpiffeKeyManager; -import io.spiffe.provider.SpiffeProviderException; import io.spiffe.provider.SpiffeSslContextFactory; import io.spiffe.provider.SpiffeSslContextFactory.SslContextOptions; import io.spiffe.provider.SpiffeTrustManager; import io.spiffe.provider.X509SourceManager; +import io.spiffe.provider.exception.SpiffeProviderException; import io.spiffe.workloadapi.X509Source; import lombok.val;