diff --git a/README.md b/README.md
index 877d5de..5fc7fef 100644
--- a/README.md
+++ b/README.md
@@ -2,20 +2,21 @@
## Overview
-The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X509 and JWT SVIDs and Bundles,
-and a Java Security Provider implementation to be plugged into the Java Security Interface plumbing. This is essentially
-a X509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
+The JAVA-SPIFFE library provides functionality to interact with the Workload API to fetch X.509 and JWT SVIDs and Bundles,
+and a Java Security Provider implementation to be plugged into the Java Security architecture. This is essentially
+a X.509-SVID based KeyStore and TrustStore implementation that handles the certificates in memory and receives the updates
asynchronously from the Workload API. The KeyStore handles the Certificate chain and Private Key to prove identity
in a TLS connection, and the TrustStore handles the trusted bundles (supporting federated bundles) and performs
peer's certificate and SPIFFE ID verification.
This library is composed of three modules:
-[java-spiffe-core](java-spiffe-core/README.md): core functionality to interact with the Workload API.
+[java-spiffe-core](java-spiffe-core/README.md): core functionality to interact with the Workload API, and to process and validate
+X.509 and JWT SVIDs and bundles.
[java-spiffe-provider](java-spiffe-provider/README.md): Java Provider implementation.
-[java-spiffe-helper](java-spiffe-helper/README.md): Helper to store X509-SVID Certificates in a Java Keystore in disk.
+[java-spiffe-helper](java-spiffe-helper/README.md): Helper to store X.509 SVIDs and Bundles in Java Keystores in disk.
**Supports Java 8+**
diff --git a/build.gradle b/build.gradle
index 12ac1c0..67ba4db 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,5 @@
subprojects {
group 'spiffe'
- version '0.6.0'
apply plugin: 'java-library'
apply plugin: 'jacoco'
@@ -15,27 +14,6 @@ subprojects {
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
-
- }
-
- jacocoTestReport {
- dependsOn test // tests are required to run before generating the report
- reports {
- xml.enabled false
- csv.enabled false
- html.destination file("${buildDir}/jacocoHtml")
- }
-
- afterEvaluate {
- classDirectories.setFrom(files(classDirectories.files.collect {
- fileTree(dir: it, exclude: ['**/internal/**', '**/exception/**'])
- }))
- }
- }
-
- jacoco {
- toolVersion = "0.8.5"
- reportsDir = file("$buildDir/customJacocoReportDir")
}
dependencies {
@@ -54,6 +32,36 @@ subprojects {
testCompileOnly 'org.projectlombok:lombok:1.18.12'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.12'
}
+
+ jacocoTestReport {
+ dependsOn test // tests are required to run before generating the report
+ reports {
+ xml.enabled false
+ csv.enabled false
+ html.destination file("${buildDir}/jacocoHtml")
+ }
+
+ afterEvaluate {
+ classDirectories.setFrom(files(classDirectories.files.collect {
+ fileTree(dir: it, exclude: ['**/grpc/**', '**/exception/**'])
+ }))
+ }
+ }
+
+ jacoco {
+ toolVersion = "0.8.5"
+ reportsDir = file("$buildDir/customJacocoReportDir")
+ }
+
+ test {
+ testLogging {
+ afterSuite { desc, result ->
+ if (!desc.parent) { // will match the outermost suite
+ println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
+ }
+ }
+ }
+ }
}
diff --git a/java-spiffe-core/README.md b/java-spiffe-core/README.md
index 115fd4b..5f8a9e9 100644
--- a/java-spiffe-core/README.md
+++ b/java-spiffe-core/README.md
@@ -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.
diff --git a/java-spiffe-core/build.gradle b/java-spiffe-core/build.gradle
index 6909f74..c9ebaa5 100644
--- a/java-spiffe-core/build.gradle
+++ b/java-spiffe-core/build.gradle
@@ -1,3 +1,4 @@
+version '0.6.0'
buildscript {
repositories {
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/Address.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/Address.java
index 426706e..92498e7 100644
--- a/java-spiffe-core/src/main/java/spiffe/workloadapi/Address.java
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/Address.java
@@ -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) {
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/JwtSource.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/JwtSource.java
index f5860dd..5679d72 100644
--- a/java-spiffe-core/src/main/java/spiffe/workloadapi/JwtSource.java
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/JwtSource.java
@@ -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 JwtSource 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()
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java
index 1fe2f49..2fa0461 100644
--- a/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/WorkloadApiClient.java
@@ -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 WorkloadApiClient represents a client to interact with the Workload API.
*
@@ -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
- * 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
- * Java Cryptography Architecture Standard Algorithm Name Documentation
- * 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
+ * It stores the Private Key along with the chain of certificates in a KeyStore, and the
+ * trusted bundles in a separate KeyStore (TrustStore).
+ *
+ * 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.
+ *
+ * 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
+ * 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;
+ }
+ }
+}
diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStoreType.java b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStoreType.java
new file mode 100644
index 0000000..5a27dd4
--- /dev/null
+++ b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStoreType.java
@@ -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));
+ }
+ }
+}
diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/PrivateKeyEntry.java b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/PrivateKeyEntry.java
similarity index 70%
rename from java-spiffe-helper/src/main/java/spiffe/helper/PrivateKeyEntry.java
rename to java-spiffe-helper/src/main/java/spiffe/helper/keystore/PrivateKeyEntry.java
index 04c2e6a..8664b64 100644
--- a/java-spiffe-helper/src/main/java/spiffe/helper/PrivateKeyEntry.java
+++ b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/PrivateKeyEntry.java
@@ -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 + '\'' +
- '}';
- }
}
diff --git a/java-spiffe-helper/src/test/java/spiffe/helper/KeyStoreTest.java b/java-spiffe-helper/src/test/java/spiffe/helper/KeyStoreTest.java
deleted file mode 100644
index 411fa5e..0000000
--- a/java-spiffe-helper/src/test/java/spiffe/helper/KeyStoreTest.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/java-spiffe-helper/src/test/java/spiffe/helper/cli/RunnerTest.java b/java-spiffe-helper/src/test/java/spiffe/helper/cli/RunnerTest.java
new file mode 100644
index 0000000..9994905
--- /dev/null
+++ b/java-spiffe-helper/src/test/java/spiffe/helper/cli/RunnerTest.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/java-spiffe-helper/src/test/java/spiffe/helper/keystore/FakeWorkloadApi.java b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/FakeWorkloadApi.java
new file mode 100644
index 0000000..58e75c5
--- /dev/null
+++ b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/FakeWorkloadApi.java
@@ -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, StreamObserverX509Source 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");
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/internal/GrpcConversionUtils.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/internal/GrpcConversionUtils.java
index e5b9698..46d710a 100644
--- a/java-spiffe-core/src/main/java/spiffe/workloadapi/internal/GrpcConversionUtils.java
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/internal/GrpcConversionUtils.java
@@ -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;
diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/internal/ThreadUtils.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/internal/ThreadUtils.java
new file mode 100644
index 0000000..a28b290
--- /dev/null
+++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/internal/ThreadUtils.java
@@ -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;
+ }
+}
diff --git a/java-spiffe-core/src/main/proto/workload.proto b/java-spiffe-core/src/main/proto/workload.proto
index 270e736..c5465ca 100644
--- a/java-spiffe-core/src/main/proto/workload.proto
+++ b/java-spiffe-core/src/main/proto/workload.proto
@@ -1,6 +1,6 @@
syntax = "proto3";
-option java_package = "spiffe.workloadapi.internal";
+option java_package = "spiffe.workloadapi.grpc";
import "google/protobuf/struct.proto";
diff --git a/java-spiffe-core/src/test/java/spiffe/workloadapi/FakeWorkloadApi.java b/java-spiffe-core/src/test/java/spiffe/workloadapi/FakeWorkloadApi.java
index d56468e..dd6fdcb 100644
--- a/java-spiffe-core/src/test/java/spiffe/workloadapi/FakeWorkloadApi.java
+++ b/java-spiffe-core/src/test/java/spiffe/workloadapi/FakeWorkloadApi.java
@@ -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";
diff --git a/java-spiffe-core/src/test/java/spiffe/workloadapi/JwtSourceTest.java b/java-spiffe-core/src/test/java/spiffe/workloadapi/JwtSourceTest.java
index 8d64a7e..9f5babc 100644
--- a/java-spiffe-core/src/test/java/spiffe/workloadapi/JwtSourceTest.java
+++ b/java-spiffe-core/src/test/java/spiffe/workloadapi/JwtSourceTest.java
@@ -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;
diff --git a/java-spiffe-core/src/test/java/spiffe/workloadapi/WorkloadApiClientTest.java b/java-spiffe-core/src/test/java/spiffe/workloadapi/WorkloadApiClientTest.java
index 28f08e2..25922c2 100644
--- a/java-spiffe-core/src/test/java/spiffe/workloadapi/WorkloadApiClientTest.java
+++ b/java-spiffe-core/src/test/java/spiffe/workloadapi/WorkloadApiClientTest.java
@@ -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;
diff --git a/java-spiffe-core/src/test/java/spiffe/workloadapi/X509SourceTest.java b/java-spiffe-core/src/test/java/spiffe/workloadapi/X509SourceTest.java
index 2f0ebbb..96a6e39 100644
--- a/java-spiffe-core/src/test/java/spiffe/workloadapi/X509SourceTest.java
+++ b/java-spiffe-core/src/test/java/spiffe/workloadapi/X509SourceTest.java
@@ -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;
diff --git a/java-spiffe-helper/README.md b/java-spiffe-helper/README.md
index 53f33ba..df737e5 100644
--- a/java-spiffe-helper/README.md
+++ b/java-spiffe-helper/README.md
@@ -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
\ No newline at end of file
+## 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.
\ No newline at end of file
diff --git a/java-spiffe-helper/build.gradle b/java-spiffe-helper/build.gradle
index fe02a21..51c3148 100644
--- a/java-spiffe-helper/build.gradle
+++ b/java-spiffe-helper/build.gradle
@@ -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"
}
diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/KeyStoreHelper.java b/java-spiffe-helper/src/main/java/spiffe/helper/KeyStoreHelper.java
deleted file mode 100644
index 2dd3bea..0000000
--- a/java-spiffe-helper/src/main/java/spiffe/helper/KeyStoreHelper.java
+++ /dev/null
@@ -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 KeyStoreHelper 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.
- * KeyStoreHelper 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.
+ *