Java Spiffe Helper implementation
Refactors Tests README improvements Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
This commit is contained in:
parent
5b27a2fc86
commit
cf761c5bdf
11
README.md
11
README.md
|
|
@ -2,20 +2,21 @@
|
|||
|
||||
## Overview
|
||||
|
||||
The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X509 and JWT SVIDs and Bundles,
|
||||
and a Java Security Provider implementation to be plugged into the Java Security Interface plumbing. This is essentially
|
||||
a X509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
|
||||
The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X.509 and JWT SVIDs and Bundles,
|
||||
and a Java Security Provider implementation to be plugged into the Java Security architecture. This is essentially
|
||||
a X.509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
|
||||
asynchronously from the Workload API. The KeyStore handles the Certificate chain and Private Key to prove identity
|
||||
in a TLS connection, and the TrustStore handles the trusted bundles (supporting federated bundles) and performs
|
||||
peer's certificate and SPIFFE ID verification.
|
||||
|
||||
This library is composed of three modules:
|
||||
|
||||
[java-spiffe-core](java-spiffe-core/README.md): core functionality to interact with the Workload API.
|
||||
[java-spiffe-core](java-spiffe-core/README.md): core functionality to interact with the Workload API, and to process and validate
|
||||
X.509 and JWT SVIDs and bundles.
|
||||
|
||||
[java-spiffe-provider](java-spiffe-provider/README.md): Java Provider implementation.
|
||||
|
||||
[java-spiffe-helper](java-spiffe-helper/README.md): Helper to store X509-SVID Certificates in a Java Keystore in disk.
|
||||
[java-spiffe-helper](java-spiffe-helper/README.md): Helper to store X.509 SVIDs and Bundles in Java Keystores in disk.
|
||||
|
||||
**Supports Java 8+**
|
||||
|
||||
|
|
|
|||
52
build.gradle
52
build.gradle
|
|
@ -1,6 +1,5 @@
|
|||
subprojects {
|
||||
group 'spiffe'
|
||||
version '0.6.0'
|
||||
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'jacoco'
|
||||
|
|
@ -15,27 +14,6 @@ subprojects {
|
|||
test {
|
||||
useJUnitPlatform()
|
||||
finalizedBy jacocoTestReport
|
||||
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test // tests are required to run before generating the report
|
||||
reports {
|
||||
xml.enabled false
|
||||
csv.enabled false
|
||||
html.destination file("${buildDir}/jacocoHtml")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||
fileTree(dir: it, exclude: ['**/internal/**', '**/exception/**'])
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.5"
|
||||
reportsDir = file("$buildDir/customJacocoReportDir")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -54,6 +32,36 @@ subprojects {
|
|||
testCompileOnly 'org.projectlombok:lombok:1.18.12'
|
||||
testAnnotationProcessor 'org.projectlombok:lombok:1.18.12'
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test // tests are required to run before generating the report
|
||||
reports {
|
||||
xml.enabled false
|
||||
csv.enabled false
|
||||
html.destination file("${buildDir}/jacocoHtml")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
classDirectories.setFrom(files(classDirectories.files.collect {
|
||||
fileTree(dir: it, exclude: ['**/grpc/**', '**/exception/**'])
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.5"
|
||||
reportsDir = file("$buildDir/customJacocoReportDir")
|
||||
}
|
||||
|
||||
test {
|
||||
testLogging {
|
||||
afterSuite { desc, result ->
|
||||
if (!desc.parent) { // will match the outermost suite
|
||||
println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,27 @@
|
|||
# JAVA-SPIFFE Core
|
||||
|
||||
Core functionality to fetch X509 and JWT SVIDs from the Workload API.
|
||||
Core functionality to fetch, process and validate X.509 and JWT SVIDs and Bundles from the Workload API.
|
||||
|
||||
## X509 source creation
|
||||
## X.509 Source
|
||||
|
||||
A `spiffe.workloadapi.X509Source` represents a source of X.509 SVIDs and X.509 bundles maintained via the Workload API.
|
||||
|
||||
To create a new X509 Source:
|
||||
|
||||
```
|
||||
X509Source x509Source;
|
||||
try {
|
||||
x509Source = X509Source.newSource();
|
||||
} catch (SocketEndpointAddressException | X509SourceException e) {
|
||||
// handle exception
|
||||
}
|
||||
|
||||
X509Svid svid = x509Source.getX509Svid();
|
||||
X509Bundle bundle = x509Source.getX509BundleForTrustDomain(TrustDomain.of("example.org"));
|
||||
```
|
||||
|
||||
The `newSource()` blocks until the X505 materials can be retrieved from the Workload API and the X509Source is
|
||||
initialized with the SVID and Bundles. A `X509 context watcher` is configured on the X509Source to get automatically
|
||||
The `newSource()` blocks until the X.509 materials can be retrieved from the Workload API and the X509Source is
|
||||
initialized with the X.509 SVIDs and Bundles. A `X509 context watcher` is configured on the X509Source to get automatically
|
||||
the updates from the Workload API. This watcher performs retries if at any time the connection to the Workload API
|
||||
reports an error.
|
||||
|
||||
|
|
@ -49,6 +53,33 @@ using a System property:
|
|||
|
||||
The Time Unit is seconds.
|
||||
|
||||
|
||||
## JWT Source
|
||||
|
||||
A `spiffe.workloadapi.JwtSource` represents a source of JWT SVIDs and bundles maintained via the Workload API.
|
||||
|
||||
To create a new JWT Source:
|
||||
|
||||
```
|
||||
JwtSource jwtSource;
|
||||
try {
|
||||
jwtSource = JwtSource.newSource();
|
||||
} catch (SocketEndpointAddressException | JwtSourceException e) {
|
||||
// handle exception
|
||||
}
|
||||
|
||||
JwtSvid svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/test"), "testaudience1", "audience2");
|
||||
|
||||
JwtBundle bundle = jwtSource.getJwtBundleForTrustDomain(TrustDomain.of("example.org"));
|
||||
```
|
||||
|
||||
The `newSource()` blocks until the JWT materials can be retrieved from the Workload API and the JwtSource is
|
||||
initialized with the JWT Bundles. A `JWT context watcher` is configured on the JwtSource to get automatically
|
||||
the updates from the Workload API. This watcher performs retries if at any time the connection to the Workload API
|
||||
reports an error.
|
||||
|
||||
The socket endpoint address is configured through the environment variable `SPIFFE_ENDPOINT_SOCKET`.
|
||||
|
||||
## Netty Event Loop thread number configuration
|
||||
|
||||
Use the variable `io.netty.eventLoopThreads` to configure the number of threads for the Netty Event Loop Group.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
version '0.6.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ public class Address {
|
|||
* @throws SocketEndpointAddressException if the address could not be parsed or if it is not valid
|
||||
*/
|
||||
public static URI parseAddress(String address) throws SocketEndpointAddressException {
|
||||
|
||||
URI parsedAddress;
|
||||
|
||||
try {
|
||||
parsedAddress = new URI(address);
|
||||
} catch (URISyntaxException e) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
|||
import spiffe.bundle.jwtbundle.JwtBundle;
|
||||
import spiffe.bundle.jwtbundle.JwtBundleSet;
|
||||
import spiffe.bundle.jwtbundle.JwtBundleSource;
|
||||
import spiffe.bundle.x509bundle.X509Bundle;
|
||||
import spiffe.exception.BundleNotFoundException;
|
||||
import spiffe.exception.JwtSourceException;
|
||||
import spiffe.exception.JwtSvidException;
|
||||
|
|
@ -25,6 +26,8 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static spiffe.workloadapi.internal.ThreadUtils.await;
|
||||
|
||||
/**
|
||||
* A <code>JwtSource</code> represents a source of SPIFFE JWT SVID and JWT bundles
|
||||
* maintained via the Workload API.
|
||||
|
|
@ -127,16 +130,64 @@ public class JwtSource implements JwtSvidSource, JwtBundleSource, Closeable {
|
|||
return jwtSource;
|
||||
}
|
||||
|
||||
private void init(Duration timeout) throws InterruptedException, TimeoutException {
|
||||
/**
|
||||
* Returns the JWT SVID handled by this source.
|
||||
*
|
||||
* @return a {@link JwtSvid}
|
||||
* @throws IllegalStateException if the source is closed
|
||||
*/
|
||||
@Override
|
||||
public JwtSvid fetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences) throws JwtSvidException {
|
||||
if (isClosed()) {
|
||||
throw new IllegalStateException("JWT SVID source is closed");
|
||||
}
|
||||
|
||||
return workloadApiClient.fetchJwtSvid(subject, audience, extraAudiences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JWT 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 JwtBundle getJwtBundleForTrustDomain(TrustDomain trustDomain) throws BundleNotFoundException {
|
||||
if (isClosed()) {
|
||||
throw new IllegalStateException("JWT bundle source is closed");
|
||||
}
|
||||
return bundles.getJwtBundleForTrustDomain(trustDomain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this source, dropping the connection to the Workload API.
|
||||
* Other source methods will return an error after close has been called.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed) {
|
||||
synchronized (this) {
|
||||
if (!closed) {
|
||||
workloadApiClient.close();
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void init(Duration timeout) throws TimeoutException {
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
setJwtBundlesWatcher(done);
|
||||
|
||||
boolean success;
|
||||
if (timeout.isZero()) {
|
||||
done.await();
|
||||
await(done);
|
||||
success = true;
|
||||
} else {
|
||||
success = done.await(timeout.getSeconds(), TimeUnit.SECONDS);
|
||||
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
if (!success) {
|
||||
throw new TimeoutException("Timeout waiting for JWT bundles update");
|
||||
|
|
@ -172,39 +223,6 @@ public class JwtSource implements JwtSvidSource, JwtBundleSource, Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtBundle getJwtBundleForTrustDomain(TrustDomain trustDomain) throws BundleNotFoundException {
|
||||
if (isClosed()) {
|
||||
throw new IllegalStateException("JWT bundle source is closed");
|
||||
}
|
||||
return bundles.getJwtBundleForTrustDomain(trustDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtSvid fetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences) throws JwtSvidException {
|
||||
if (isClosed()) {
|
||||
throw new IllegalStateException("JWT SVID source is closed");
|
||||
}
|
||||
|
||||
return workloadApiClient.fetchJwtSvid(subject, audience, extraAudiences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this source, dropping the connection to the Workload API.
|
||||
* Other source methods will return an error after close has been called.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (!closed) {
|
||||
synchronized (this) {
|
||||
if (!closed) {
|
||||
workloadApiClient.close();
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static WorkloadApiClient createClient(@NonNull JwtSourceOptions options) throws SocketEndpointAddressException {
|
||||
val clientOptions = WorkloadApiClient.ClientOptions
|
||||
.builder()
|
||||
|
|
|
|||
|
|
@ -13,9 +13,14 @@ import spiffe.bundle.jwtbundle.JwtBundleSet;
|
|||
import spiffe.exception.*;
|
||||
import spiffe.spiffeid.SpiffeId;
|
||||
import spiffe.svid.jwtsvid.JwtSvid;
|
||||
import spiffe.workloadapi.internal.*;
|
||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
|
||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
|
||||
import spiffe.workloadapi.grpc.Workload;
|
||||
import spiffe.workloadapi.internal.GrpcConversionUtils;
|
||||
import spiffe.workloadapi.internal.GrpcManagedChannelFactory;
|
||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||
import spiffe.workloadapi.retry.BackoffPolicy;
|
||||
import spiffe.workloadapi.retry.RetryHandler;
|
||||
|
||||
|
|
@ -31,9 +36,6 @@ import java.util.concurrent.Executors;
|
|||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static spiffe.workloadapi.internal.Workload.X509SVIDRequest;
|
||||
import static spiffe.workloadapi.internal.Workload.X509SVIDResponse;
|
||||
|
||||
/**
|
||||
* A <code>WorkloadApiClient</code> represents a client to interact with the Workload API.
|
||||
* <p>
|
||||
|
|
@ -61,32 +63,6 @@ public class WorkloadApiClient implements Closeable {
|
|||
|
||||
private boolean closed;
|
||||
|
||||
private WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub,
|
||||
SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
|
||||
ManagedChannelWrapper managedChannel,
|
||||
BackoffPolicy backoffPolicy,
|
||||
ScheduledExecutorService retryExecutor,
|
||||
ExecutorService executorService) {
|
||||
this.workloadApiAsyncStub = workloadApiAsyncStub;
|
||||
this.workloadApiBlockingStub = workloadApiBlockingStub;
|
||||
this.managedChannel = managedChannel;
|
||||
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
|
||||
this.backoffPolicy = backoffPolicy;
|
||||
this.retryExecutor = retryExecutor;
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
// package private constructor, used to inject workloadApi stubs for testing
|
||||
WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub, SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub, ManagedChannelWrapper managedChannel) {
|
||||
this.workloadApiAsyncStub = workloadApiAsyncStub;
|
||||
this.workloadApiBlockingStub = workloadApiBlockingStub;
|
||||
this.backoffPolicy = new BackoffPolicy();
|
||||
this.executorService = Executors.newCachedThreadPool();
|
||||
this.retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
this.cancellableContexts = new ArrayList<>();
|
||||
this.managedChannel = managedChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Workload API client using the default socket endpoint address.
|
||||
*
|
||||
|
|
@ -145,6 +121,33 @@ public class WorkloadApiClient implements Closeable {
|
|||
options.executorService);
|
||||
}
|
||||
|
||||
private WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub,
|
||||
SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
|
||||
ManagedChannelWrapper managedChannel,
|
||||
BackoffPolicy backoffPolicy,
|
||||
ScheduledExecutorService retryExecutor,
|
||||
ExecutorService executorService) {
|
||||
this.workloadApiAsyncStub = workloadApiAsyncStub;
|
||||
this.workloadApiBlockingStub = workloadApiBlockingStub;
|
||||
this.managedChannel = managedChannel;
|
||||
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
|
||||
this.backoffPolicy = backoffPolicy;
|
||||
this.retryExecutor = retryExecutor;
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
public WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub,
|
||||
SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
|
||||
ManagedChannelWrapper managedChannel) {
|
||||
this.workloadApiAsyncStub = workloadApiAsyncStub;
|
||||
this.workloadApiBlockingStub = workloadApiBlockingStub;
|
||||
this.backoffPolicy = new BackoffPolicy();
|
||||
this.executorService = Executors.newCachedThreadPool();
|
||||
this.retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
|
||||
this.managedChannel = managedChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-shot blocking fetch call to get an X.509 context.
|
||||
*
|
||||
|
|
@ -271,12 +274,12 @@ public class WorkloadApiClient implements Closeable {
|
|||
log.log(Level.INFO, "WorkloadAPI client is closed");
|
||||
}
|
||||
|
||||
private StreamObserver<X509SVIDResponse> getX509ContextStreamObserver(Watcher<X509Context> watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) {
|
||||
return new StreamObserver<X509SVIDResponse>() {
|
||||
private StreamObserver<Workload.X509SVIDResponse> getX509ContextStreamObserver(Watcher<X509Context> watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) {
|
||||
return new StreamObserver<Workload.X509SVIDResponse>() {
|
||||
@Override
|
||||
public void onNext(X509SVIDResponse value) {
|
||||
public void onNext(Workload.X509SVIDResponse value) {
|
||||
try {
|
||||
X509Context x509Context = GrpcConversionUtils.toX509Context(value);
|
||||
val x509Context = GrpcConversionUtils.toX509Context(value);
|
||||
validateX509Context(x509Context);
|
||||
watcher.onUpdate(x509Context);
|
||||
retryHandler.reset();
|
||||
|
|
@ -287,6 +290,7 @@ public class WorkloadApiClient implements Closeable {
|
|||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
log.log(Level.SEVERE, "X.509 context observer error", t);
|
||||
handleWatchX509ContextError(t);
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +318,7 @@ public class WorkloadApiClient implements Closeable {
|
|||
@Override
|
||||
public void onNext(Workload.JWTBundlesResponse value) {
|
||||
try {
|
||||
JwtBundleSet jwtBundleSet = GrpcConversionUtils.toBundleSet(value);
|
||||
val jwtBundleSet = GrpcConversionUtils.toBundleSet(value);
|
||||
watcher.onUpdate(jwtBundleSet);
|
||||
retryHandler.reset();
|
||||
} catch (KeyException | JwtBundleException e) {
|
||||
|
|
@ -324,6 +328,7 @@ public class WorkloadApiClient implements Closeable {
|
|||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
log.log(Level.SEVERE, "JWT observer error", t);
|
||||
handleWatchJwtBundleError(t);
|
||||
}
|
||||
|
||||
|
|
@ -357,8 +362,8 @@ public class WorkloadApiClient implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private X509SVIDRequest newX509SvidRequest() {
|
||||
return X509SVIDRequest.newBuilder().build();
|
||||
private Workload.X509SVIDRequest newX509SvidRequest() {
|
||||
return Workload.X509SVIDRequest.newBuilder().build();
|
||||
}
|
||||
|
||||
private Workload.JWTBundlesRequest newJwtBundlesRequest() {
|
||||
|
|
@ -367,7 +372,7 @@ public class WorkloadApiClient implements Closeable {
|
|||
|
||||
private X509Context processX509Context() throws X509ContextException {
|
||||
try {
|
||||
Iterator<X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
|
||||
Iterator<Workload.X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
|
||||
if (x509SVIDResponse.hasNext()) {
|
||||
return GrpcConversionUtils.toX509Context(x509SVIDResponse.next());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import java.util.concurrent.TimeoutException;
|
|||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static spiffe.workloadapi.internal.ThreadUtils.await;
|
||||
|
||||
/**
|
||||
* A <code>X509Source</code> represents a source of X.509 SVIDs and X.509 bundles maintained via the
|
||||
* Workload API.
|
||||
|
|
@ -199,16 +201,16 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
|
|||
return WorkloadApiClient.newClient(clientOptions);
|
||||
}
|
||||
|
||||
private void init(Duration timeout) throws InterruptedException, TimeoutException {
|
||||
private void init(Duration timeout) throws TimeoutException {
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
setX509ContextWatcher(done);
|
||||
|
||||
boolean success;
|
||||
if (timeout.isZero()) {
|
||||
done.await();
|
||||
await(done);
|
||||
success = true;
|
||||
} else {
|
||||
success = done.await(timeout.getSeconds(), TimeUnit.SECONDS);
|
||||
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
if (!success) {
|
||||
throw new TimeoutException("Timeout waiting for X509 Context update");
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import spiffe.spiffeid.SpiffeId;
|
|||
import spiffe.spiffeid.TrustDomain;
|
||||
import spiffe.svid.x509svid.X509Svid;
|
||||
import spiffe.workloadapi.X509Context;
|
||||
import spiffe.workloadapi.grpc.Workload;
|
||||
|
||||
import java.security.KeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package spiffe.workloadapi.internal;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ThreadUtils {
|
||||
|
||||
public static void await(CountDownLatch latch) {
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean await(CountDownLatch latch, long timeout, TimeUnit unit) {
|
||||
try {
|
||||
return latch.await(timeout, unit);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option java_package = "spiffe.workloadapi.internal";
|
||||
option java_package = "spiffe.workloadapi.grpc";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import org.junit.platform.commons.util.StringUtils;
|
|||
import spiffe.exception.JwtSvidException;
|
||||
import spiffe.svid.jwtsvid.JwtSvid;
|
||||
import spiffe.utils.TestUtils;
|
||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
||||
import spiffe.workloadapi.internal.Workload;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIImplBase;
|
||||
import spiffe.workloadapi.grpc.Workload;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
|
@ -24,7 +24,7 @@ import java.nio.file.Paths;
|
|||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
|
||||
class FakeWorkloadApi extends SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIImplBase {
|
||||
class FakeWorkloadApi extends SpiffeWorkloadAPIImplBase {
|
||||
|
||||
final String privateKey = "testdata/workloadapi/svid.key";
|
||||
final String svid = "testdata/workloadapi/svid.pem";
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ import spiffe.exception.SocketEndpointAddressException;
|
|||
import spiffe.spiffeid.SpiffeId;
|
||||
import spiffe.spiffeid.TrustDomain;
|
||||
import spiffe.svid.jwtsvid.JwtSvid;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import spiffe.spiffeid.SpiffeId;
|
|||
import spiffe.spiffeid.TrustDomain;
|
||||
import spiffe.svid.jwtsvid.JwtSvid;
|
||||
import spiffe.utils.TestUtils;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
||||
import spiffe.workloadapi.retry.BackoffPolicy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import spiffe.exception.X509SourceException;
|
|||
import spiffe.spiffeid.SpiffeId;
|
||||
import spiffe.spiffeid.TrustDomain;
|
||||
import spiffe.svid.x509svid.X509Svid;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,101 @@
|
|||
# JAVA-SPIFFE Helper
|
||||
|
||||
Helper to store SPIFFE X509-SVIDs and Bundles in a Java KeyStore file.
|
||||
The JAVA-SPIFFE Helper is a simple utility for fetching X.509 SVID certificates from the SPIFFE Workload API,
|
||||
and storing the Private Key and the chain of certificates in a Java KeyStore in disk, and the trusted bundles (CAs)
|
||||
in a separated TrustStore in disk.
|
||||
|
||||
## Configuration
|
||||
The Helper automatically gets the SVID updates and stores them in the KeyStore and TrustStore.
|
||||
|
||||
TBD
|
||||
## Usage
|
||||
|
||||
`java -jar java-spiffe-helper-0.1.0.jar -c helper.conf`
|
||||
|
||||
(The jar can be found in `build/libs`, after running the gradle build)
|
||||
|
||||
Either `-c` or `--config` should be used to pass the path to the config file.
|
||||
|
||||
## Config file
|
||||
|
||||
```
|
||||
keyStorePath = /tmp/keystore.p12
|
||||
keyStorePass = example123
|
||||
keyPass = pass123
|
||||
|
||||
trustStorePath = /tmp/truststore.p12
|
||||
trustStorePass = otherpass123
|
||||
|
||||
keyStoreType = pkcs12
|
||||
|
||||
keyAlias = spiffe
|
||||
|
||||
spiffeSocketPath = unix:/tmp/agent.sock
|
||||
```
|
||||
|
||||
### Configuration Properties
|
||||
|
||||
|Configuration | Description | Default value |
|
||||
|------------------|--------------------------------------------------------------------------------| ------------- |
|
||||
|`keyStorePath` | Path to the Java KeyStore File for storing the Private Key and chain of certs | none |
|
||||
|`keyStorePass` | Password to protect the Java KeyStore File | none |
|
||||
|`keyPass` | Password to protect the Private Key entry in the KeyStore | none |
|
||||
|`trustStorePath` | Path to the Java TrustStore File for storing the trusted bundles | none |
|
||||
|`trustStorePass` | Password to protect the Java TrustStore File | none |
|
||||
|`keyStoreType` | Java KeyStore Type. (`pkcs12` and `jks` are supported). Case insensitive. | pkcs12 |
|
||||
|`keyAlias` | Alias for the Private Key entry | spiffe |
|
||||
|`spiffeSocketPath`| Path the Workload API | Read from the system variable: SPIFFE_ENDPOINT_SOCKET |
|
||||
|
||||
KeyStore and TrustStore **must** be in separate files. If `keyStorePath` and `trustStorePath` points to the same file, an error
|
||||
is shown
|
||||
.
|
||||
If the store files do not exist, they are created.
|
||||
|
||||
The default and **recommended KeyStore Type** is `PKCS12`. The same type is used for both KeyStore and TrustStore.
|
||||
|
||||
It is **strongly recommended** to set restrictive file permissions for KeyStore file, since it stores a private key:
|
||||
|
||||
`chmod 600 keystore_file_name`
|
||||
|
||||
Make sure that the process running the JAVA-SPIFFE Helper has _write_ permission on the KeyStores files.
|
||||
|
||||
### Debug
|
||||
|
||||
To check that the certs are being stored in the KeyStore:
|
||||
|
||||
`keytool -list -v -keystore keystore.path -storepass example123`
|
||||
|
||||
The ouput should a `PrivateKeyEntry`:
|
||||
|
||||
```
|
||||
Keystore type: PKCS12
|
||||
Keystore provider: SUN
|
||||
|
||||
Your keystore contains 1 entry
|
||||
|
||||
Alias name: spiffe
|
||||
Creation date: Jun 2, 2020
|
||||
Entry type: PrivateKeyEntry
|
||||
|
||||
Owner: O=SPIFFE, C=US
|
||||
Issuer: O=SPIFFE, C=US
|
||||
...
|
||||
```
|
||||
|
||||
In the case of the TrustStore, it should display a `trustedCertEntry`:
|
||||
|
||||
```
|
||||
Keystore type: PKCS12
|
||||
Keystore provider: SUN
|
||||
|
||||
Your keystore contains 1 entry
|
||||
|
||||
Alias name: example.org.0
|
||||
Creation date: Jun 2, 2020
|
||||
Entry type: trustedCertEntry
|
||||
|
||||
Owner: O=SPIFFE, C=US
|
||||
Issuer: O=SPIFFE, C=US
|
||||
...
|
||||
```
|
||||
|
||||
The aliases for the trusted certs are generated using the Trust Domain of the SPIFFE ID in the SAN URI, and adding a
|
||||
correlative number suffix.
|
||||
|
|
@ -1,3 +1,31 @@
|
|||
dependencies {
|
||||
compile(project(":java-spiffe-core"))
|
||||
|
||||
plugins {
|
||||
id "com.github.johnrengelman.shadow" version "5.2.0"
|
||||
}
|
||||
|
||||
version '0.1.0'
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Main-Class': 'spiffe.helper.cli.Runner'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
|
||||
assemble.dependsOn shadowJar
|
||||
|
||||
shadowJar {
|
||||
classifier = ""
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(':java-spiffe-core'))
|
||||
implementation group: 'commons-cli', name: 'commons-cli', version: '1.4'
|
||||
|
||||
// pull grpc libraries for testing
|
||||
testImplementation group: 'io.grpc', name: 'grpc-netty', version: "1.29.0"
|
||||
testImplementation group: 'io.grpc', name: 'grpc-protobuf', version: "1.29.0"
|
||||
testImplementation group: 'io.grpc', name: 'grpc-stub', version: "1.29.0"
|
||||
testImplementation group: 'io.grpc', name: 'grpc-testing', version: "1.29.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
package spiffe.helper;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.java.Log;
|
||||
import lombok.val;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import spiffe.exception.SocketEndpointAddressException;
|
||||
import spiffe.workloadapi.Watcher;
|
||||
import spiffe.workloadapi.WorkloadApiClient;
|
||||
import spiffe.workloadapi.WorkloadApiClient.ClientOptions;
|
||||
import spiffe.workloadapi.X509Context;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStoreException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* A <code>KeyStoreHelper</code> represents a helper for storing X.509 SVIDs and bundles,
|
||||
* that are automatically rotated via the Workload API, in a Java KeyStore in a file in disk.
|
||||
*/
|
||||
@Log
|
||||
public class KeyStoreHelper {
|
||||
|
||||
private final spiffe.helper.KeyStore keyStore;
|
||||
|
||||
private final char[] privateKeyPassword;
|
||||
private final String privateKeyAlias;
|
||||
|
||||
private final String spiffeSocketPath;
|
||||
|
||||
/**
|
||||
* Create an instance of a KeyStoreHelper for fetching X.509 SVIDs and bundles
|
||||
* from a Workload API and store them in a binary Java KeyStore in disk.
|
||||
* <p>
|
||||
* It blocks until the initial update has been received from the Workload API.
|
||||
*
|
||||
* @param keyStoreFilePath path to File storing the KeyStore.
|
||||
* @param keyStoreType the type of keystore. Only JKS and PKCS12 are supported. If it's not provided, PKCS12 is used
|
||||
* See the KeyStore section in the <a href=
|
||||
* "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyStore">
|
||||
* Java Cryptography Architecture Standard Algorithm Name Documentation</a>
|
||||
* for information about standard keystore types.
|
||||
* @param keyStorePassword the password to generate the keystore integrity check
|
||||
* @param privateKeyPassword the password to protect the key
|
||||
* @param privateKeyAlias the alias name
|
||||
* @param spiffeSocketPath optional spiffeSocketPath, if absent uses SPIFFE_ENDPOINT_SOCKET env variable
|
||||
*
|
||||
* @throws SocketEndpointAddressException is the socket endpoint address is not valid
|
||||
* @throws KeyStoreException is the entry cannot be stored in the KeyStore
|
||||
* @throws RuntimeException if there is an error fetching the certificates from the Workload API
|
||||
*/
|
||||
@Builder
|
||||
public KeyStoreHelper(
|
||||
@NonNull final Path keyStoreFilePath,
|
||||
@NonNull final KeyStoreType keyStoreType,
|
||||
@NonNull final char[] keyStorePassword,
|
||||
@NonNull final char[] privateKeyPassword,
|
||||
@NonNull final String privateKeyAlias,
|
||||
@NonNull String spiffeSocketPath)
|
||||
throws SocketEndpointAddressException, KeyStoreException {
|
||||
|
||||
|
||||
this.privateKeyPassword = privateKeyPassword.clone();
|
||||
this.privateKeyAlias = privateKeyAlias;
|
||||
this.spiffeSocketPath = spiffeSocketPath;
|
||||
|
||||
this.keyStore =
|
||||
KeyStore
|
||||
.builder()
|
||||
.keyStoreFilePath(keyStoreFilePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(keyStorePassword)
|
||||
.build();
|
||||
|
||||
setupX509ContextFetcher();
|
||||
}
|
||||
|
||||
private void setupX509ContextFetcher() throws SocketEndpointAddressException {
|
||||
WorkloadApiClient workloadApiClient;
|
||||
|
||||
if (StringUtils.isNotBlank(spiffeSocketPath)) {
|
||||
ClientOptions clientOptions = ClientOptions.builder().spiffeSocketPath(spiffeSocketPath).build();
|
||||
workloadApiClient = WorkloadApiClient.newClient(clientOptions);
|
||||
} else {
|
||||
workloadApiClient = WorkloadApiClient.newClient();
|
||||
}
|
||||
|
||||
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
setX509ContextWatcher(workloadApiClient, countDownLatch);
|
||||
await(countDownLatch);
|
||||
}
|
||||
|
||||
private void setX509ContextWatcher(WorkloadApiClient workloadApiClient, CountDownLatch countDownLatch) {
|
||||
workloadApiClient.watchX509Context(new Watcher<X509Context>() {
|
||||
@Override
|
||||
public void onUpdate(X509Context update) {
|
||||
log.log(Level.INFO, "Received X509Context update");
|
||||
try {
|
||||
storeX509ContextUpdate(update);
|
||||
} catch (KeyStoreException e) {
|
||||
this.onError(e);
|
||||
}
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeX509ContextUpdate(final X509Context update) throws KeyStoreException {
|
||||
val privateKeyEntry = PrivateKeyEntry.builder()
|
||||
.alias(privateKeyAlias)
|
||||
.password(privateKeyPassword)
|
||||
.privateKey(update.getDefaultSvid().getPrivateKey())
|
||||
.certificateChain(update.getDefaultSvid().getChainArray())
|
||||
.build();
|
||||
|
||||
keyStore.storePrivateKey(privateKeyEntry);
|
||||
|
||||
log.log(Level.INFO, "Stored X509Context update");
|
||||
|
||||
// TODO: Store all the Bundles
|
||||
throw new NotImplementedException("Bundle Storing is not implemented");
|
||||
}
|
||||
|
||||
private void await(CountDownLatch countDownLatch) {
|
||||
try {
|
||||
countDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package spiffe.helper;
|
||||
|
||||
/**
|
||||
* KeyStore types supported by the KeyStoreHelper
|
||||
*/
|
||||
public enum KeyStoreType {
|
||||
|
||||
JKS("jks"),
|
||||
PKCS12("pkcs12");
|
||||
|
||||
private final String value;
|
||||
|
||||
KeyStoreType(final String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package spiffe.helper.cli;
|
||||
|
||||
import lombok.extern.java.Log;
|
||||
import lombok.val;
|
||||
import org.apache.commons.cli.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import spiffe.exception.SocketEndpointAddressException;
|
||||
import spiffe.helper.keystore.KeyStoreHelper;
|
||||
import spiffe.helper.keystore.KeyStoreType;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStoreException;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Entry point of the CLI to run the KeyStoreHelper.
|
||||
*/
|
||||
@Log
|
||||
public class Runner {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Properties parameters;
|
||||
String configFilePath = null;
|
||||
try {
|
||||
configFilePath = getCliConfigOption(args);
|
||||
parameters = parseConfigFile(configFilePath);
|
||||
KeyStoreHelper.KeyStoreOptions options = toKeyStoreOptions(parameters);
|
||||
new KeyStoreHelper(options);
|
||||
} catch (IOException e) {
|
||||
log.severe(String.format("Cannot open config file: %s %n %s", configFilePath, e.getMessage()));
|
||||
} catch (KeyStoreException e) {
|
||||
log.severe(String.format("Error storing certs in keystores: %s", e.getMessage()));
|
||||
} catch (SocketEndpointAddressException e) {
|
||||
log.severe(String.format("Workload API address is not valid: %s", e.getMessage()));
|
||||
} catch (ParseException e) {
|
||||
log.severe(String.format( "%s. Use -c, --config <arg>", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
static Properties parseConfigFile(String configFile) throws IOException {
|
||||
Properties prop = new Properties();
|
||||
try (InputStream in = new FileInputStream(configFile)){
|
||||
prop.load(in);
|
||||
}
|
||||
return prop;
|
||||
}
|
||||
|
||||
static String getCliConfigOption(String[] args) throws ParseException {
|
||||
final Options cliOptions = new Options();
|
||||
final Option confOption = new Option("c", "config", true, "config file");
|
||||
confOption.setRequired(true);
|
||||
cliOptions.addOption(confOption);
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
CommandLine cmd = parser.parse(cliOptions, args);
|
||||
return cmd.getOptionValue("config");
|
||||
}
|
||||
|
||||
private static KeyStoreHelper.KeyStoreOptions toKeyStoreOptions(Properties properties) {
|
||||
|
||||
val keyStorePath = getString(properties, "keyStorePath");
|
||||
if (StringUtils.isBlank(keyStorePath)) {
|
||||
throw new IllegalArgumentException("keyStorePath config is missing");
|
||||
}
|
||||
|
||||
val keyStorePass = getString(properties, "keyStorePass");
|
||||
if (StringUtils.isBlank(keyStorePass)) {
|
||||
throw new IllegalArgumentException("keyStorePass config is missing");
|
||||
}
|
||||
|
||||
val keyPass = getString(properties, "keyPass");
|
||||
if (StringUtils.isBlank(keyPass)) {
|
||||
throw new IllegalArgumentException("keyPass config is missing");
|
||||
}
|
||||
|
||||
val trustStorePath = getString(properties, "trustStorePath");
|
||||
if (StringUtils.isBlank(trustStorePath)) {
|
||||
throw new IllegalArgumentException("trustStorePath config is missing");
|
||||
}
|
||||
|
||||
val trustStorePass = getString(properties, "trustStorePass");
|
||||
if (StringUtils.isBlank(trustStorePass)) {
|
||||
throw new IllegalArgumentException("trustStorePass config is missing");
|
||||
}
|
||||
|
||||
val keyAlias = getString(properties, "keyAlias");
|
||||
val spiffeSocketPath = getString(properties, "spiffeSocketPath");
|
||||
|
||||
KeyStoreType keyStoreType = null;
|
||||
val keyStoreTypeProp = properties.get("keyStoreType");
|
||||
if (keyStoreTypeProp != null) {
|
||||
keyStoreType = KeyStoreType.parse(keyStoreTypeProp);
|
||||
}
|
||||
|
||||
return KeyStoreHelper.KeyStoreOptions.builder()
|
||||
.keyStorePath(Paths.get(keyStorePath))
|
||||
.keyStorePass(keyStorePass)
|
||||
.keyPass(keyPass)
|
||||
.trustStorePath(Paths.get(trustStorePath))
|
||||
.trustStorePass(trustStorePass)
|
||||
.keyAlias(keyAlias)
|
||||
.spiffeSocketPath(spiffeSocketPath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String getString(Properties properties, String propName) {
|
||||
final String property = properties.getProperty(propName);
|
||||
if (property == null) {
|
||||
return "";
|
||||
}
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package spiffe.helper;
|
||||
package spiffe.helper.keystore;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
|
@ -6,22 +6,15 @@ import lombok.Value;
|
|||
import java.security.cert.X509Certificate;
|
||||
|
||||
@Value
|
||||
class BundleEntry {
|
||||
class AuthorityEntry {
|
||||
String alias;
|
||||
X509Certificate certificate;
|
||||
|
||||
@Builder
|
||||
BundleEntry(
|
||||
AuthorityEntry(
|
||||
final String alias,
|
||||
final X509Certificate certificate) {
|
||||
this.alias = alias;
|
||||
this.certificate = certificate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BundleEntry{" +
|
||||
"alias='" + alias + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package spiffe.helper;
|
||||
package spiffe.helper.keystore;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NonNull;
|
||||
import lombok.val;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
|
@ -23,38 +24,39 @@ class KeyStore {
|
|||
|
||||
private final Path keyStoreFilePath;
|
||||
private final KeyStoreType keyStoreType;
|
||||
private final char[] keyStorePassword;
|
||||
private final String keyStorePassword;
|
||||
|
||||
private java.security.KeyStore javaKeyStore;
|
||||
private File keyStoreFile;
|
||||
private final java.security.KeyStore javaKeyStore;
|
||||
private final File keyStoreFile;
|
||||
|
||||
@Builder
|
||||
KeyStore(
|
||||
@NonNull final Path keyStoreFilePath,
|
||||
@NonNull final KeyStoreType keyStoreType,
|
||||
@NonNull final char[] keyStorePassword) throws KeyStoreException {
|
||||
@NonNull final String keyStorePassword) throws KeyStoreException {
|
||||
this.keyStoreFilePath = keyStoreFilePath;
|
||||
this.keyStoreType = keyStoreType;
|
||||
this.keyStorePassword = keyStorePassword;
|
||||
setupKeyStore();
|
||||
}
|
||||
|
||||
private void setupKeyStore() throws KeyStoreException {
|
||||
if (StringUtils.isBlank(keyStorePassword)) {
|
||||
throw new IllegalArgumentException("keyStorePassword cannot be blank");
|
||||
}
|
||||
this.keyStorePassword = keyStorePassword;
|
||||
this.keyStoreFile = new File(keyStoreFilePath.toUri());
|
||||
this.javaKeyStore = loadKeyStore(keyStoreFile);
|
||||
}
|
||||
|
||||
|
||||
private java.security.KeyStore loadKeyStore(final File keyStoreFile) throws KeyStoreException {
|
||||
try {
|
||||
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
||||
|
||||
// Initialize KeyStore
|
||||
if (Files.exists(keyStoreFilePath)) {
|
||||
keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword);
|
||||
try (final FileInputStream fileInputStream = new FileInputStream(keyStoreFile)) {
|
||||
keyStore.load(fileInputStream, keyStorePassword.toCharArray());
|
||||
}
|
||||
} else {
|
||||
//create new keyStore
|
||||
keyStore.load(null, keyStorePassword);
|
||||
keyStore.load(null, keyStorePassword.toCharArray());
|
||||
}
|
||||
return keyStore;
|
||||
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
|
||||
|
|
@ -66,36 +68,36 @@ class KeyStore {
|
|||
/**
|
||||
* Store a private key and X.509 certificate chain in a Java KeyStore
|
||||
*
|
||||
* @param privateKeyEntry contains the alias, privateKey, chain, privateKey password
|
||||
* @param keyEntry contains the alias, privateKey, chain, privateKey password
|
||||
*/
|
||||
void storePrivateKey(final PrivateKeyEntry privateKeyEntry) throws KeyStoreException {
|
||||
void storePrivateKeyEntry(final PrivateKeyEntry keyEntry) throws KeyStoreException {
|
||||
// Store PrivateKey Entry in KeyStore
|
||||
javaKeyStore.setKeyEntry(
|
||||
privateKeyEntry.getAlias(),
|
||||
privateKeyEntry.getPrivateKey(),
|
||||
privateKeyEntry.getPassword(),
|
||||
privateKeyEntry.getCertificateChain()
|
||||
keyEntry.getAlias(),
|
||||
keyEntry.getPrivateKey(),
|
||||
keyEntry.getPassword().toCharArray(),
|
||||
keyEntry.getCertificateChain()
|
||||
);
|
||||
|
||||
this.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a Bundle Entry in the KeyStore
|
||||
* Store an Authority Entry in the KeyStore.
|
||||
*/
|
||||
void storeBundleEntry(BundleEntry bundleEntry) throws KeyStoreException {
|
||||
void storeAuthorityEntry(AuthorityEntry authorityEntry) throws KeyStoreException {
|
||||
// Store Bundle Entry in KeyStore
|
||||
this.javaKeyStore.setCertificateEntry(
|
||||
bundleEntry.getAlias(),
|
||||
bundleEntry.getCertificate()
|
||||
authorityEntry.getAlias(),
|
||||
authorityEntry.getCertificate()
|
||||
);
|
||||
this.flush();
|
||||
}
|
||||
|
||||
// Flush KeyStore to disk, to the configured (@see keyStoreFilePath)
|
||||
// Flush KeyStore to disk, to the configured keyStoreFilePath
|
||||
private void flush() throws KeyStoreException {
|
||||
try {
|
||||
javaKeyStore.store(new FileOutputStream(keyStoreFile), keyStorePassword);
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(keyStoreFile)){
|
||||
javaKeyStore.store(fileOutputStream, keyStorePassword.toCharArray());
|
||||
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
|
||||
throw new KeyStoreException(e);
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
package spiffe.helper.keystore;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.java.Log;
|
||||
import lombok.val;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import spiffe.bundle.x509bundle.X509Bundle;
|
||||
import spiffe.exception.SocketEndpointAddressException;
|
||||
import spiffe.spiffeid.TrustDomain;
|
||||
import spiffe.workloadapi.Watcher;
|
||||
import spiffe.workloadapi.WorkloadApiClient;
|
||||
import spiffe.workloadapi.WorkloadApiClient.ClientOptions;
|
||||
import spiffe.workloadapi.X509Context;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* A <code>KeyStoreHelper</code> represents a helper for storing X.509 SVIDs and bundles,
|
||||
* that are automatically rotated via the Workload API, in a Java KeyStore and a TrustStore in files in disk.
|
||||
* <p>
|
||||
* It stores the Private Key along with the chain of certificates in a KeyStore, and the
|
||||
* trusted bundles in a separate KeyStore (TrustStore).
|
||||
* <p>
|
||||
* The underlying workload api client uses a backoff retry policy to reconnect to the Workload API
|
||||
* when the connection is lost.
|
||||
*/
|
||||
@Log
|
||||
public class KeyStoreHelper {
|
||||
|
||||
// case insensitive private key default alias
|
||||
static final String DEFAULT_ALIAS = "spiffe";
|
||||
|
||||
// stores private key and chain of certificates
|
||||
private final KeyStore keyStore;
|
||||
|
||||
// stores trusted bundles
|
||||
private final KeyStore trustStore;
|
||||
|
||||
// password that protects the private key
|
||||
private final String keyPass;
|
||||
|
||||
// alias of the private key entry (case-insensitive)
|
||||
private final String keyAlias;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance of a KeyStoreHelper for fetching X.509 SVIDs and bundles
|
||||
* from a Workload API and store them in a binary Java KeyStore in disk.
|
||||
* <p>
|
||||
* It blocks until the initial update has been received from the Workload API.
|
||||
*
|
||||
* @throws SocketEndpointAddressException is the socket endpoint address is not valid
|
||||
* @throws KeyStoreException is the entry cannot be stored in the KeyStore
|
||||
*/
|
||||
public KeyStoreHelper(@NonNull KeyStoreOptions options) throws SocketEndpointAddressException, KeyStoreException {
|
||||
|
||||
KeyStoreType keyStoreType;
|
||||
if (options.keyStoreType == null) {
|
||||
keyStoreType = KeyStoreType.getDefaultType();
|
||||
} else {
|
||||
keyStoreType = options.keyStoreType;
|
||||
}
|
||||
|
||||
this.keyPass = options.keyPass;
|
||||
|
||||
if (StringUtils.isBlank(options.keyAlias)) {
|
||||
this.keyAlias = DEFAULT_ALIAS;
|
||||
} else {
|
||||
this.keyAlias = options.keyAlias;
|
||||
}
|
||||
|
||||
if (options.keyStorePath.equals(options.trustStorePath)) {
|
||||
throw new KeyStoreException("KeyStore and TrustStore should use different files");
|
||||
}
|
||||
|
||||
this.keyStore = KeyStore.builder()
|
||||
.keyStoreFilePath(options.keyStorePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(options.keyStorePass)
|
||||
.build();
|
||||
|
||||
this.trustStore = KeyStore.builder()
|
||||
.keyStoreFilePath(options.trustStorePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(options.trustStorePass)
|
||||
.build();
|
||||
|
||||
WorkloadApiClient client;
|
||||
if (options.client != null) {
|
||||
client = options.client;
|
||||
} else {
|
||||
client = createNewClient(options.spiffeSocketPath);
|
||||
}
|
||||
|
||||
setX509ContextWatcher(client);
|
||||
}
|
||||
|
||||
private WorkloadApiClient createNewClient(String spiffeSocketPath) throws SocketEndpointAddressException {
|
||||
ClientOptions clientOptions = ClientOptions.builder().spiffeSocketPath(spiffeSocketPath).build();
|
||||
return WorkloadApiClient.newClient(clientOptions);
|
||||
}
|
||||
|
||||
private void setX509ContextWatcher(WorkloadApiClient workloadApiClient) {
|
||||
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
workloadApiClient.watchX509Context(new Watcher<X509Context>() {
|
||||
@Override
|
||||
public void onUpdate(X509Context update) {
|
||||
try {
|
||||
storeX509ContextUpdate(update);
|
||||
} catch (KeyStoreException e) {
|
||||
this.onError(e);
|
||||
}
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
log.log(Level.SEVERE, "Error processing X.509 context update", t);
|
||||
}
|
||||
});
|
||||
|
||||
await(countDownLatch);
|
||||
}
|
||||
|
||||
private void storeX509ContextUpdate(final X509Context update) throws KeyStoreException {
|
||||
val privateKeyEntry = PrivateKeyEntry.builder()
|
||||
.alias(keyAlias)
|
||||
.password(keyPass)
|
||||
.privateKey(update.getDefaultSvid().getPrivateKey())
|
||||
.certificateChain(update.getDefaultSvid().getChainArray())
|
||||
.build();
|
||||
|
||||
keyStore.storePrivateKeyEntry(privateKeyEntry);
|
||||
|
||||
for (Map.Entry<TrustDomain, X509Bundle> entry : update.getX509BundleSet().getBundles().entrySet()) {
|
||||
TrustDomain trustDomain = entry.getKey();
|
||||
X509Bundle bundle = entry.getValue();
|
||||
storeBundle(trustDomain, bundle);
|
||||
}
|
||||
|
||||
log.log(Level.INFO, "Stored X.509 context update in Java KeyStore");
|
||||
}
|
||||
|
||||
private void storeBundle(TrustDomain trustDomain, X509Bundle bundle) throws KeyStoreException {
|
||||
int index = 0;
|
||||
for (X509Certificate certificate : bundle.getX509Authorities()) {
|
||||
final AuthorityEntry authorityEntry = AuthorityEntry.builder()
|
||||
.alias(generateAlias(trustDomain, index))
|
||||
.certificate(certificate)
|
||||
.build();
|
||||
trustStore.storeAuthorityEntry(authorityEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateAlias(TrustDomain trustDomain, int index) {
|
||||
return trustDomain.getName().concat(".").concat(String.valueOf(index));
|
||||
}
|
||||
|
||||
private void await(CountDownLatch latch) {
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a KeyStoreHelper.
|
||||
*/
|
||||
@Data
|
||||
public static class KeyStoreOptions {
|
||||
|
||||
/**
|
||||
* Absolute path to File storing the Key Store. Cannot be null.
|
||||
*/
|
||||
Path keyStorePath;
|
||||
|
||||
/**
|
||||
* Absolute path to File storing the Trust Store. Cannot be null.
|
||||
*/
|
||||
Path trustStorePath;
|
||||
|
||||
/**
|
||||
* The type of keystore. Only JKS and PKCS12 are supported. If it's not provided, PKCS12 is used
|
||||
* See the KeyStore section in the <a href=
|
||||
* "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyStore">
|
||||
* Java Cryptography Architecture Standard Algorithm Name Documentation</a>
|
||||
* for information about standard keystore types.
|
||||
* <p>
|
||||
* The same type is used for both the KeyStore and the TrustStore.
|
||||
*
|
||||
* Optional. Default is PKCS12.
|
||||
*/
|
||||
KeyStoreType keyStoreType;
|
||||
|
||||
/**
|
||||
* The password to generate the keystore integrity check.
|
||||
*/
|
||||
String keyStorePass;
|
||||
|
||||
/**
|
||||
* The password to generate the truststore integrity check.
|
||||
*/
|
||||
String trustStorePass;
|
||||
|
||||
/**
|
||||
* The password to protect the key.
|
||||
*/
|
||||
String keyPass;
|
||||
|
||||
/**
|
||||
* Alias of the keyEntry. Default: spiffe
|
||||
* Note: java keystore aliases are case-insensitive.
|
||||
*/
|
||||
String keyAlias;
|
||||
|
||||
/**
|
||||
* Optional spiffeSocketPath, if absent, SPIFFE_ENDPOINT_SOCKET env variable is used.
|
||||
*/
|
||||
String spiffeSocketPath;
|
||||
|
||||
WorkloadApiClient client;
|
||||
|
||||
@Builder
|
||||
public KeyStoreOptions(@NonNull Path keyStorePath, @NonNull Path trustStorePath, @NonNull String keyStorePass,
|
||||
@NonNull String trustStorePass, @NonNull String keyPass, KeyStoreType keyStoreType,
|
||||
String keyAlias, WorkloadApiClient client, String spiffeSocketPath) {
|
||||
this.keyStorePath = keyStorePath;
|
||||
this.trustStorePath = trustStorePath;
|
||||
this.keyStoreType = keyStoreType;
|
||||
this.keyStorePass = keyStorePass;
|
||||
this.trustStorePass = trustStorePass;
|
||||
this.keyPass = keyPass;
|
||||
this.keyAlias = keyAlias;
|
||||
this.client = client;
|
||||
this.spiffeSocketPath = spiffeSocketPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package spiffe.helper.keystore;
|
||||
|
||||
/**
|
||||
* KeyStore types supported by the KeyStoreHelper
|
||||
*/
|
||||
public enum KeyStoreType {
|
||||
|
||||
JKS("jks"),
|
||||
|
||||
PKCS12("pkcs12");
|
||||
|
||||
private final String value;
|
||||
|
||||
KeyStoreType(final String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static KeyStoreType getDefaultType() {
|
||||
return PKCS12;
|
||||
}
|
||||
|
||||
public static KeyStoreType parse(Object type) {
|
||||
if (String.valueOf(type).equalsIgnoreCase(JKS.value)) {
|
||||
return JKS;
|
||||
} else if (String.valueOf(type).equalsIgnoreCase(PKCS12.value)) {
|
||||
return PKCS12;
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("KeyStore type not supported: %s", type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package spiffe.helper;
|
||||
package spiffe.helper.keystore;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
|
@ -10,25 +10,18 @@ import java.security.cert.X509Certificate;
|
|||
class PrivateKeyEntry {
|
||||
String alias;
|
||||
Key privateKey;
|
||||
char[] password;
|
||||
String password;
|
||||
X509Certificate[] certificateChain;
|
||||
|
||||
@Builder
|
||||
PrivateKeyEntry(
|
||||
final String alias,
|
||||
final Key privateKey,
|
||||
final char[] password,
|
||||
final String password,
|
||||
final X509Certificate[] certificateChain) {
|
||||
this.alias = alias;
|
||||
this.privateKey = privateKey;
|
||||
this.password = password;
|
||||
this.certificateChain = certificateChain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PrivateKeyEntry{" +
|
||||
"alias='" + alias + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
package spiffe.helper;
|
||||
|
||||
import lombok.val;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import spiffe.exception.X509SvidException;
|
||||
import spiffe.internal.CertificateUtils;
|
||||
import spiffe.svid.x509svid.X509Svid;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
public class KeyStoreTest {
|
||||
|
||||
static final String DEFAULT_ALIAS = "Spiffe";
|
||||
|
||||
X509Svid x509Svid;
|
||||
private Path keyStoreFilePath;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws X509SvidException, URISyntaxException {
|
||||
x509Svid = X509Svid
|
||||
.load(
|
||||
Paths.get(toUri("testdata/x509cert.pem")),
|
||||
Paths.get(toUri("testdata/pkcs8key.pem"))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStoreX509Svid_PrivateKey_and_Cert_in_PKCS12_KeyStore() throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
|
||||
keyStoreFilePath = Paths.get("keystore.p12");
|
||||
val keyStoreType = KeyStoreType.PKCS12;
|
||||
val keyStorePassword = "keystore-password".toCharArray();
|
||||
val privateKeyPassword = "privatekey-password".toCharArray();
|
||||
|
||||
val keyStore = KeyStore.builder()
|
||||
.keyStoreFilePath(keyStoreFilePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(keyStorePassword)
|
||||
.build();
|
||||
|
||||
val privateKeyEntry = PrivateKeyEntry.builder()
|
||||
.alias(DEFAULT_ALIAS)
|
||||
.privateKey(x509Svid.getPrivateKey())
|
||||
.certificateChain(x509Svid.getChainArray())
|
||||
.password(privateKeyPassword)
|
||||
.build();
|
||||
|
||||
|
||||
keyStore.storePrivateKey(privateKeyEntry);
|
||||
|
||||
checkEntryWasStored(keyStoreFilePath, keyStorePassword, privateKeyPassword, keyStoreType, DEFAULT_ALIAS);
|
||||
}
|
||||
|
||||
private void checkEntryWasStored(Path keyStoreFilePath,
|
||||
char[] keyStorePassword,
|
||||
char[] privateKeyPassword,
|
||||
KeyStoreType keyStoreType,
|
||||
String alias)
|
||||
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
|
||||
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
||||
|
||||
keyStore.load(new FileInputStream(new File(keyStoreFilePath.toUri())), keyStorePassword);
|
||||
val chain = keyStore.getCertificateChain(alias);
|
||||
val spiffeId = CertificateUtils.getSpiffeId((X509Certificate) chain[0]);
|
||||
val privateKey = (PrivateKey) keyStore.getKey(alias, privateKeyPassword);
|
||||
|
||||
assertEquals(1, chain.length);
|
||||
assertEquals("spiffe://example.org/test", spiffeId.toString());
|
||||
assertNotNull(privateKey);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
try {
|
||||
Files.delete(keyStoreFilePath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private URI toUri(String path) throws URISyntaxException {
|
||||
return getClass().getClassLoader().getResource(path).toURI();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package spiffe.helper.cli;
|
||||
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import spiffe.exception.SocketEndpointAddressException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStoreException;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class RunnerTest {
|
||||
|
||||
@Test
|
||||
void test_Main_KeyStorePathIsMissing() throws KeyStoreException, SocketEndpointAddressException, URISyntaxException {
|
||||
final Path path = Paths.get(toUri("testdata/cli/missing-keystorepath.conf"));
|
||||
try {
|
||||
Runner.main(new String[]{"-c", path.toString()});
|
||||
fail("expected exception: property is missing");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("keyStorePath config is missing", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_Main_KeyStorePassIsMissing() throws KeyStoreException, SocketEndpointAddressException, URISyntaxException {
|
||||
final Path path = Paths.get(toUri("testdata/cli/missing-keystorepass.conf"));
|
||||
try {
|
||||
Runner.main(new String[]{"-c", path.toString()});
|
||||
fail("expected exception: property is missing");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("keyStorePass config is missing", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_Main_KeyPassIsMissing() throws KeyStoreException, SocketEndpointAddressException, URISyntaxException {
|
||||
final Path path = Paths.get(toUri("testdata/cli/missing-keypass.conf"));
|
||||
try {
|
||||
Runner.main(new String[]{"-c", path.toString()});
|
||||
fail("expected exception: property is missing");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("keyPass config is missing", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_Main_TrustStorePathIsMissing() throws KeyStoreException, SocketEndpointAddressException, URISyntaxException {
|
||||
final Path path = Paths.get(toUri("testdata/cli/missing-truststorepath.conf"));
|
||||
try {
|
||||
Runner.main(new String[]{"-c", path.toString()});
|
||||
fail("expected exception: property is missing");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("trustStorePath config is missing", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_Main_TrustStorePassIsMissing() throws KeyStoreException, SocketEndpointAddressException, URISyntaxException {
|
||||
final Path path = Paths.get(toUri("testdata/cli/missing-truststorepass.conf"));
|
||||
try {
|
||||
Runner.main(new String[]{"-c", path.toString()});
|
||||
fail("expected exception: property is missing");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("trustStorePass config is missing", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCliConfigOption_abbreviated() {
|
||||
String option = null;
|
||||
try {
|
||||
option = Runner.getCliConfigOption(new String[]{"-c", "example"});
|
||||
} catch (ParseException e) {
|
||||
fail(e);
|
||||
}
|
||||
assertEquals("example", option);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCliConfigOption() {
|
||||
String option = null;
|
||||
try {
|
||||
option = Runner.getCliConfigOption(new String[]{"--config", "example"});
|
||||
} catch (ParseException e) {
|
||||
fail(e);
|
||||
}
|
||||
assertEquals("example", option);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCliConfigOption_nonExistent() {
|
||||
final String option;
|
||||
try {
|
||||
option = Runner.getCliConfigOption(new String[]{"--unknown", "example"});
|
||||
fail("expected parse exception");
|
||||
} catch (ParseException e) {
|
||||
assertEquals("Unrecognized option: --unknown", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_ParseConfigFile() throws URISyntaxException, IOException {
|
||||
final Path path = Paths.get(toUri("testdata/cli/correct.conf"));
|
||||
final Properties properties = Runner.parseConfigFile(path.toString());
|
||||
|
||||
assertEquals("keystore123.p12", properties.getProperty("keyStorePath"));
|
||||
assertEquals("example123", properties.getProperty("keyStorePass"));
|
||||
assertEquals("pass123", properties.getProperty("keyPass"));
|
||||
assertEquals("truststore123.p12", properties.getProperty("trustStorePath"));
|
||||
assertEquals("otherpass123", properties.getProperty("trustStorePass"));
|
||||
assertEquals("jks", properties.getProperty("keyStoreType"));
|
||||
assertEquals("other_alias", properties.getProperty("keyAlias"));
|
||||
assertEquals("unix:/tmp/agent.sock", properties.getProperty("spiffeSocketPath"));
|
||||
}
|
||||
|
||||
private URI toUri(String path) throws URISyntaxException {
|
||||
return getClass().getClassLoader().getResource(path).toURI();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package spiffe.helper.keystore;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIImplBase;
|
||||
import spiffe.workloadapi.grpc.Workload;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
class FakeWorkloadApi extends SpiffeWorkloadAPIImplBase {
|
||||
|
||||
final String privateKey = "testdata/svid.key";
|
||||
final String svid = "testdata/svid.pem";
|
||||
final String x509Bundle = "testdata/bundle.pem";
|
||||
|
||||
|
||||
// Loads cert, bundle and key from files and generates a X509SVIDResponse.
|
||||
@Override
|
||||
public void fetchX509SVID(Workload.X509SVIDRequest request, StreamObserver<Workload.X509SVIDResponse> responseObserver) {
|
||||
try {
|
||||
Path pathCert = Paths.get(toUri(svid));
|
||||
byte[] svidBytes = Files.readAllBytes(pathCert);
|
||||
|
||||
Path pathKey = Paths.get(toUri(privateKey));
|
||||
byte[] keyBytes = Files.readAllBytes(pathKey);
|
||||
|
||||
Path pathBundle = Paths.get(toUri(x509Bundle));
|
||||
byte[] bundleBytes = Files.readAllBytes(pathBundle);
|
||||
|
||||
Workload.X509SVID svid = Workload.X509SVID
|
||||
.newBuilder()
|
||||
.setSpiffeId("spiffe://example.org/workload-server")
|
||||
.setX509Svid(ByteString.copyFrom(svidBytes))
|
||||
.setX509SvidKey(ByteString.copyFrom(keyBytes))
|
||||
.setBundle(ByteString.copyFrom(bundleBytes))
|
||||
.build();
|
||||
Workload.X509SVIDResponse response = Workload.X509SVIDResponse.newBuilder().addSvids(svid).build();
|
||||
responseObserver.onNext(response);
|
||||
responseObserver.onCompleted();
|
||||
} catch (URISyntaxException | IOException e) {
|
||||
throw new Error("Failed FakeSpiffeWorkloadApiService.fetchX509SVID", e);
|
||||
}
|
||||
}
|
||||
|
||||
private URI toUri(String path) throws URISyntaxException {
|
||||
return getClass().getClassLoader().getResource(path).toURI();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
package spiffe.helper.keystore;
|
||||
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.testing.GrpcCleanupRule;
|
||||
import lombok.val;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.junit.Rule;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import spiffe.exception.SocketEndpointAddressException;
|
||||
import spiffe.internal.CertificateUtils;
|
||||
import spiffe.spiffeid.SpiffeId;
|
||||
import spiffe.workloadapi.WorkloadApiClient;
|
||||
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static spiffe.helper.keystore.KeyStoreHelper.DEFAULT_ALIAS;
|
||||
|
||||
class KeyStoreHelperTest {
|
||||
|
||||
@Rule
|
||||
public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
|
||||
|
||||
private WorkloadApiClient workloadApiClient;
|
||||
private Path keyStoreFilePath;
|
||||
private Path trustStoreFilePath;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
// Generate a unique in-process server name.
|
||||
String serverName = InProcessServerBuilder.generateName();
|
||||
|
||||
// Create a server, add service, start, and register for automatic graceful shutdown.
|
||||
FakeWorkloadApi fakeWorkloadApi = new FakeWorkloadApi();
|
||||
Server server = InProcessServerBuilder.forName(serverName).directExecutor().addService(fakeWorkloadApi).build().start();
|
||||
grpcCleanup.register(server);
|
||||
|
||||
// Create WorkloadApiClient using Stubs that will connect to the fake WorkloadApiService.
|
||||
ManagedChannel inProcessChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
|
||||
grpcCleanup.register(inProcessChannel);
|
||||
|
||||
SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub = SpiffeWorkloadAPIGrpc
|
||||
.newBlockingStub(inProcessChannel)
|
||||
.withInterceptors(new SecurityHeaderInterceptor());
|
||||
|
||||
SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub workloadAPIStub = SpiffeWorkloadAPIGrpc
|
||||
.newStub(inProcessChannel)
|
||||
.withInterceptors(new SecurityHeaderInterceptor());
|
||||
|
||||
workloadApiClient = new WorkloadApiClient(workloadAPIStub, workloadApiBlockingStub, new ManagedChannelWrapper(inProcessChannel));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewHelper_certs_are_stored_successfully() throws KeyStoreException, SocketEndpointAddressException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, IOException {
|
||||
|
||||
val keyStorefileName = RandomStringUtils.randomAlphabetic(10);
|
||||
keyStoreFilePath = Paths.get(keyStorefileName);
|
||||
|
||||
val trustStoreFileName = RandomStringUtils.randomAlphabetic(10);
|
||||
trustStoreFilePath = Paths.get(trustStoreFileName);
|
||||
|
||||
val trustStorePass = "truststore123";
|
||||
val keyStorePass = "keystore123";
|
||||
val keyPass = "keypass123";
|
||||
val alias = "other_alias";
|
||||
val keyStoreType = KeyStoreType.JKS;
|
||||
|
||||
final KeyStoreHelper.KeyStoreOptions options = KeyStoreHelper.KeyStoreOptions
|
||||
.builder()
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePath(keyStoreFilePath)
|
||||
.keyStorePass(keyStorePass)
|
||||
.trustStorePath(trustStoreFilePath)
|
||||
.trustStorePass(trustStorePass)
|
||||
.keyPass(keyPass)
|
||||
.keyAlias(alias)
|
||||
.client(workloadApiClient)
|
||||
.build();
|
||||
|
||||
// run KeyStoreHelper
|
||||
new KeyStoreHelper(options);
|
||||
|
||||
checkPrivateKeyEntry(keyStoreFilePath, keyStorePass, keyPass, keyStoreType, alias);
|
||||
val authority1Alias = "example.org.0";
|
||||
checkBundleEntries(trustStoreFilePath, trustStorePass, keyStoreType, authority1Alias);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewHelper_use_default_type_and_alias() throws KeyStoreException, SocketEndpointAddressException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, IOException {
|
||||
|
||||
val keyStorefileName = RandomStringUtils.randomAlphabetic(10);
|
||||
keyStoreFilePath = Paths.get(keyStorefileName);
|
||||
|
||||
val trustStoreFileName = RandomStringUtils.randomAlphabetic(10);
|
||||
trustStoreFilePath = Paths.get(trustStoreFileName);
|
||||
|
||||
val trustStorePass = "truststore123";
|
||||
val keyStorePass = "keystore123";
|
||||
val keyPass = "keypass123";
|
||||
|
||||
final KeyStoreHelper.KeyStoreOptions options = KeyStoreHelper.KeyStoreOptions
|
||||
.builder()
|
||||
.keyStorePath(keyStoreFilePath)
|
||||
.keyStorePass(keyStorePass)
|
||||
.trustStorePath(trustStoreFilePath)
|
||||
.trustStorePass(trustStorePass)
|
||||
.keyPass(keyPass)
|
||||
.client(workloadApiClient)
|
||||
.build();
|
||||
|
||||
// run KeyStoreHelper
|
||||
new KeyStoreHelper(options);
|
||||
|
||||
checkPrivateKeyEntry(keyStoreFilePath, keyStorePass, keyPass, KeyStoreType.getDefaultType(), DEFAULT_ALIAS);
|
||||
val authority1Alias = "example.org.0";
|
||||
checkBundleEntries(trustStoreFilePath, trustStorePass, KeyStoreType.getDefaultType(), authority1Alias);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewHelper_keyStore_trustStore_same_file_throwsException() throws SocketEndpointAddressException {
|
||||
|
||||
val keyStorefileName = RandomStringUtils.randomAlphabetic(10);
|
||||
keyStoreFilePath = Paths.get(keyStorefileName);
|
||||
|
||||
val trustStorePass = "truststore123";
|
||||
val keyStorePass = "keystore123";
|
||||
val keyPass = "keypass123";
|
||||
val alias = "other_alias";
|
||||
|
||||
final KeyStoreHelper.KeyStoreOptions options = KeyStoreHelper.KeyStoreOptions
|
||||
.builder()
|
||||
.keyStorePath(keyStoreFilePath)
|
||||
.keyStorePass(keyStorePass)
|
||||
.trustStorePath(keyStoreFilePath)
|
||||
.trustStorePass(trustStorePass)
|
||||
.keyPass(keyPass)
|
||||
.keyAlias(alias)
|
||||
.client(workloadApiClient)
|
||||
.build();
|
||||
|
||||
try {
|
||||
new KeyStoreHelper(options);
|
||||
fail("expected exception: KeyStore and TrustStore should use different files");
|
||||
} catch (KeyStoreException e) {
|
||||
assertEquals("KeyStore and TrustStore should use different files", e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
deleteFile(keyStoreFilePath);
|
||||
deleteFile(trustStoreFilePath);
|
||||
workloadApiClient.close();
|
||||
}
|
||||
|
||||
private void checkPrivateKeyEntry(Path keyStoreFilePath,
|
||||
String keyStorePassword,
|
||||
String privateKeyPassword,
|
||||
KeyStoreType keyStoreType,
|
||||
String alias)
|
||||
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
|
||||
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
||||
|
||||
keyStore.load(new FileInputStream(new File(keyStoreFilePath.toUri())), keyStorePassword.toCharArray());
|
||||
val chain = keyStore.getCertificateChain(alias);
|
||||
val spiffeId = CertificateUtils.getSpiffeId((X509Certificate) chain[0]);
|
||||
val privateKey = (PrivateKey) keyStore.getKey(alias, privateKeyPassword.toCharArray());
|
||||
|
||||
assertEquals(1, chain.length);
|
||||
assertEquals("spiffe://example.org/workload-server", spiffeId.toString());
|
||||
assertNotNull(privateKey);
|
||||
}
|
||||
|
||||
private void checkBundleEntries(Path keyStoreFilePath,
|
||||
String keyStorePassword,
|
||||
KeyStoreType keyStoreType,
|
||||
String alias)
|
||||
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
|
||||
|
||||
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
||||
keyStore.load(new FileInputStream(new File(keyStoreFilePath.toUri())), keyStorePassword.toCharArray());
|
||||
val certificate = keyStore.getCertificate(alias);
|
||||
assertNotNull(certificate);
|
||||
|
||||
val spiffeId = CertificateUtils.getSpiffeId((X509Certificate) certificate);
|
||||
assertEquals(SpiffeId.parse("spiffe://example.org"), spiffeId);
|
||||
}
|
||||
|
||||
private void deleteFile(Path file) {
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
package spiffe.helper.keystore;
|
||||
|
||||
import lombok.val;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import spiffe.bundle.x509bundle.X509Bundle;
|
||||
import spiffe.exception.X509SvidException;
|
||||
import spiffe.internal.CertificateUtils;
|
||||
import spiffe.spiffeid.SpiffeId;
|
||||
import spiffe.spiffeid.TrustDomain;
|
||||
import spiffe.svid.x509svid.X509Svid;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class KeyStoreTest {
|
||||
|
||||
static final String ENTRY_ALIAS = "spiffe";
|
||||
|
||||
private X509Svid x509Svid;
|
||||
|
||||
private X509Bundle x509Bundle;
|
||||
private Path keyStoreFilePath;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws X509SvidException, URISyntaxException, IOException, CertificateException {
|
||||
x509Svid = X509Svid.load(
|
||||
Paths.get(toUri("testdata/svid.pem")),
|
||||
Paths.get(toUri("testdata/svid.key")));
|
||||
|
||||
x509Bundle = X509Bundle.load(
|
||||
TrustDomain.of("spiffe://example.org"),
|
||||
Paths.get(toUri("testdata/bundle.pem")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStore_PrivateKey_and_Cert_in_PKCS12_KeyStore() throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
|
||||
val fileName = RandomStringUtils.randomAlphabetic(10);
|
||||
keyStoreFilePath = Paths.get(fileName);
|
||||
|
||||
val keyStoreType = KeyStoreType.getDefaultType();
|
||||
val keyStorePassword = RandomStringUtils.randomAscii(12);
|
||||
val privateKeyPassword = RandomStringUtils.randomAlphanumeric(12);
|
||||
|
||||
val keyStore = KeyStore.builder()
|
||||
.keyStoreFilePath(keyStoreFilePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(keyStorePassword)
|
||||
.build();
|
||||
|
||||
val privateKeyEntry = PrivateKeyEntry.builder()
|
||||
.alias(ENTRY_ALIAS)
|
||||
.privateKey(x509Svid.getPrivateKey())
|
||||
.certificateChain(x509Svid.getChainArray())
|
||||
.password(privateKeyPassword)
|
||||
.build();
|
||||
|
||||
keyStore.storePrivateKeyEntry(privateKeyEntry);
|
||||
|
||||
checkPrivateKeyEntry(keyStoreFilePath, keyStorePassword, privateKeyPassword, keyStoreType, ENTRY_ALIAS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStoreBundle_in_JKS_KeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
|
||||
val fileName = RandomStringUtils.randomAlphabetic(10);
|
||||
val keyStoreType = KeyStoreType.JKS;
|
||||
val keyStorePassword = RandomStringUtils.randomAlphanumeric(12);
|
||||
keyStoreFilePath = Paths.get(fileName);
|
||||
|
||||
val keyStore = KeyStore.builder()
|
||||
.keyStoreFilePath(keyStoreFilePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(keyStorePassword)
|
||||
.build();
|
||||
|
||||
val authority1Alias = x509Bundle.getTrustDomain().getName() + ".1";
|
||||
val authority2Alias = x509Bundle.getTrustDomain().getName() + ".2";
|
||||
val entry1 = AuthorityEntry.builder()
|
||||
.alias(authority1Alias)
|
||||
.certificate(x509Bundle.getX509Authorities().iterator().next())
|
||||
.build();
|
||||
|
||||
val entry2 = AuthorityEntry.builder()
|
||||
.alias(authority2Alias)
|
||||
.certificate(x509Bundle.getX509Authorities().iterator().next())
|
||||
.build();
|
||||
|
||||
keyStore.storeAuthorityEntry(entry1);
|
||||
keyStore.storeAuthorityEntry(entry2);
|
||||
|
||||
checkBundleEntries(keyStoreFilePath, keyStorePassword, keyStoreType, authority1Alias);
|
||||
checkBundleEntries(keyStoreFilePath, keyStorePassword, keyStoreType, authority2Alias);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewKeyStore_from_existing_file() throws UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
|
||||
val fileName = RandomStringUtils.randomAlphabetic(10);
|
||||
keyStoreFilePath = Paths.get(fileName);
|
||||
|
||||
val keyStoreType = KeyStoreType.getDefaultType();
|
||||
val keyStorePassword = RandomStringUtils.randomAscii(12);
|
||||
val privateKeyPassword = RandomStringUtils.randomAlphanumeric(12);
|
||||
|
||||
val keyStore = KeyStore.builder()
|
||||
.keyStoreFilePath(keyStoreFilePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(keyStorePassword)
|
||||
.build();
|
||||
|
||||
val privateKeyEntry = PrivateKeyEntry.builder()
|
||||
.alias(ENTRY_ALIAS)
|
||||
.privateKey(x509Svid.getPrivateKey())
|
||||
.certificateChain(x509Svid.getChainArray())
|
||||
.password(privateKeyPassword)
|
||||
.build();
|
||||
|
||||
keyStore.storePrivateKeyEntry(privateKeyEntry);
|
||||
|
||||
// create a new KeyStore using the same file
|
||||
KeyStore.builder()
|
||||
.keyStoreFilePath(keyStoreFilePath)
|
||||
.keyStoreType(keyStoreType)
|
||||
.keyStorePassword(keyStorePassword)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewKeyStore_nullKeyStorePath_throwsException() throws KeyStoreException {
|
||||
try {
|
||||
KeyStore.builder()
|
||||
.keyStoreFilePath(null)
|
||||
.keyStoreType(KeyStoreType.JKS)
|
||||
.keyStorePassword("keyStorePassword")
|
||||
.build();
|
||||
fail("exception expected");
|
||||
} catch (NullPointerException e) {
|
||||
assertEquals("keyStoreFilePath is marked non-null but is null", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewKeyStore_nullKeyStorePassword_throwsException() throws KeyStoreException {
|
||||
try {
|
||||
KeyStore.builder()
|
||||
.keyStoreFilePath(Paths.get("anypath"))
|
||||
.keyStoreType(KeyStoreType.PKCS12)
|
||||
.keyStorePassword(null)
|
||||
.build();
|
||||
fail("exception expected");
|
||||
} catch (NullPointerException e) {
|
||||
assertEquals("keyStorePassword is marked non-null but is null", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewKeyStore_emptyKeyStorePassword_throwsException() throws KeyStoreException {
|
||||
try {
|
||||
KeyStore.builder()
|
||||
.keyStoreFilePath(Paths.get("anypath"))
|
||||
.keyStoreType(KeyStoreType.PKCS12)
|
||||
.keyStorePassword("")
|
||||
.build();
|
||||
fail("exception expected: keyStorePassword cannot be blank");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("keyStorePassword cannot be blank", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
deleteFile(keyStoreFilePath);
|
||||
}
|
||||
|
||||
private void checkPrivateKeyEntry(Path keyStoreFilePath,
|
||||
String keyStorePassword,
|
||||
String privateKeyPassword,
|
||||
KeyStoreType keyStoreType,
|
||||
String alias)
|
||||
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
|
||||
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
||||
|
||||
keyStore.load(new FileInputStream(new File(keyStoreFilePath.toUri())), keyStorePassword.toCharArray());
|
||||
val chain = keyStore.getCertificateChain(alias);
|
||||
val spiffeId = CertificateUtils.getSpiffeId((X509Certificate) chain[0]);
|
||||
val privateKey = (PrivateKey) keyStore.getKey(alias, privateKeyPassword.toCharArray());
|
||||
|
||||
assertEquals(1, chain.length);
|
||||
assertEquals("spiffe://example.org/workload-server", spiffeId.toString());
|
||||
assertNotNull(privateKey);
|
||||
}
|
||||
|
||||
private void checkBundleEntries(Path keyStoreFilePath,
|
||||
String keyStorePassword,
|
||||
KeyStoreType keyStoreType,
|
||||
String alias)
|
||||
throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
|
||||
|
||||
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
||||
keyStore.load(new FileInputStream(new File(keyStoreFilePath.toUri())), keyStorePassword.toCharArray());
|
||||
val certificate = keyStore.getCertificate(alias);
|
||||
assertNotNull(certificate);
|
||||
|
||||
val spiffeId = CertificateUtils.getSpiffeId((X509Certificate) certificate);
|
||||
assertEquals(SpiffeId.parse("spiffe://example.org"), spiffeId);
|
||||
}
|
||||
|
||||
private URI toUri(String path) throws URISyntaxException {
|
||||
return getClass().getClassLoader().getResource(path).toURI();
|
||||
}
|
||||
|
||||
private void deleteFile(Path filePath) {
|
||||
try {
|
||||
Files.delete(filePath);
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package spiffe.helper.keystore;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class KeyStoreTypeTest {
|
||||
|
||||
@Test
|
||||
void value() {
|
||||
assertEquals("jks", KeyStoreType.JKS.value());
|
||||
assertEquals("pkcs12", KeyStoreType.PKCS12.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDefaultType() {
|
||||
assertEquals(KeyStoreType.PKCS12, KeyStoreType.getDefaultType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseJKS() {
|
||||
final KeyStoreType type = KeyStoreType.parse("jks");
|
||||
assertEquals(KeyStoreType.JKS, type);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePKCS12() {
|
||||
final KeyStoreType type = KeyStoreType.parse("pkcs12");
|
||||
assertEquals(KeyStoreType.PKCS12, type);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseUnknownType() {
|
||||
try {
|
||||
KeyStoreType.parse("other_unknown");
|
||||
fail("expected error: KeyStore type not supported");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("KeyStore type not supported: other_unknown", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjjCCATSgAwIBAgIBADAKBggqhkjOPQQDAjAeMQswCQYDVQQGEwJVUzEPMA0G
|
||||
A1UEChMGU1BJRkZFMB4XDTIwMDUyMDE3MDc1N1oXDTIwMDUyNzE3MDgwN1owHjEL
|
||||
MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTBZMBMGByqGSM49AgEGCCqGSM49
|
||||
AwEHA0IABO3/qXKapLzDi3wgqW8Lkjm35WrJclRr8aN7IF8Px2jeJpV4KG+wdLa7
|
||||
rXSOJH8xCotu9QnQcGo4FuinMsJPlZKjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQEOa83CNDa8BcLL/mU3ep//rxyNjAfBgNV
|
||||
HREEGDAWhhRzcGlmZmU6Ly9leGFtcGxlLm9yZzAKBggqhkjOPQQDAgNIADBFAiBC
|
||||
RTRaKR1nphUMjFcLfopHk+VJgB97yZ8TEZRlNF8vLQIhAJchfcPmlOk9OFiAnSoU
|
||||
th2m6yJcLC3axw94n1fg0qcd
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjjCCATSgAwIBAgIBADAKBggqhkjOPQQDAjAeMQswCQYDVQQGEwJVUzEPMA0G
|
||||
A1UEChMGU1BJRkZFMB4XDTIwMDUyNTExNDEyMVoXDTIwMDYwMTExNDEzMVowHjEL
|
||||
MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTBZMBMGByqGSM49AgEGCCqGSM49
|
||||
AwEHA0IABAED6MJ6JluKjEVjKiOP8gPgcqxdJpQKI7iJLDTTd8Ums1/bXTvUxQXG
|
||||
PmMcqYAtEvTgs1ew/FDSh5L8XNvaghWjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQtAHWFv+CwKHD7G/VNm6oke6CTtTAfBgNV
|
||||
HREEGDAWhhRzcGlmZmU6Ly9leGFtcGxlLm9yZzAKBggqhkjOPQQDAgNIADBFAiAn
|
||||
VJkxslbz+KJMvsenGo9id3FllKxK1edi2gdyQay62gIhANK6B1ExwDYzUOB5KQUH
|
||||
XZg4m88DL41Jn2b6k+fQggVh
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjjCCATSgAwIBAgIBADAKBggqhkjOPQQDAjAeMQswCQYDVQQGEwJVUzEPMA0G
|
||||
A1UEChMGU1BJRkZFMB4XDTIwMDYwMTE2NTAyM1oXDTIwMDYwODE2NTAzM1owHjEL
|
||||
MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTBZMBMGByqGSM49AgEGCCqGSM49
|
||||
AwEHA0IABPRnZa26tZSfHZ3XrrFmX1xCoU7VzoftkcaIfInGWdEYi3PyyBlJnYgY
|
||||
DsirlAz1Ia+zHZyH4784BjfZErRLocejYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQeeQ48k+lNLMAH0aQcdi8Ka6Op3TAfBgNV
|
||||
HREEGDAWhhRzcGlmZmU6Ly9leGFtcGxlLm9yZzAKBggqhkjOPQQDAgNIADBFAiEA
|
||||
6DAO2Yi+zwJrv1awYIlzZ1yJCwGam9MT6kI+lE94Xs8CIDjmFSRHj4Kr1McIvxPH
|
||||
P5gGQxQNbXxm9iIyoZShXdcb
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
keyStorePath = keystore123.p12
|
||||
keyStorePass = example123
|
||||
keyPass = pass123
|
||||
trustStorePath = truststore123.p12
|
||||
trustStorePass = otherpass123
|
||||
keyStoreType = jks
|
||||
keyAlias = other_alias
|
||||
spiffeSocketPath = unix:/tmp/agent.sock
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
keyStorePath = keystore123.p12
|
||||
keyStorePass = example123
|
||||
trustStorePath = truststore123.p12
|
||||
trustStorePass = otherpass123
|
||||
keyStoreType = jks
|
||||
keyAlias = other_alias
|
||||
spiffeSocketPath = unix:/tmp/agent.sock
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
keyStorePath = keystore123.p12
|
||||
keyPass = pass123
|
||||
trustStorePath = truststore123.p12
|
||||
trustStorePass = otherpass123
|
||||
keyStoreType = jks
|
||||
keyAlias = other_alias
|
||||
spiffeSocketPath = unix:/tmp/agent.sock
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
keyStorePass = example123
|
||||
keyPass = pass123
|
||||
trustStorePath = truststore123.p12
|
||||
trustStorePass = otherpass123
|
||||
keyStoreType = jks
|
||||
keyAlias = other_alias
|
||||
spiffeSocketPath = unix:/tmp/agent.sock
|
||||
7
java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepass.conf
vendored
Normal file
7
java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepass.conf
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
keyStorePath = keystore123.p12
|
||||
keyStorePass = example123
|
||||
keyPass = pass123
|
||||
trustStorePath = truststore123.p12
|
||||
keyStoreType = jks
|
||||
keyAlias = other_alias
|
||||
spiffeSocketPath = unix:/tmp/agent.sock
|
||||
7
java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepath.conf
vendored
Normal file
7
java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepath.conf
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
keyStorePath = keystore123.p12
|
||||
keyStorePass = example123
|
||||
keyPass = pass123
|
||||
trustStorePass = otherpass123
|
||||
keyStoreType = jks
|
||||
keyAlias = other_alias
|
||||
spiffeSocketPath = unix:/tmp/agent.sock
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGSpi81I7JqXLgi9O
|
||||
CvwUSIdt7Ep/Ki7iupcHTYAziSWhRANCAATj80Da6Z4r2PVTn56A+p/OiaOemcvP
|
||||
ryM29Q210DQuDoTv+eya+qg1tVVtoPzgmmV+Dr/odZKeg7srsD6hnWD6
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXNOpBRBUIzE4VRvy
|
||||
nfxWNboXxrdVFTPesmpmp8V8D5+hRANCAAR0nJ2niXQw2PL8p97m3OyprAKEIA9R
|
||||
LoH21QBHWxCkQ+gMS8ZPWfqz3io+r78HcYmLgadwqQGEd4ydpK5zOWfE
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIB6jCCAZGgAwIBAgIQEu8Zv0CRj/f7IlVC+MFgjDAKBggqhkjOPQQDAjAeMQsw
|
||||
CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMB4XDTIwMDYwMTE2NTExNloXDTIw
|
||||
MDYwMTE3NTEyNlowHTELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBVNQSVJFMFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAEdJydp4l0MNjy/Kfe5tzsqawChCAPUS6B9tUA
|
||||
R1sQpEPoDEvGT1n6s94qPq+/B3GJi4GncKkBhHeMnaSuczlnxKOBsTCBrjAOBgNV
|
||||
HQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud
|
||||
EwEB/wQCMAAwHQYDVR0OBBYEFLDXoDiQmFWIBeqPukkAhIhbfS2TMB8GA1UdIwQY
|
||||
MBaAFB55DjyT6U0swAfRpBx2Lwpro6ndMC8GA1UdEQQoMCaGJHNwaWZmZTovL2V4
|
||||
YW1wbGUub3JnL3dvcmtsb2FkLXNlcnZlcjAKBggqhkjOPQQDAgNHADBEAiBT2Lzq
|
||||
RDMkptYCU2TK8v9htJ/4/aD3SxSqPglK//ENawIgODWEA/wmjdcO/bGK4pfZIwfN
|
||||
NkaX3AN5M+Gu37K409I=
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICADCCAYagAwIBAgIQJqqLnXOR3tjUV43PpGW4YDAKBggqhkjOPQQDAzAeMQsw
|
||||
CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMB4XDTIwMDMxODE5MDc1MloXDTIw
|
||||
MDMyMzE4MDUwN1owHTELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBVNQSVJFMFkwEwYH
|
||||
KoZIzj0CAQYIKoZIzj0DAQcDQgAE4/NA2umeK9j1U5+egPqfzomjnpnLz68jNvUN
|
||||
tdA0Lg6E7/nsmvqoNbVVbaD84Jplfg6/6HWSnoO7K7A+oZ1g+qOBpjCBozAOBgNV
|
||||
HQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud
|
||||
EwEB/wQCMAAwHQYDVR0OBBYEFPLf59AkFCva6ehKx8L4i+pjCU1CMB8GA1UdIwQY
|
||||
MBaAFCMzt4WGhen9N2N+MwzUkwEtG6YLMCQGA1UdEQQdMBuGGXNwaWZmZTovL2V4
|
||||
YW1wbGUub3JnL3Rlc3QwCgYIKoZIzj0EAwMDaAAwZQIwRzrN6Rh8X28UYJEuql/1
|
||||
GZEeto7zzj0UtjZFwQy2ODl48nFFRGKUnq8mc4cIMI/kAjEAlixJBUHqb4ty8Ff+
|
||||
d0XHzA5duE1hzFxd2feRppjqiOHKJu7Rh2dzZd3rZhMZWrrd
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Java SPIFFE Provider
|
||||
|
||||
This module provides a Java Security Provider implementation supporting X509-SVIDs and methods for
|
||||
This module provides a Java Security Provider implementation supporting X.509-SVIDs and methods for
|
||||
creating SSLContexts that are backed by the Workload API.
|
||||
|
||||
## Create an SSL Context backed by the Workload API
|
||||
|
|
@ -11,11 +11,12 @@ Security property defined in the `java.security` containing the list of SPIFFE I
|
|||
will trust for TLS connections.
|
||||
|
||||
```
|
||||
val sslContextOptions = SslContextOptions
|
||||
SslContextOptions options = SslContextOptions
|
||||
.builder()
|
||||
.x509Source(x509Source.newSource()())
|
||||
.x509Source(x509Source.newSource())
|
||||
.build();
|
||||
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
|
||||
|
||||
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(options);
|
||||
```
|
||||
|
||||
See [HttpsServer example](src/main/java/spiffe/provider/examples/HttpsServer.java).
|
||||
|
|
@ -24,24 +25,25 @@ Alternatively, a different Workload API address can be used by passing it to the
|
|||
Supplier of accepted SPIFFE IDs list can be provided as part of the `SslContextOptions`:
|
||||
|
||||
```
|
||||
val sourceOptions = X509SourceOptions
|
||||
X509SourceOptions sourceOptions = X509SourceOptions
|
||||
.builder()
|
||||
.spiffeSocketPath(spiffeSocket)
|
||||
.build();
|
||||
val x509Source = X509Source.newSource(sourceOptions);
|
||||
X509Source x509Source = X509Source.newSource(sourceOptions);
|
||||
|
||||
SslContextOptions sslContextOptions = SslContextOptions
|
||||
.builder()
|
||||
.acceptedSpiffeIdsSupplier(acceptedSpiffeIdsListSupplier)
|
||||
.x509Source(x509Source())
|
||||
.x509Source(x509Source)
|
||||
.build();
|
||||
|
||||
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
|
||||
```
|
||||
|
||||
See [HttpsClient example](src/test/java/spiffe/provider/examples/mtls/HttpsClient.java) that defines a Supplier for providing
|
||||
the list of SPIFFE IDs from a file.
|
||||
|
||||
## Plug Java SPIFFE Provider into Java Security
|
||||
## Plug Java SPIFFE Provider into Java Security architecture
|
||||
|
||||
Java Security Providers are configured in the master security properties file `<java-home>/jre/lib/security/java.security`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
ext.shadowPluginVersion = '5.2.0'
|
||||
|
||||
dependencies {
|
||||
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: "${shadowPluginVersion}"
|
||||
}
|
||||
plugins {
|
||||
id "com.github.johnrengelman.shadow" version "5.2.0"
|
||||
}
|
||||
|
||||
version '0.6.0'
|
||||
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
|
||||
assemble.dependsOn shadowJar
|
||||
|
||||
shadowJar {
|
||||
classifier = "all"
|
||||
}
|
||||
dependencies {
|
||||
compile(project(":java-spiffe-core"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ public class SpiffeProviderConstants {
|
|||
public static final String ALGORITHM = "Spiffe";
|
||||
|
||||
/**
|
||||
* Alias used by the SpiffeKeyStore
|
||||
* Alias used by the SpiffeKeyStore.
|
||||
* Note: KeyStore aliases are case-insensitive.
|
||||
*/
|
||||
public static final String DEFAULT_ALIAS = "Spiffe";
|
||||
public static final String DEFAULT_ALIAS = "spiffe";
|
||||
|
||||
private SpiffeProviderConstants() {
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue