Refactoring to improve testability.

Add X509Source interface.
Add tests to cover provider module.

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
This commit is contained in:
Max Lambrecht 2020-07-16 15:08:09 -03:00
parent e81a936a96
commit e9df15e44b
52 changed files with 1616 additions and 541 deletions

View File

@ -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

View File

@ -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.

View File

@ -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'
}

View File

@ -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");
}
/**

View File

@ -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");
}
/**

View File

@ -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<X509Bundle> {
* @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<X509Bundle> {
* @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<X509Bundle> {
public void removeX509Authority(@NonNull final X509Certificate x509Authority) {
x509Authorities.remove(x509Authority);
}
private static List<X509Certificate> generateX509Certificates(byte[] bundleBytes) throws X509BundleException {
List<X509Certificate> x509Certificates;
try {
x509Certificates = CertificateUtils.generateCertificates(bundleBytes);
} catch (CertificateParsingException e) {
throw new X509BundleException("Bundle certificates could not be parsed from bundle path", e);
}
return x509Certificates;
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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<X509Certificate> generateCertificates(@NonNull final byte[] input) throws CertificateParsingException {
public static List<X509Certificate> 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<X509Certificate> chain, final Collection<X509Certificate> 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;
}

View File

@ -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.
* <p>
* It handles a {@link X509Svid} and a {@link X509BundleSet} that are updated automatically
* whenever there is an update from the Workload API.
* <p>
* Implements {@link X509SvidSource} and {@link BundleSource}.
* <p>
* 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<List<X509Svid>, X509Svid> picker;
private final WorkloadApiClient workloadApiClient;
private volatile boolean closed;
// private constructor
private DefaultX509Source(final Function<List<X509Svid>, 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.
* <p>
* It uses the default address socket endpoint from the environment variable to get the Workload API address.
* <p>
* 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.
* <p>
* 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.
* <p>
* The {@link WorkloadApiClient} can be provided in the options, if it is not,
* a new client is created.
* <p>
* 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.
* <p>
* 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<X509Context>() {
@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}
* <p>
* <code>spiffeSocketPath</code> Address to the Workload API, if it is not set, the default address will be used.
* <p>
* <code>initTimeout</code> 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.
* <p>
* <code>svidPicker</code> 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.
* <p>
* <code>workloadApiClient</code> 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<List<X509Svid>, X509Svid> svidPicker;
private WorkloadApiClient workloadApiClient;
@Builder
public X509SourceOptions(final String spiffeSocketPath,
final Duration initTimeout,
final Function<List<X509Svid>, X509Svid> svidPicker,
final WorkloadApiClient workloadApiClient) {
this.spiffeSocketPath = spiffeSocketPath;
this.initTimeout = initTimeout;
this.svidPicker = svidPicker;
this.workloadApiClient = workloadApiClient;
}
}
}

View File

@ -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<Workload.X509SVIDResponse> 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);
}
}

View File

@ -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.
* <p>
* It handles a {@link X509Svid} and a {@link X509BundleSet} that are updated automatically
* whenever there is an update from the Workload API.
* <p>
* Implements {@link X509SvidSource} and {@link BundleSource}.
* <p>
* 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<X509Bundle>, 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<List<X509Svid>, X509Svid> picker;
private final WorkloadApiClient workloadApiClient;
private volatile boolean closed;
// private constructor
private X509Source(final Function<List<X509Svid>, 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.
* <p>
* It uses the default address socket endpoint from the environment variable to get the Workload API address.
* <p>
* 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.
* <p>
* 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.
* <p>
* The {@link WorkloadApiClient} can be provided in the options, if it is not,
* a new client is created.
* <p>
* 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.
* <p>
* 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<X509Context>() {
@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}
* <p>
* <code>spiffeSocketPath</code> Address to the Workload API, if it is not set, the default address will be used.
* <p>
* <code>initTimeout</code> 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.
* <p>
* <code>svidPicker</code> 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.
* <p>
* <code>workloadApiClient</code> 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<List<X509Svid>, X509Svid> svidPicker;
private WorkloadApiClient workloadApiClient;
@Builder
public X509SourceOptions(final String spiffeSocketPath,
final Duration initTimeout,
final Function<List<X509Svid>, X509Svid> svidPicker,
final WorkloadApiClient workloadApiClient) {
this.spiffeSocketPath = spiffeSocketPath;
this.initTimeout = initTimeout;
this.svidPicker = svidPicker;
this.workloadApiClient = workloadApiClient;
}
}
public interface X509Source extends X509SvidSource, BundleSource<X509Bundle>, Closeable {
}

View File

@ -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) {

View File

@ -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()
)
);

View File

@ -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());
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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();

View File

@ -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")))
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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")));

View File

@ -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);
}
}

View File

@ -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<String, String> map = (Map<String, String>) 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<String, String>) obj).put(key, value);
}
public static URI toUri(String path) throws URISyntaxException {
return Thread.currentThread().getContextClassLoader().getResource(path).toURI();
}
}

View File

@ -13,4 +13,6 @@ shadowJar {
dependencies {
api(project(":java-spiffe-core"))
testImplementation(testFixtures(project(":java-spiffe-core")))
}

View File

@ -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;
}

View File

@ -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.
* <p>
* The Java Security API will call <code>engineGetKeyManagers()</code> 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) {

View File

@ -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 <code>acceptedSpiffeIdsSupplier</code> is not provided, the Set of accepted SPIFFE IDs
* is read from the Security or System Property <code>ssl.spiffe.accept</code>.
* 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 {
* <p>
* <code>sslProtocol</code> The SSL Protocol. Default: TLSv1.2
* <p>
* <code>x509Source</code> An {@link X509Source} that provides the X.509 materials.
* <code>x509Source</code> An {@link DefaultX509Source} that provides the X.509 materials.
* <p>
* <code>acceptedSpiffeIdsSupplier</code> A supplier of a set of {@link SpiffeId} that will be accepted
* for a secure socket connection.

View File

@ -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();

View File

@ -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.
* <p>
* The Java Security API will call <code>engineGetTrustManagers()</code> 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'.
* <p>

View File

@ -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}.
* <p>
* 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 <code>IllegalStateException</code>.
* If the X509Source cannot be initialized, it will throw a <code>RuntimeException</code>.
* <p>
* 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;
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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<String> 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);
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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<X509Bundle> bundleSource = getX509BundleBundleSource(trustManager);
Supplier<Set<SpiffeId>> 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<X509Bundle> bundleSource = getX509BundleBundleSource(trustManager);
Supplier<Set<SpiffeId>> 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<X509Bundle> getX509BundleBundleSource(SpiffeTrustManager trustManager) throws NoSuchFieldException, IllegalAccessException {
Field bundleField = SpiffeTrustManager.class.getDeclaredField("x509BundleSource");
bundleField.setAccessible(true);
return (BundleSource<X509Bundle>) bundleField.get(trustManager);
}
private Supplier<Set<SpiffeId>> getSetSupplier(SpiffeTrustManager trustManager) throws NoSuchFieldException, IllegalAccessException {
Field supplierField = SpiffeTrustManager.class.getDeclaredField("acceptedSpiffeIdsSupplier");
supplierField.setAccessible(true);
return (Supplier<Set<SpiffeId>>) 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);
}
}

View File

@ -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<X509Bundle> bundleSource;
static X509Bundle x509Bundle;
static X509Svid x509Svid;
static X509Svid otherX509Svid;
static X509Certificate[] chain;
static X509Bundle bundleKnown;
static X509Bundle bundleUnknown;
Set<SpiffeId> 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;
}
};
}
}

View File

@ -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());
}
}
}

View File

@ -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() {
}
}

View File

@ -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()

View File

@ -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;