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
|
## Overview
|
||||||
|
|
||||||
The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X509 and JWT SVIDs and Bundles,
|
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 Interface plumbing. This is essentially
|
and a Java Security Provider implementation to be plugged into the Java Security architecture. This is essentially
|
||||||
a X509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
|
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
|
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
|
in a TLS connection, and the TrustStore handles the trusted bundles (supporting federated bundles) and performs
|
||||||
peer's certificate and SPIFFE ID verification.
|
peer's certificate and SPIFFE ID verification.
|
||||||
|
|
||||||
This library is composed of three modules:
|
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-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+**
|
**Supports Java 8+**
|
||||||
|
|
||||||
|
|
|
||||||
52
build.gradle
52
build.gradle
|
|
@ -1,6 +1,5 @@
|
||||||
subprojects {
|
subprojects {
|
||||||
group 'spiffe'
|
group 'spiffe'
|
||||||
version '0.6.0'
|
|
||||||
|
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
|
|
@ -15,27 +14,6 @@ subprojects {
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
finalizedBy jacocoTestReport
|
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 {
|
dependencies {
|
||||||
|
|
@ -54,6 +32,36 @@ subprojects {
|
||||||
testCompileOnly 'org.projectlombok:lombok:1.18.12'
|
testCompileOnly 'org.projectlombok:lombok:1.18.12'
|
||||||
testAnnotationProcessor '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
|
# 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.
|
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:
|
To create a new X509 Source:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
X509Source x509Source;
|
||||||
try {
|
try {
|
||||||
x509Source = X509Source.newSource();
|
x509Source = X509Source.newSource();
|
||||||
} catch (SocketEndpointAddressException | X509SourceException e) {
|
} catch (SocketEndpointAddressException | X509SourceException e) {
|
||||||
// handle exception
|
// 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
|
The `newSource()` blocks until the X.509 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
|
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
|
the updates from the Workload API. This watcher performs retries if at any time the connection to the Workload API
|
||||||
reports an error.
|
reports an error.
|
||||||
|
|
||||||
|
|
@ -49,6 +53,33 @@ using a System property:
|
||||||
|
|
||||||
The Time Unit is seconds.
|
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
|
## Netty Event Loop thread number configuration
|
||||||
|
|
||||||
Use the variable `io.netty.eventLoopThreads` to configure the number of threads for the Netty Event Loop Group.
|
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 {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ public class Address {
|
||||||
* @throws SocketEndpointAddressException if the address could not be parsed or if it is not valid
|
* @throws SocketEndpointAddressException if the address could not be parsed or if it is not valid
|
||||||
*/
|
*/
|
||||||
public static URI parseAddress(String address) throws SocketEndpointAddressException {
|
public static URI parseAddress(String address) throws SocketEndpointAddressException {
|
||||||
|
|
||||||
URI parsedAddress;
|
URI parsedAddress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedAddress = new URI(address);
|
parsedAddress = new URI(address);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import spiffe.bundle.jwtbundle.JwtBundle;
|
import spiffe.bundle.jwtbundle.JwtBundle;
|
||||||
import spiffe.bundle.jwtbundle.JwtBundleSet;
|
import spiffe.bundle.jwtbundle.JwtBundleSet;
|
||||||
import spiffe.bundle.jwtbundle.JwtBundleSource;
|
import spiffe.bundle.jwtbundle.JwtBundleSource;
|
||||||
|
import spiffe.bundle.x509bundle.X509Bundle;
|
||||||
import spiffe.exception.BundleNotFoundException;
|
import spiffe.exception.BundleNotFoundException;
|
||||||
import spiffe.exception.JwtSourceException;
|
import spiffe.exception.JwtSourceException;
|
||||||
import spiffe.exception.JwtSvidException;
|
import spiffe.exception.JwtSvidException;
|
||||||
|
|
@ -25,6 +26,8 @@ import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.logging.Level;
|
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
|
* A <code>JwtSource</code> represents a source of SPIFFE JWT SVID and JWT bundles
|
||||||
* maintained via the Workload API.
|
* maintained via the Workload API.
|
||||||
|
|
@ -127,16 +130,64 @@ public class JwtSource implements JwtSvidSource, JwtBundleSource, Closeable {
|
||||||
return jwtSource;
|
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);
|
CountDownLatch done = new CountDownLatch(1);
|
||||||
setJwtBundlesWatcher(done);
|
setJwtBundlesWatcher(done);
|
||||||
|
|
||||||
boolean success;
|
boolean success;
|
||||||
if (timeout.isZero()) {
|
if (timeout.isZero()) {
|
||||||
done.await();
|
await(done);
|
||||||
success = true;
|
success = true;
|
||||||
} else {
|
} else {
|
||||||
success = done.await(timeout.getSeconds(), TimeUnit.SECONDS);
|
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new TimeoutException("Timeout waiting for JWT bundles update");
|
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 {
|
private static WorkloadApiClient createClient(@NonNull JwtSourceOptions options) throws SocketEndpointAddressException {
|
||||||
val clientOptions = WorkloadApiClient.ClientOptions
|
val clientOptions = WorkloadApiClient.ClientOptions
|
||||||
.builder()
|
.builder()
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,14 @@ import spiffe.bundle.jwtbundle.JwtBundleSet;
|
||||||
import spiffe.exception.*;
|
import spiffe.exception.*;
|
||||||
import spiffe.spiffeid.SpiffeId;
|
import spiffe.spiffeid.SpiffeId;
|
||||||
import spiffe.svid.jwtsvid.JwtSvid;
|
import spiffe.svid.jwtsvid.JwtSvid;
|
||||||
import spiffe.workloadapi.internal.*;
|
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
|
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
|
||||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
|
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.BackoffPolicy;
|
||||||
import spiffe.workloadapi.retry.RetryHandler;
|
import spiffe.workloadapi.retry.RetryHandler;
|
||||||
|
|
||||||
|
|
@ -31,9 +36,6 @@ import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.logging.Level;
|
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.
|
* A <code>WorkloadApiClient</code> represents a client to interact with the Workload API.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -61,32 +63,6 @@ public class WorkloadApiClient implements Closeable {
|
||||||
|
|
||||||
private boolean closed;
|
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.
|
* Creates a new Workload API client using the default socket endpoint address.
|
||||||
*
|
*
|
||||||
|
|
@ -145,6 +121,33 @@ public class WorkloadApiClient implements Closeable {
|
||||||
options.executorService);
|
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.
|
* 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");
|
log.log(Level.INFO, "WorkloadAPI client is closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
private StreamObserver<X509SVIDResponse> getX509ContextStreamObserver(Watcher<X509Context> watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) {
|
private StreamObserver<Workload.X509SVIDResponse> getX509ContextStreamObserver(Watcher<X509Context> watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) {
|
||||||
return new StreamObserver<X509SVIDResponse>() {
|
return new StreamObserver<Workload.X509SVIDResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onNext(X509SVIDResponse value) {
|
public void onNext(Workload.X509SVIDResponse value) {
|
||||||
try {
|
try {
|
||||||
X509Context x509Context = GrpcConversionUtils.toX509Context(value);
|
val x509Context = GrpcConversionUtils.toX509Context(value);
|
||||||
validateX509Context(x509Context);
|
validateX509Context(x509Context);
|
||||||
watcher.onUpdate(x509Context);
|
watcher.onUpdate(x509Context);
|
||||||
retryHandler.reset();
|
retryHandler.reset();
|
||||||
|
|
@ -287,6 +290,7 @@ public class WorkloadApiClient implements Closeable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable t) {
|
public void onError(Throwable t) {
|
||||||
|
log.log(Level.SEVERE, "X.509 context observer error", t);
|
||||||
handleWatchX509ContextError(t);
|
handleWatchX509ContextError(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,7 +318,7 @@ public class WorkloadApiClient implements Closeable {
|
||||||
@Override
|
@Override
|
||||||
public void onNext(Workload.JWTBundlesResponse value) {
|
public void onNext(Workload.JWTBundlesResponse value) {
|
||||||
try {
|
try {
|
||||||
JwtBundleSet jwtBundleSet = GrpcConversionUtils.toBundleSet(value);
|
val jwtBundleSet = GrpcConversionUtils.toBundleSet(value);
|
||||||
watcher.onUpdate(jwtBundleSet);
|
watcher.onUpdate(jwtBundleSet);
|
||||||
retryHandler.reset();
|
retryHandler.reset();
|
||||||
} catch (KeyException | JwtBundleException e) {
|
} catch (KeyException | JwtBundleException e) {
|
||||||
|
|
@ -324,6 +328,7 @@ public class WorkloadApiClient implements Closeable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable t) {
|
public void onError(Throwable t) {
|
||||||
|
log.log(Level.SEVERE, "JWT observer error", t);
|
||||||
handleWatchJwtBundleError(t);
|
handleWatchJwtBundleError(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,8 +362,8 @@ public class WorkloadApiClient implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private X509SVIDRequest newX509SvidRequest() {
|
private Workload.X509SVIDRequest newX509SvidRequest() {
|
||||||
return X509SVIDRequest.newBuilder().build();
|
return Workload.X509SVIDRequest.newBuilder().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Workload.JWTBundlesRequest newJwtBundlesRequest() {
|
private Workload.JWTBundlesRequest newJwtBundlesRequest() {
|
||||||
|
|
@ -367,7 +372,7 @@ public class WorkloadApiClient implements Closeable {
|
||||||
|
|
||||||
private X509Context processX509Context() throws X509ContextException {
|
private X509Context processX509Context() throws X509ContextException {
|
||||||
try {
|
try {
|
||||||
Iterator<X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
|
Iterator<Workload.X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
|
||||||
if (x509SVIDResponse.hasNext()) {
|
if (x509SVIDResponse.hasNext()) {
|
||||||
return GrpcConversionUtils.toX509Context(x509SVIDResponse.next());
|
return GrpcConversionUtils.toX509Context(x509SVIDResponse.next());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.logging.Level;
|
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
|
* A <code>X509Source</code> represents a source of X.509 SVIDs and X.509 bundles maintained via the
|
||||||
* Workload API.
|
* Workload API.
|
||||||
|
|
@ -199,16 +201,16 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
|
||||||
return WorkloadApiClient.newClient(clientOptions);
|
return WorkloadApiClient.newClient(clientOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init(Duration timeout) throws InterruptedException, TimeoutException {
|
private void init(Duration timeout) throws TimeoutException {
|
||||||
CountDownLatch done = new CountDownLatch(1);
|
CountDownLatch done = new CountDownLatch(1);
|
||||||
setX509ContextWatcher(done);
|
setX509ContextWatcher(done);
|
||||||
|
|
||||||
boolean success;
|
boolean success;
|
||||||
if (timeout.isZero()) {
|
if (timeout.isZero()) {
|
||||||
done.await();
|
await(done);
|
||||||
success = true;
|
success = true;
|
||||||
} else {
|
} else {
|
||||||
success = done.await(timeout.getSeconds(), TimeUnit.SECONDS);
|
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new TimeoutException("Timeout waiting for X509 Context update");
|
throw new TimeoutException("Timeout waiting for X509 Context update");
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import spiffe.spiffeid.SpiffeId;
|
||||||
import spiffe.spiffeid.TrustDomain;
|
import spiffe.spiffeid.TrustDomain;
|
||||||
import spiffe.svid.x509svid.X509Svid;
|
import spiffe.svid.x509svid.X509Svid;
|
||||||
import spiffe.workloadapi.X509Context;
|
import spiffe.workloadapi.X509Context;
|
||||||
|
import spiffe.workloadapi.grpc.Workload;
|
||||||
|
|
||||||
import java.security.KeyException;
|
import java.security.KeyException;
|
||||||
import java.security.cert.CertificateException;
|
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";
|
syntax = "proto3";
|
||||||
|
|
||||||
option java_package = "spiffe.workloadapi.internal";
|
option java_package = "spiffe.workloadapi.grpc";
|
||||||
|
|
||||||
import "google/protobuf/struct.proto";
|
import "google/protobuf/struct.proto";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import org.junit.platform.commons.util.StringUtils;
|
||||||
import spiffe.exception.JwtSvidException;
|
import spiffe.exception.JwtSvidException;
|
||||||
import spiffe.svid.jwtsvid.JwtSvid;
|
import spiffe.svid.jwtsvid.JwtSvid;
|
||||||
import spiffe.utils.TestUtils;
|
import spiffe.utils.TestUtils;
|
||||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIImplBase;
|
||||||
import spiffe.workloadapi.internal.Workload;
|
import spiffe.workloadapi.grpc.Workload;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
@ -24,7 +24,7 @@ import java.nio.file.Paths;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
class FakeWorkloadApi extends SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIImplBase {
|
class FakeWorkloadApi extends SpiffeWorkloadAPIImplBase {
|
||||||
|
|
||||||
final String privateKey = "testdata/workloadapi/svid.key";
|
final String privateKey = "testdata/workloadapi/svid.key";
|
||||||
final String svid = "testdata/workloadapi/svid.pem";
|
final String svid = "testdata/workloadapi/svid.pem";
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ import spiffe.exception.SocketEndpointAddressException;
|
||||||
import spiffe.spiffeid.SpiffeId;
|
import spiffe.spiffeid.SpiffeId;
|
||||||
import spiffe.spiffeid.TrustDomain;
|
import spiffe.spiffeid.TrustDomain;
|
||||||
import spiffe.svid.jwtsvid.JwtSvid;
|
import spiffe.svid.jwtsvid.JwtSvid;
|
||||||
|
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ import spiffe.spiffeid.SpiffeId;
|
||||||
import spiffe.spiffeid.TrustDomain;
|
import spiffe.spiffeid.TrustDomain;
|
||||||
import spiffe.svid.jwtsvid.JwtSvid;
|
import spiffe.svid.jwtsvid.JwtSvid;
|
||||||
import spiffe.utils.TestUtils;
|
import spiffe.utils.TestUtils;
|
||||||
|
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
|
||||||
import spiffe.workloadapi.retry.BackoffPolicy;
|
import spiffe.workloadapi.retry.BackoffPolicy;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ import spiffe.exception.X509SourceException;
|
||||||
import spiffe.spiffeid.SpiffeId;
|
import spiffe.spiffeid.SpiffeId;
|
||||||
import spiffe.spiffeid.TrustDomain;
|
import spiffe.spiffeid.TrustDomain;
|
||||||
import spiffe.svid.x509svid.X509Svid;
|
import spiffe.svid.x509svid.X509Svid;
|
||||||
|
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
|
||||||
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
import spiffe.workloadapi.internal.ManagedChannelWrapper;
|
||||||
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
|
||||||
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,101 @@
|
||||||
# JAVA-SPIFFE Helper
|
# 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.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
@ -6,22 +6,15 @@ import lombok.Value;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
class BundleEntry {
|
class AuthorityEntry {
|
||||||
String alias;
|
String alias;
|
||||||
X509Certificate certificate;
|
X509Certificate certificate;
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
BundleEntry(
|
AuthorityEntry(
|
||||||
final String alias,
|
final String alias,
|
||||||
final X509Certificate certificate) {
|
final X509Certificate certificate) {
|
||||||
this.alias = alias;
|
this.alias = alias;
|
||||||
this.certificate = certificate;
|
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.Builder;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
|
@ -23,38 +24,39 @@ class KeyStore {
|
||||||
|
|
||||||
private final Path keyStoreFilePath;
|
private final Path keyStoreFilePath;
|
||||||
private final KeyStoreType keyStoreType;
|
private final KeyStoreType keyStoreType;
|
||||||
private final char[] keyStorePassword;
|
private final String keyStorePassword;
|
||||||
|
|
||||||
private java.security.KeyStore javaKeyStore;
|
private final java.security.KeyStore javaKeyStore;
|
||||||
private File keyStoreFile;
|
private final File keyStoreFile;
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
KeyStore(
|
KeyStore(
|
||||||
@NonNull final Path keyStoreFilePath,
|
@NonNull final Path keyStoreFilePath,
|
||||||
@NonNull final KeyStoreType keyStoreType,
|
@NonNull final KeyStoreType keyStoreType,
|
||||||
@NonNull final char[] keyStorePassword) throws KeyStoreException {
|
@NonNull final String keyStorePassword) throws KeyStoreException {
|
||||||
this.keyStoreFilePath = keyStoreFilePath;
|
this.keyStoreFilePath = keyStoreFilePath;
|
||||||
this.keyStoreType = keyStoreType;
|
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.keyStoreFile = new File(keyStoreFilePath.toUri());
|
||||||
this.javaKeyStore = loadKeyStore(keyStoreFile);
|
this.javaKeyStore = loadKeyStore(keyStoreFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private java.security.KeyStore loadKeyStore(final File keyStoreFile) throws KeyStoreException {
|
private java.security.KeyStore loadKeyStore(final File keyStoreFile) throws KeyStoreException {
|
||||||
try {
|
try {
|
||||||
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
|
||||||
|
|
||||||
// Initialize KeyStore
|
// Initialize KeyStore
|
||||||
if (Files.exists(keyStoreFilePath)) {
|
if (Files.exists(keyStoreFilePath)) {
|
||||||
keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword);
|
try (final FileInputStream fileInputStream = new FileInputStream(keyStoreFile)) {
|
||||||
|
keyStore.load(fileInputStream, keyStorePassword.toCharArray());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
//create new keyStore
|
//create new keyStore
|
||||||
keyStore.load(null, keyStorePassword);
|
keyStore.load(null, keyStorePassword.toCharArray());
|
||||||
}
|
}
|
||||||
return keyStore;
|
return keyStore;
|
||||||
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
|
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
|
||||||
|
|
@ -66,36 +68,36 @@ class KeyStore {
|
||||||
/**
|
/**
|
||||||
* Store a private key and X.509 certificate chain in a Java 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
|
// Store PrivateKey Entry in KeyStore
|
||||||
javaKeyStore.setKeyEntry(
|
javaKeyStore.setKeyEntry(
|
||||||
privateKeyEntry.getAlias(),
|
keyEntry.getAlias(),
|
||||||
privateKeyEntry.getPrivateKey(),
|
keyEntry.getPrivateKey(),
|
||||||
privateKeyEntry.getPassword(),
|
keyEntry.getPassword().toCharArray(),
|
||||||
privateKeyEntry.getCertificateChain()
|
keyEntry.getCertificateChain()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.flush();
|
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
|
// Store Bundle Entry in KeyStore
|
||||||
this.javaKeyStore.setCertificateEntry(
|
this.javaKeyStore.setCertificateEntry(
|
||||||
bundleEntry.getAlias(),
|
authorityEntry.getAlias(),
|
||||||
bundleEntry.getCertificate()
|
authorityEntry.getCertificate()
|
||||||
);
|
);
|
||||||
this.flush();
|
this.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush KeyStore to disk, to the configured (@see keyStoreFilePath)
|
// Flush KeyStore to disk, to the configured keyStoreFilePath
|
||||||
private void flush() throws KeyStoreException {
|
private void flush() throws KeyStoreException {
|
||||||
try {
|
try (FileOutputStream fileOutputStream = new FileOutputStream(keyStoreFile)){
|
||||||
javaKeyStore.store(new FileOutputStream(keyStoreFile), keyStorePassword);
|
javaKeyStore.store(fileOutputStream, keyStorePassword.toCharArray());
|
||||||
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
|
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
|
||||||
throw new KeyStoreException(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.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
@ -10,25 +10,18 @@ import java.security.cert.X509Certificate;
|
||||||
class PrivateKeyEntry {
|
class PrivateKeyEntry {
|
||||||
String alias;
|
String alias;
|
||||||
Key privateKey;
|
Key privateKey;
|
||||||
char[] password;
|
String password;
|
||||||
X509Certificate[] certificateChain;
|
X509Certificate[] certificateChain;
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
PrivateKeyEntry(
|
PrivateKeyEntry(
|
||||||
final String alias,
|
final String alias,
|
||||||
final Key privateKey,
|
final Key privateKey,
|
||||||
final char[] password,
|
final String password,
|
||||||
final X509Certificate[] certificateChain) {
|
final X509Certificate[] certificateChain) {
|
||||||
this.alias = alias;
|
this.alias = alias;
|
||||||
this.privateKey = privateKey;
|
this.privateKey = privateKey;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.certificateChain = certificateChain;
|
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
|
# 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.
|
creating SSLContexts that are backed by the Workload API.
|
||||||
|
|
||||||
## Create an SSL Context 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.
|
will trust for TLS connections.
|
||||||
|
|
||||||
```
|
```
|
||||||
val sslContextOptions = SslContextOptions
|
SslContextOptions options = SslContextOptions
|
||||||
.builder()
|
.builder()
|
||||||
.x509Source(x509Source.newSource()())
|
.x509Source(x509Source.newSource())
|
||||||
.build();
|
.build();
|
||||||
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
|
|
||||||
|
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(options);
|
||||||
```
|
```
|
||||||
|
|
||||||
See [HttpsServer example](src/main/java/spiffe/provider/examples/HttpsServer.java).
|
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`:
|
Supplier of accepted SPIFFE IDs list can be provided as part of the `SslContextOptions`:
|
||||||
|
|
||||||
```
|
```
|
||||||
val sourceOptions = X509SourceOptions
|
X509SourceOptions sourceOptions = X509SourceOptions
|
||||||
.builder()
|
.builder()
|
||||||
.spiffeSocketPath(spiffeSocket)
|
.spiffeSocketPath(spiffeSocket)
|
||||||
.build();
|
.build();
|
||||||
val x509Source = X509Source.newSource(sourceOptions);
|
X509Source x509Source = X509Source.newSource(sourceOptions);
|
||||||
|
|
||||||
SslContextOptions sslContextOptions = SslContextOptions
|
SslContextOptions sslContextOptions = SslContextOptions
|
||||||
.builder()
|
.builder()
|
||||||
.acceptedSpiffeIdsSupplier(acceptedSpiffeIdsListSupplier)
|
.acceptedSpiffeIdsSupplier(acceptedSpiffeIdsListSupplier)
|
||||||
.x509Source(x509Source())
|
.x509Source(x509Source)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
|
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
|
||||||
```
|
```
|
||||||
|
|
||||||
See [HttpsClient example](src/test/java/spiffe/provider/examples/mtls/HttpsClient.java) that defines a Supplier for providing
|
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.
|
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`.
|
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'
|
plugins {
|
||||||
|
id "com.github.johnrengelman.shadow" version "5.2.0"
|
||||||
dependencies {
|
|
||||||
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: "${shadowPluginVersion}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
version '0.6.0'
|
||||||
|
|
||||||
apply plugin: 'com.github.johnrengelman.shadow'
|
apply plugin: 'com.github.johnrengelman.shadow'
|
||||||
|
|
||||||
assemble.dependsOn shadowJar
|
assemble.dependsOn shadowJar
|
||||||
|
|
||||||
shadowJar {
|
|
||||||
classifier = "all"
|
|
||||||
}
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile(project(":java-spiffe-core"))
|
compile(project(":java-spiffe-core"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@ public class SpiffeProviderConstants {
|
||||||
public static final String ALGORITHM = "Spiffe";
|
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() {
|
private SpiffeProviderConstants() {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue