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 getX509ContextStreamObserver(Watcher watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) { - return new StreamObserver() { + private StreamObserver getX509ContextStreamObserver(Watcher watcher, RetryHandler retryHandler, Context.CancellableContext cancellableContext) { + return new StreamObserver() { @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 = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest()); + Iterator x509SVIDResponse = workloadApiBlockingStub.fetchX509SVID(newX509SvidRequest()); if (x509SVIDResponse.hasNext()) { return GrpcConversionUtils.toX509Context(x509SVIDResponse.next()); } diff --git a/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java b/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java index e71fc7c..c095b76 100644 --- a/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java +++ b/java-spiffe-core/src/main/java/spiffe/workloadapi/X509Source.java @@ -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 X509Source 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. - *

- * 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() { - @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); - } - } -} diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/KeyStoreType.java b/java-spiffe-helper/src/main/java/spiffe/helper/KeyStoreType.java deleted file mode 100644 index ddb6202..0000000 --- a/java-spiffe-helper/src/main/java/spiffe/helper/KeyStoreType.java +++ /dev/null @@ -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; - } -} diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/cli/Runner.java b/java-spiffe-helper/src/main/java/spiffe/helper/cli/Runner.java new file mode 100644 index 0000000..429962e --- /dev/null +++ b/java-spiffe-helper/src/main/java/spiffe/helper/cli/Runner.java @@ -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 ", 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; + } +} diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/BundleEntry.java b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/AuthorityEntry.java similarity index 60% rename from java-spiffe-helper/src/main/java/spiffe/helper/BundleEntry.java rename to java-spiffe-helper/src/main/java/spiffe/helper/keystore/AuthorityEntry.java index 7f635fb..f89509f 100644 --- a/java-spiffe-helper/src/main/java/spiffe/helper/BundleEntry.java +++ b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/AuthorityEntry.java @@ -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 + '\'' + - '}'; - } } diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/KeyStore.java b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStore.java similarity index 59% rename from java-spiffe-helper/src/main/java/spiffe/helper/KeyStore.java rename to java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStore.java index 043284e..71c3ba0 100644 --- a/java-spiffe-helper/src/main/java/spiffe/helper/KeyStore.java +++ b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStore.java @@ -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); } diff --git a/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStoreHelper.java b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStoreHelper.java new file mode 100644 index 0000000..e73771f --- /dev/null +++ b/java-spiffe-helper/src/main/java/spiffe/helper/keystore/KeyStoreHelper.java @@ -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 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. + *

+ * 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() { + @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 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 + * Java Cryptography Architecture Standard Algorithm Name Documentation + * for information about standard keystore types. + *

+ * 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, StreamObserver 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(); + } +} + diff --git a/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreHelperTest.java b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreHelperTest.java new file mode 100644 index 0000000..f708cc3 --- /dev/null +++ b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreHelperTest.java @@ -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 + } + } + +} \ No newline at end of file diff --git a/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreTest.java b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreTest.java new file mode 100644 index 0000000..7db6bcb --- /dev/null +++ b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreTest.java @@ -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 + } + } +} diff --git a/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreTypeTest.java b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreTypeTest.java new file mode 100644 index 0000000..b3b5a0d --- /dev/null +++ b/java-spiffe-helper/src/test/java/spiffe/helper/keystore/KeyStoreTypeTest.java @@ -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()); + } + } +} \ No newline at end of file diff --git a/java-spiffe-helper/src/test/resources/testdata/bundle.pem b/java-spiffe-helper/src/test/resources/testdata/bundle.pem new file mode 100644 index 0000000..5970f55 --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/bundle.pem @@ -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----- diff --git a/java-spiffe-helper/src/test/resources/testdata/cli/correct.conf b/java-spiffe-helper/src/test/resources/testdata/cli/correct.conf new file mode 100644 index 0000000..949aef5 --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/cli/correct.conf @@ -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 diff --git a/java-spiffe-helper/src/test/resources/testdata/cli/missing-keypass.conf b/java-spiffe-helper/src/test/resources/testdata/cli/missing-keypass.conf new file mode 100644 index 0000000..9526afd --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/cli/missing-keypass.conf @@ -0,0 +1,7 @@ +keyStorePath = keystore123.p12 +keyStorePass = example123 +trustStorePath = truststore123.p12 +trustStorePass = otherpass123 +keyStoreType = jks +keyAlias = other_alias +spiffeSocketPath = unix:/tmp/agent.sock diff --git a/java-spiffe-helper/src/test/resources/testdata/cli/missing-keystorepass.conf b/java-spiffe-helper/src/test/resources/testdata/cli/missing-keystorepass.conf new file mode 100644 index 0000000..6c683e5 --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/cli/missing-keystorepass.conf @@ -0,0 +1,7 @@ +keyStorePath = keystore123.p12 +keyPass = pass123 +trustStorePath = truststore123.p12 +trustStorePass = otherpass123 +keyStoreType = jks +keyAlias = other_alias +spiffeSocketPath = unix:/tmp/agent.sock diff --git a/java-spiffe-helper/src/test/resources/testdata/cli/missing-keystorepath.conf b/java-spiffe-helper/src/test/resources/testdata/cli/missing-keystorepath.conf new file mode 100644 index 0000000..de66b1d --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/cli/missing-keystorepath.conf @@ -0,0 +1,7 @@ +keyStorePass = example123 +keyPass = pass123 +trustStorePath = truststore123.p12 +trustStorePass = otherpass123 +keyStoreType = jks +keyAlias = other_alias +spiffeSocketPath = unix:/tmp/agent.sock diff --git a/java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepass.conf b/java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepass.conf new file mode 100644 index 0000000..3015897 --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepass.conf @@ -0,0 +1,7 @@ +keyStorePath = keystore123.p12 +keyStorePass = example123 +keyPass = pass123 +trustStorePath = truststore123.p12 +keyStoreType = jks +keyAlias = other_alias +spiffeSocketPath = unix:/tmp/agent.sock diff --git a/java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepath.conf b/java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepath.conf new file mode 100644 index 0000000..ca4769f --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/cli/missing-truststorepath.conf @@ -0,0 +1,7 @@ +keyStorePath = keystore123.p12 +keyStorePass = example123 +keyPass = pass123 +trustStorePass = otherpass123 +keyStoreType = jks +keyAlias = other_alias +spiffeSocketPath = unix:/tmp/agent.sock diff --git a/java-spiffe-helper/src/test/resources/testdata/pkcs8key.pem b/java-spiffe-helper/src/test/resources/testdata/pkcs8key.pem deleted file mode 100644 index e0bcb4c..0000000 --- a/java-spiffe-helper/src/test/resources/testdata/pkcs8key.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGSpi81I7JqXLgi9O -CvwUSIdt7Ep/Ki7iupcHTYAziSWhRANCAATj80Da6Z4r2PVTn56A+p/OiaOemcvP -ryM29Q210DQuDoTv+eya+qg1tVVtoPzgmmV+Dr/odZKeg7srsD6hnWD6 ------END PRIVATE KEY----- diff --git a/java-spiffe-helper/src/test/resources/testdata/svid.key b/java-spiffe-helper/src/test/resources/testdata/svid.key new file mode 100644 index 0000000..71821ab --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/svid.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXNOpBRBUIzE4VRvy +nfxWNboXxrdVFTPesmpmp8V8D5+hRANCAAR0nJ2niXQw2PL8p97m3OyprAKEIA9R +LoH21QBHWxCkQ+gMS8ZPWfqz3io+r78HcYmLgadwqQGEd4ydpK5zOWfE +-----END PRIVATE KEY----- diff --git a/java-spiffe-helper/src/test/resources/testdata/svid.pem b/java-spiffe-helper/src/test/resources/testdata/svid.pem new file mode 100644 index 0000000..934b06e --- /dev/null +++ b/java-spiffe-helper/src/test/resources/testdata/svid.pem @@ -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----- diff --git a/java-spiffe-helper/src/test/resources/testdata/x509cert.pem b/java-spiffe-helper/src/test/resources/testdata/x509cert.pem deleted file mode 100644 index 437603d..0000000 --- a/java-spiffe-helper/src/test/resources/testdata/x509cert.pem +++ /dev/null @@ -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----- diff --git a/java-spiffe-provider/README.md b/java-spiffe-provider/README.md index ca0e94a..cc647bb 100644 --- a/java-spiffe-provider/README.md +++ b/java-spiffe-provider/README.md @@ -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 `/jre/lib/security/java.security`. diff --git a/java-spiffe-provider/build.gradle b/java-spiffe-provider/build.gradle index 26f6e46..3976f66 100644 --- a/java-spiffe-provider/build.gradle +++ b/java-spiffe-provider/build.gradle @@ -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")) } diff --git a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java index 4910e7c..c815338 100644 --- a/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java +++ b/java-spiffe-provider/src/main/java/spiffe/provider/SpiffeProviderConstants.java @@ -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() { }