Java Spiffe Helper implementation

Refactors
Tests
README improvements

Signed-off-by: Max Lambrecht <maxlambrecht@gmail.com>
This commit is contained in:
Max Lambrecht 2020-06-04 10:19:19 -03:00
parent 5b27a2fc86
commit cf761c5bdf
45 changed files with 1553 additions and 469 deletions

View File

@ -2,20 +2,21 @@
## Overview
The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X509 and JWT SVIDs and Bundles,
and a Java Security Provider implementation to be plugged into the Java Security Interface plumbing. This is essentially
a X509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X.509 and JWT SVIDs and Bundles,
and a Java Security Provider implementation to be plugged into the Java Security architecture. This is essentially
a X.509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
asynchronously from the Workload API. The KeyStore handles the Certificate chain and Private Key to prove identity
in a TLS connection, and the TrustStore handles the trusted bundles (supporting federated bundles) and performs
peer's certificate and SPIFFE ID verification.
This library is composed of three modules:
[java-spiffe-core](java-spiffe-core/README.md): core functionality to interact with the Workload API.
[java-spiffe-core](java-spiffe-core/README.md): core functionality to interact with the Workload API, and to process and validate
X.509 and JWT SVIDs and bundles.
[java-spiffe-provider](java-spiffe-provider/README.md): Java Provider implementation.
[java-spiffe-helper](java-spiffe-helper/README.md): Helper to store X509-SVID Certificates in a Java Keystore in disk.
[java-spiffe-helper](java-spiffe-helper/README.md): Helper to store X.509 SVIDs and Bundles in Java Keystores in disk.
**Supports Java 8+**

View File

@ -1,6 +1,5 @@
subprojects {
group 'spiffe'
version '0.6.0'
apply plugin: 'java-library'
apply plugin: 'jacoco'
@ -15,27 +14,6 @@ subprojects {
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test // tests are required to run before generating the report
reports {
xml.enabled false
csv.enabled false
html.destination file("${buildDir}/jacocoHtml")
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: ['**/internal/**', '**/exception/**'])
}))
}
}
jacoco {
toolVersion = "0.8.5"
reportsDir = file("$buildDir/customJacocoReportDir")
}
dependencies {
@ -54,6 +32,36 @@ subprojects {
testCompileOnly 'org.projectlombok:lombok:1.18.12'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.12'
}
jacocoTestReport {
dependsOn test // tests are required to run before generating the report
reports {
xml.enabled false
csv.enabled false
html.destination file("${buildDir}/jacocoHtml")
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: ['**/grpc/**', '**/exception/**'])
}))
}
}
jacoco {
toolVersion = "0.8.5"
reportsDir = file("$buildDir/customJacocoReportDir")
}
test {
testLogging {
afterSuite { desc, result ->
if (!desc.parent) { // will match the outermost suite
println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
}
}
}
}
}

View File

@ -1,23 +1,27 @@
# JAVA-SPIFFE Core
Core functionality to fetch X509 and JWT SVIDs from the Workload API.
Core functionality to fetch, process and validate X.509 and JWT SVIDs and Bundles from the Workload API.
## X509 source creation
## X.509 Source
A `spiffe.workloadapi.X509Source` represents a source of X.509 SVIDs and X.509 bundles maintained via the Workload API.
To create a new X509 Source:
```
X509Source x509Source;
try {
x509Source = X509Source.newSource();
} catch (SocketEndpointAddressException | X509SourceException e) {
// handle exception
}
X509Svid svid = x509Source.getX509Svid();
X509Bundle bundle = x509Source.getX509BundleForTrustDomain(TrustDomain.of("example.org"));
```
The `newSource()` blocks until the X505 materials can be retrieved from the Workload API and the X509Source is
initialized with the SVID and Bundles. A `X509 context watcher` is configured on the X509Source to get automatically
The `newSource()` blocks until the X.509 materials can be retrieved from the Workload API and the X509Source is
initialized with the X.509 SVIDs and Bundles. A `X509 context watcher` is configured on the X509Source to get automatically
the updates from the Workload API. This watcher performs retries if at any time the connection to the Workload API
reports an error.
@ -49,6 +53,33 @@ using a System property:
The Time Unit is seconds.
## JWT Source
A `spiffe.workloadapi.JwtSource` represents a source of JWT SVIDs and bundles maintained via the Workload API.
To create a new JWT Source:
```
JwtSource jwtSource;
try {
jwtSource = JwtSource.newSource();
} catch (SocketEndpointAddressException | JwtSourceException e) {
// handle exception
}
JwtSvid svid = jwtSource.fetchJwtSvid(SpiffeId.parse("spiffe://example.org/test"), "testaudience1", "audience2");
JwtBundle bundle = jwtSource.getJwtBundleForTrustDomain(TrustDomain.of("example.org"));
```
The `newSource()` blocks until the JWT materials can be retrieved from the Workload API and the JwtSource is
initialized with the JWT Bundles. A `JWT context watcher` is configured on the JwtSource to get automatically
the updates from the Workload API. This watcher performs retries if at any time the connection to the Workload API
reports an error.
The socket endpoint address is configured through the environment variable `SPIFFE_ENDPOINT_SOCKET`.
## Netty Event Loop thread number configuration
Use the variable `io.netty.eventLoopThreads` to configure the number of threads for the Netty Event Loop Group.

View File

@ -1,3 +1,4 @@
version '0.6.0'
buildscript {
repositories {

View File

@ -49,7 +49,9 @@ public class Address {
* @throws SocketEndpointAddressException if the address could not be parsed or if it is not valid
*/
public static URI parseAddress(String address) throws SocketEndpointAddressException {
URI parsedAddress;
try {
parsedAddress = new URI(address);
} catch (URISyntaxException e) {

View File

@ -9,6 +9,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import spiffe.bundle.jwtbundle.JwtBundle;
import spiffe.bundle.jwtbundle.JwtBundleSet;
import spiffe.bundle.jwtbundle.JwtBundleSource;
import spiffe.bundle.x509bundle.X509Bundle;
import spiffe.exception.BundleNotFoundException;
import spiffe.exception.JwtSourceException;
import spiffe.exception.JwtSvidException;
@ -25,6 +26,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import static spiffe.workloadapi.internal.ThreadUtils.await;
/**
* A <code>JwtSource</code> represents a source of SPIFFE JWT SVID and JWT bundles
* maintained via the Workload API.
@ -127,16 +130,64 @@ public class JwtSource implements JwtSvidSource, JwtBundleSource, Closeable {
return jwtSource;
}
private void init(Duration timeout) throws InterruptedException, TimeoutException {
/**
* Returns the JWT SVID handled by this source.
*
* @return a {@link JwtSvid}
* @throws IllegalStateException if the source is closed
*/
@Override
public JwtSvid fetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences) throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return workloadApiClient.fetchJwtSvid(subject, audience, extraAudiences);
}
/**
* Returns the JWT bundle for a given trust domain.
*
* @return an instance of a {@link X509Bundle}
*
* @throws BundleNotFoundException is there is no bundle for the trust domain provided
* @throws IllegalStateException if the source is closed
*/
@Override
public JwtBundle getJwtBundleForTrustDomain(TrustDomain trustDomain) throws BundleNotFoundException {
if (isClosed()) {
throw new IllegalStateException("JWT bundle source is closed");
}
return bundles.getJwtBundleForTrustDomain(trustDomain);
}
/**
* Closes this source, dropping the connection to the Workload API.
* Other source methods will return an error after close has been called.
*/
@Override
public void close() {
if (!closed) {
synchronized (this) {
if (!closed) {
workloadApiClient.close();
closed = true;
}
}
}
}
private void init(Duration timeout) throws TimeoutException {
CountDownLatch done = new CountDownLatch(1);
setJwtBundlesWatcher(done);
boolean success;
if (timeout.isZero()) {
done.await();
await(done);
success = true;
} else {
success = done.await(timeout.getSeconds(), TimeUnit.SECONDS);
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
}
if (!success) {
throw new TimeoutException("Timeout waiting for JWT bundles update");
@ -172,39 +223,6 @@ public class JwtSource implements JwtSvidSource, JwtBundleSource, Closeable {
}
}
@Override
public JwtBundle getJwtBundleForTrustDomain(TrustDomain trustDomain) throws BundleNotFoundException {
if (isClosed()) {
throw new IllegalStateException("JWT bundle source is closed");
}
return bundles.getJwtBundleForTrustDomain(trustDomain);
}
@Override
public JwtSvid fetchJwtSvid(SpiffeId subject, String audience, String... extraAudiences) throws JwtSvidException {
if (isClosed()) {
throw new IllegalStateException("JWT SVID source is closed");
}
return workloadApiClient.fetchJwtSvid(subject, audience, extraAudiences);
}
/**
* Closes this source, dropping the connection to the Workload API.
* Other source methods will return an error after close has been called.
*/
@Override
public void close() {
if (!closed) {
synchronized (this) {
if (!closed) {
workloadApiClient.close();
closed = true;
}
}
}
}
private static WorkloadApiClient createClient(@NonNull JwtSourceOptions options) throws SocketEndpointAddressException {
val clientOptions = WorkloadApiClient.ClientOptions
.builder()

View File

@ -13,9 +13,14 @@ import spiffe.bundle.jwtbundle.JwtBundleSet;
import spiffe.exception.*;
import spiffe.spiffeid.SpiffeId;
import spiffe.svid.jwtsvid.JwtSvid;
import spiffe.workloadapi.internal.*;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIBlockingStub;
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIStub;
import spiffe.workloadapi.grpc.Workload;
import spiffe.workloadapi.internal.GrpcConversionUtils;
import spiffe.workloadapi.internal.GrpcManagedChannelFactory;
import spiffe.workloadapi.internal.ManagedChannelWrapper;
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
import spiffe.workloadapi.retry.BackoffPolicy;
import spiffe.workloadapi.retry.RetryHandler;
@ -31,9 +36,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import static spiffe.workloadapi.internal.Workload.X509SVIDRequest;
import static spiffe.workloadapi.internal.Workload.X509SVIDResponse;
/**
* A <code>WorkloadApiClient</code> represents a client to interact with the Workload API.
* <p>
@ -61,32 +63,6 @@ public class WorkloadApiClient implements Closeable {
private boolean closed;
private WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub,
SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
ManagedChannelWrapper managedChannel,
BackoffPolicy backoffPolicy,
ScheduledExecutorService retryExecutor,
ExecutorService executorService) {
this.workloadApiAsyncStub = workloadApiAsyncStub;
this.workloadApiBlockingStub = workloadApiBlockingStub;
this.managedChannel = managedChannel;
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
this.backoffPolicy = backoffPolicy;
this.retryExecutor = retryExecutor;
this.executorService = executorService;
}
// package private constructor, used to inject workloadApi stubs for testing
WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub, SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub, ManagedChannelWrapper managedChannel) {
this.workloadApiAsyncStub = workloadApiAsyncStub;
this.workloadApiBlockingStub = workloadApiBlockingStub;
this.backoffPolicy = new BackoffPolicy();
this.executorService = Executors.newCachedThreadPool();
this.retryExecutor = Executors.newSingleThreadScheduledExecutor();
this.cancellableContexts = new ArrayList<>();
this.managedChannel = managedChannel;
}
/**
* Creates a new Workload API client using the default socket endpoint address.
*
@ -145,6 +121,33 @@ public class WorkloadApiClient implements Closeable {
options.executorService);
}
private WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub,
SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
ManagedChannelWrapper managedChannel,
BackoffPolicy backoffPolicy,
ScheduledExecutorService retryExecutor,
ExecutorService executorService) {
this.workloadApiAsyncStub = workloadApiAsyncStub;
this.workloadApiBlockingStub = workloadApiBlockingStub;
this.managedChannel = managedChannel;
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
this.backoffPolicy = backoffPolicy;
this.retryExecutor = retryExecutor;
this.executorService = executorService;
}
public WorkloadApiClient(SpiffeWorkloadAPIStub workloadApiAsyncStub,
SpiffeWorkloadAPIBlockingStub workloadApiBlockingStub,
ManagedChannelWrapper managedChannel) {
this.workloadApiAsyncStub = workloadApiAsyncStub;
this.workloadApiBlockingStub = workloadApiBlockingStub;
this.backoffPolicy = new BackoffPolicy();
this.executorService = Executors.newCachedThreadPool();
this.retryExecutor = Executors.newSingleThreadScheduledExecutor();
this.cancellableContexts = Collections.synchronizedList(new ArrayList<>());
this.managedChannel = managedChannel;
}
/**
* One-shot blocking fetch call to get an X.509 context.
*
@ -271,12 +274,12 @@ public class WorkloadApiClient implements Closeable {
log.log(Level.INFO, "WorkloadAPI client is closed");
}
private StreamObserver<X509SVIDResponse> getX509ContextStreamObserver(Watcher<X509Context> watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) {
return new StreamObserver<X509SVIDResponse>() {
private StreamObserver<Workload.X509SVIDResponse> getX509ContextStreamObserver(Watcher<X509Context> watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) {
return new StreamObserver<Workload.X509SVIDResponse>() {
@Override
public void onNext(X509SVIDResponse value) {
public void onNext(Workload.X509SVIDResponse value) {
try {
X509Context x509Context = GrpcConversionUtils.toX509Context(value);
val x509Context = GrpcConversionUtils.toX509Context(value);
validateX509Context(x509Context);
watcher.onUpdate(x509Context);
retryHandler.reset();
@ -287,6 +290,7 @@ public class WorkloadApiClient implements Closeable {
@Override
public void onError(Throwable t) {
log.log(Level.SEVERE, "X.509 context observer error", t);
handleWatchX509ContextError(t);
}
@ -314,7 +318,7 @@ public class WorkloadApiClient implements Closeable {
@Override
public void onNext(Workload.JWTBundlesResponse value) {
try {
JwtBundleSet jwtBundleSet = GrpcConversionUtils.toBundleSet(value);
val jwtBundleSet = GrpcConversionUtils.toBundleSet(value);
watcher.onUpdate(jwtBundleSet);
retryHandler.reset();
} catch (KeyException | JwtBundleException e) {
@ -324,6 +328,7 @@ public class WorkloadApiClient implements Closeable {
@Override
public void onError(Throwable t) {
log.log(Level.SEVERE, "JWT observer error", t);
handleWatchJwtBundleError(t);
}
@ -357,8 +362,8 @@ public class WorkloadApiClient implements Closeable {
}
}
private X509SVIDRequest newX509SvidRequest() {
return X509SVIDRequest.newBuilder().build();
private Workload.X509SVIDRequest newX509SvidRequest() {
return Workload.X509SVIDRequest.newBuilder().build();
}
private Workload.JWTBundlesRequest newJwtBundlesRequest() {
@ -367,7 +372,7 @@ public class WorkloadApiClient implements Closeable {
private X509Context processX509Context() throws X509ContextException {
try {
Iterator<X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
Iterator<Workload.X509SVIDResponse> x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest());
if (x509SVIDResponse.hasNext()) {
return GrpcConversionUtils.toX509Context(x509SVIDResponse.next());
}

View File

@ -25,6 +25,8 @@ import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.logging.Level;
import static spiffe.workloadapi.internal.ThreadUtils.await;
/**
* A <code>X509Source</code> represents a source of X.509 SVIDs and X.509 bundles maintained via the
* Workload API.
@ -199,16 +201,16 @@ public class X509Source implements X509SvidSource, X509BundleSource, Closeable {
return WorkloadApiClient.newClient(clientOptions);
}
private void init(Duration timeout) throws InterruptedException, TimeoutException {
private void init(Duration timeout) throws TimeoutException {
CountDownLatch done = new CountDownLatch(1);
setX509ContextWatcher(done);
boolean success;
if (timeout.isZero()) {
done.await();
await(done);
success = true;
} else {
success = done.await(timeout.getSeconds(), TimeUnit.SECONDS);
success = await(done, timeout.getSeconds(), TimeUnit.SECONDS);
}
if (!success) {
throw new TimeoutException("Timeout waiting for X509 Context update");

View File

@ -12,6 +12,7 @@ import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.x509svid.X509Svid;
import spiffe.workloadapi.X509Context;
import spiffe.workloadapi.grpc.Workload;
import java.security.KeyException;
import java.security.cert.CertificateException;

View File

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

View File

@ -1,6 +1,6 @@
syntax = "proto3";
option java_package = "spiffe.workloadapi.internal";
option java_package = "spiffe.workloadapi.grpc";
import "google/protobuf/struct.proto";

View File

@ -12,8 +12,8 @@ import org.junit.platform.commons.util.StringUtils;
import spiffe.exception.JwtSvidException;
import spiffe.svid.jwtsvid.JwtSvid;
import spiffe.utils.TestUtils;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
import spiffe.workloadapi.internal.Workload;
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIImplBase;
import spiffe.workloadapi.grpc.Workload;
import java.io.IOException;
import java.net.URI;
@ -24,7 +24,7 @@ import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.*;
class FakeWorkloadApi extends SpiffeWorkloadAPIGrpc.SpiffeWorkloadAPIImplBase {
class FakeWorkloadApi extends SpiffeWorkloadAPIImplBase {
final String privateKey = "testdata/workloadapi/svid.key";
final String svid = "testdata/workloadapi/svid.pem";

View File

@ -17,9 +17,9 @@ import spiffe.exception.SocketEndpointAddressException;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.jwtsvid.JwtSvid;
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
import spiffe.workloadapi.internal.ManagedChannelWrapper;
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
import java.io.IOException;
import java.util.Arrays;

View File

@ -21,9 +21,9 @@ import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.jwtsvid.JwtSvid;
import spiffe.utils.TestUtils;
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
import spiffe.workloadapi.internal.ManagedChannelWrapper;
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
import spiffe.workloadapi.retry.BackoffPolicy;
import java.io.IOException;

View File

@ -16,9 +16,9 @@ import spiffe.exception.X509SourceException;
import spiffe.spiffeid.SpiffeId;
import spiffe.spiffeid.TrustDomain;
import spiffe.svid.x509svid.X509Svid;
import spiffe.workloadapi.grpc.SpiffeWorkloadAPIGrpc;
import spiffe.workloadapi.internal.ManagedChannelWrapper;
import spiffe.workloadapi.internal.SecurityHeaderInterceptor;
import spiffe.workloadapi.internal.SpiffeWorkloadAPIGrpc;
import java.io.IOException;

View File

@ -1,7 +1,101 @@
# JAVA-SPIFFE Helper
Helper to store SPIFFE X509-SVIDs and Bundles in a Java KeyStore file.
The JAVA-SPIFFE Helper is a simple utility for fetching X.509 SVID certificates from the SPIFFE Workload API,
and storing the Private Key and the chain of certificates in a Java KeyStore in disk, and the trusted bundles (CAs)
in a separated TrustStore in disk.
## Configuration
The Helper automatically gets the SVID updates and stores them in the KeyStore and TrustStore.
TBD
## Usage
`java -jar java-spiffe-helper-0.1.0.jar -c helper.conf`
(The jar can be found in `build/libs`, after running the gradle build)
Either `-c` or `--config` should be used to pass the path to the config file.
## Config file
```
keyStorePath = /tmp/keystore.p12
keyStorePass = example123
keyPass = pass123
trustStorePath = /tmp/truststore.p12
trustStorePass = otherpass123
keyStoreType = pkcs12
keyAlias = spiffe
spiffeSocketPath = unix:/tmp/agent.sock
```
### Configuration Properties
|Configuration | Description | Default value |
|------------------|--------------------------------------------------------------------------------| ------------- |
|`keyStorePath` | Path to the Java KeyStore File for storing the Private Key and chain of certs | none |
|`keyStorePass` | Password to protect the Java KeyStore File | none |
|`keyPass` | Password to protect the Private Key entry in the KeyStore | none |
|`trustStorePath` | Path to the Java TrustStore File for storing the trusted bundles | none |
|`trustStorePass` | Password to protect the Java TrustStore File | none |
|`keyStoreType` | Java KeyStore Type. (`pkcs12` and `jks` are supported). Case insensitive. | pkcs12 |
|`keyAlias` | Alias for the Private Key entry | spiffe |
|`spiffeSocketPath`| Path the Workload API | Read from the system variable: SPIFFE_ENDPOINT_SOCKET |
KeyStore and TrustStore **must** be in separate files. If `keyStorePath` and `trustStorePath` points to the same file, an error
is shown
.
If the store files do not exist, they are created.
The default and **recommended KeyStore Type** is `PKCS12`. The same type is used for both KeyStore and TrustStore.
It is **strongly recommended** to set restrictive file permissions for KeyStore file, since it stores a private key:
`chmod 600 keystore_file_name`
Make sure that the process running the JAVA-SPIFFE Helper has _write_ permission on the KeyStores files.
### Debug
To check that the certs are being stored in the KeyStore:
`keytool -list -v -keystore keystore.path -storepass example123`
The ouput should a `PrivateKeyEntry`:
```
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
Alias name: spiffe
Creation date: Jun 2, 2020
Entry type: PrivateKeyEntry
Owner: O=SPIFFE, C=US
Issuer: O=SPIFFE, C=US
...
```
In the case of the TrustStore, it should display a `trustedCertEntry`:
```
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
Alias name: example.org.0
Creation date: Jun 2, 2020
Entry type: trustedCertEntry
Owner: O=SPIFFE, C=US
Issuer: O=SPIFFE, C=US
...
```
The aliases for the trusted certs are generated using the Trust Domain of the SPIFFE ID in the SAN URI, and adding a
correlative number suffix.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package spiffe.helper;
package spiffe.helper.keystore;
import lombok.Builder;
import lombok.Value;
@ -6,22 +6,15 @@ import lombok.Value;
import java.security.cert.X509Certificate;
@Value
class BundleEntry {
class AuthorityEntry {
String alias;
X509Certificate certificate;
@Builder
BundleEntry(
AuthorityEntry(
final String alias,
final X509Certificate certificate) {
this.alias = alias;
this.certificate = certificate;
}
@Override
public String toString() {
return "BundleEntry{" +
"alias='" + alias + '\'' +
'}';
}
}

View File

@ -1,8 +1,9 @@
package spiffe.helper;
package spiffe.helper.keystore;
import lombok.Builder;
import lombok.NonNull;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileInputStream;
@ -23,38 +24,39 @@ class KeyStore {
private final Path keyStoreFilePath;
private final KeyStoreType keyStoreType;
private final char[] keyStorePassword;
private final String keyStorePassword;
private java.security.KeyStore javaKeyStore;
private File keyStoreFile;
private final java.security.KeyStore javaKeyStore;
private final File keyStoreFile;
@Builder
KeyStore(
@NonNull final Path keyStoreFilePath,
@NonNull final KeyStoreType keyStoreType,
@NonNull final char[] keyStorePassword) throws KeyStoreException {
@NonNull final String keyStorePassword) throws KeyStoreException {
this.keyStoreFilePath = keyStoreFilePath;
this.keyStoreType = keyStoreType;
this.keyStorePassword = keyStorePassword;
setupKeyStore();
}
private void setupKeyStore() throws KeyStoreException {
if (StringUtils.isBlank(keyStorePassword)) {
throw new IllegalArgumentException("keyStorePassword cannot be blank");
}
this.keyStorePassword = keyStorePassword;
this.keyStoreFile = new File(keyStoreFilePath.toUri());
this.javaKeyStore = loadKeyStore(keyStoreFile);
}
private java.security.KeyStore loadKeyStore(final File keyStoreFile) throws KeyStoreException {
try {
val keyStore = java.security.KeyStore.getInstance(keyStoreType.value());
// Initialize KeyStore
if (Files.exists(keyStoreFilePath)) {
keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword);
try (final FileInputStream fileInputStream = new FileInputStream(keyStoreFile)) {
keyStore.load(fileInputStream, keyStorePassword.toCharArray());
}
} else {
//create new keyStore
keyStore.load(null, keyStorePassword);
keyStore.load(null, keyStorePassword.toCharArray());
}
return keyStore;
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
@ -66,36 +68,36 @@ class KeyStore {
/**
* Store a private key and X.509 certificate chain in a Java KeyStore
*
* @param privateKeyEntry contains the alias, privateKey, chain, privateKey password
* @param keyEntry contains the alias, privateKey, chain, privateKey password
*/
void storePrivateKey(final PrivateKeyEntry privateKeyEntry) throws KeyStoreException {
void storePrivateKeyEntry(final PrivateKeyEntry keyEntry) throws KeyStoreException {
// Store PrivateKey Entry in KeyStore
javaKeyStore.setKeyEntry(
privateKeyEntry.getAlias(),
privateKeyEntry.getPrivateKey(),
privateKeyEntry.getPassword(),
privateKeyEntry.getCertificateChain()
keyEntry.getAlias(),
keyEntry.getPrivateKey(),
keyEntry.getPassword().toCharArray(),
keyEntry.getCertificateChain()
);
this.flush();
}
/**
* Store a Bundle Entry in the KeyStore
* Store an Authority Entry in the KeyStore.
*/
void storeBundleEntry(BundleEntry bundleEntry) throws KeyStoreException {
void storeAuthorityEntry(AuthorityEntry authorityEntry) throws KeyStoreException {
// Store Bundle Entry in KeyStore
this.javaKeyStore.setCertificateEntry(
bundleEntry.getAlias(),
bundleEntry.getCertificate()
authorityEntry.getAlias(),
authorityEntry.getCertificate()
);
this.flush();
}
// Flush KeyStore to disk, to the configured (@see keyStoreFilePath)
// Flush KeyStore to disk, to the configured keyStoreFilePath
private void flush() throws KeyStoreException {
try {
javaKeyStore.store(new FileOutputStream(keyStoreFile), keyStorePassword);
try (FileOutputStream fileOutputStream = new FileOutputStream(keyStoreFile)){
javaKeyStore.store(fileOutputStream, keyStorePassword.toCharArray());
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
throw new KeyStoreException(e);
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package spiffe.helper;
package spiffe.helper.keystore;
import lombok.Builder;
import lombok.Value;
@ -10,25 +10,18 @@ import java.security.cert.X509Certificate;
class PrivateKeyEntry {
String alias;
Key privateKey;
char[] password;
String password;
X509Certificate[] certificateChain;
@Builder
PrivateKeyEntry(
final String alias,
final Key privateKey,
final char[] password,
final String password,
final X509Certificate[] certificateChain) {
this.alias = alias;
this.privateKey = privateKey;
this.password = password;
this.certificateChain = certificateChain;
}
@Override
public String toString() {
return "PrivateKeyEntry{" +
"alias='" + alias + '\'' +
'}';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
keyStorePath = keystore123.p12
keyStorePass = example123
trustStorePath = truststore123.p12
trustStorePass = otherpass123
keyStoreType = jks
keyAlias = other_alias
spiffeSocketPath = unix:/tmp/agent.sock

View File

@ -0,0 +1,7 @@
keyStorePath = keystore123.p12
keyPass = pass123
trustStorePath = truststore123.p12
trustStorePass = otherpass123
keyStoreType = jks
keyAlias = other_alias
spiffeSocketPath = unix:/tmp/agent.sock

View File

@ -0,0 +1,7 @@
keyStorePass = example123
keyPass = pass123
trustStorePath = truststore123.p12
trustStorePass = otherpass123
keyStoreType = jks
keyAlias = other_alias
spiffeSocketPath = unix:/tmp/agent.sock

View 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

View File

@ -0,0 +1,7 @@
keyStorePath = keystore123.p12
keyStorePass = example123
keyPass = pass123
trustStorePass = otherpass123
keyStoreType = jks
keyAlias = other_alias
spiffeSocketPath = unix:/tmp/agent.sock

View File

@ -1,5 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGSpi81I7JqXLgi9O
CvwUSIdt7Ep/Ki7iupcHTYAziSWhRANCAATj80Da6Z4r2PVTn56A+p/OiaOemcvP
ryM29Q210DQuDoTv+eya+qg1tVVtoPzgmmV+Dr/odZKeg7srsD6hnWD6
-----END PRIVATE KEY-----

View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXNOpBRBUIzE4VRvy
nfxWNboXxrdVFTPesmpmp8V8D5+hRANCAAR0nJ2niXQw2PL8p97m3OyprAKEIA9R
LoH21QBHWxCkQ+gMS8ZPWfqz3io+r78HcYmLgadwqQGEd4ydpK5zOWfE
-----END PRIVATE KEY-----

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# Java SPIFFE Provider
This module provides a Java Security Provider implementation supporting X509-SVIDs and methods for
This module provides a Java Security Provider implementation supporting X.509-SVIDs and methods for
creating SSLContexts that are backed by the Workload API.
## Create an SSL Context backed by the Workload API
@ -11,11 +11,12 @@ Security property defined in the `java.security` containing the list of SPIFFE I
will trust for TLS connections.
```
val sslContextOptions = SslContextOptions
SslContextOptions options = SslContextOptions
.builder()
.x509Source(x509Source.newSource()())
.x509Source(x509Source.newSource())
.build();
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(options);
```
See [HttpsServer example](src/main/java/spiffe/provider/examples/HttpsServer.java).
@ -24,24 +25,25 @@ Alternatively, a different Workload API address can be used by passing it to the
Supplier of accepted SPIFFE IDs list can be provided as part of the `SslContextOptions`:
```
val sourceOptions = X509SourceOptions
X509SourceOptions sourceOptions = X509SourceOptions
.builder()
.spiffeSocketPath(spiffeSocket)
.build();
val x509Source = X509Source.newSource(sourceOptions);
X509Source x509Source = X509Source.newSource(sourceOptions);
SslContextOptions sslContextOptions = SslContextOptions
.builder()
.acceptedSpiffeIdsSupplier(acceptedSpiffeIdsListSupplier)
.x509Source(x509Source())
.x509Source(x509Source)
.build();
SSLContext sslContext = SpiffeSslContextFactory.getSslContext(sslContextOptions);
```
See [HttpsClient example](src/test/java/spiffe/provider/examples/mtls/HttpsClient.java) that defines a Supplier for providing
the list of SPIFFE IDs from a file.
## Plug Java SPIFFE Provider into Java Security
## Plug Java SPIFFE Provider into Java Security architecture
Java Security Providers are configured in the master security properties file `<java-home>/jre/lib/security/java.security`.

View File

@ -1,22 +1,14 @@
buildscript {
repositories {
jcenter()
}
ext.shadowPluginVersion = '5.2.0'
dependencies {
classpath group: 'com.github.jengelman.gradle.plugins', name: 'shadow', version: "${shadowPluginVersion}"
}
plugins {
id "com.github.johnrengelman.shadow" version "5.2.0"
}
version '0.6.0'
apply plugin: 'com.github.johnrengelman.shadow'
assemble.dependsOn shadowJar
shadowJar {
classifier = "all"
}
dependencies {
compile(project(":java-spiffe-core"))
}

View File

@ -22,9 +22,10 @@ public class SpiffeProviderConstants {
public static final String ALGORITHM = "Spiffe";
/**
* Alias used by the SpiffeKeyStore
* Alias used by the SpiffeKeyStore.
* Note: KeyStore aliases are case-insensitive.
*/
public static final String DEFAULT_ALIAS = "Spiffe";
public static final String DEFAULT_ALIAS = "spiffe";
private SpiffeProviderConstants() {
}