From f7b6e44b0326de70cc8b00ee463db991e7704aad Mon Sep 17 00:00:00 2001 From: Timothy Tschampel Date: Tue, 28 May 2024 12:45:17 -0700 Subject: [PATCH 1/2] Issue #57 - add SSLContext for comms. also update to java 17 Issue #57 - add docs rollback to jdk 11 :(, Address PR comments = use protocol lib independant approach. Add SSL test --- README.md | 6 + pom.xml | 40 +++++ sdk/pom.xml | 30 ++-- .../platform/sdk/GRPCAuthInterceptor.java | 10 +- .../io/opentdf/platform/sdk/SDKBuilder.java | 73 ++++++++- .../opentdf/platform/sdk/SDKBuilderTest.java | 140 +++++++++++++++--- 6 files changed, 260 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index f19f66c4..3fe68e78 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,9 @@ OpenTDF Java SDK ### Logging We use [slf4j](https://www.slf4j.org/), without providing a backend. We use log4j2 in our tests. + +### SSL - Untrusted Certificates +Use the SDKBuilder.withSSL... methods to build an SDKBuilder with: +- An SSLFactory: ```sdkBuilder.sslFactory(mySSLFactory)``` +- Directory containing trusted certificates: ```sdkBuilder.sslFactoryFromDirectory(myDirectoryWithCerts)``` +- Java Keystore: ```sdkBuilder.sslFactoryFromKeyStore(keystorepath, keystorePassword)``` diff --git a/pom.xml b/pom.xml index cec30b22..caf70549 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ 2.20.0 1.63.0 3.25.3 + 8.3.5 protocol @@ -69,6 +70,45 @@ 3.4 provided + + io.github.hakky54 + sslcontext-kickstart-for-pem + ${sslcontext.version} + + + org.slf4j + slf4j-api + + + + + io.github.hakky54 + sslcontext-kickstart + ${sslcontext.version} + + + org.slf4j + slf4j-api + + + + + io.github.hakky54 + sslcontext-kickstart-for-netty + ${sslcontext.version} + + + org.slf4j + slf4j-api + + + + + io.grpc + grpc-netty-shaded + ${grpc.version} + runtime + diff --git a/sdk/pom.xml b/sdk/pom.xml index a2e5edd5..459c89a1 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -4,18 +4,6 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 sdk - - - - org.apache.maven.plugins - maven-compiler-plugin - - 11 - 11 - - - - sdk sdk-pom @@ -111,5 +99,23 @@ 1.26.1 test + + io.github.hakky54 + sslcontext-kickstart-for-pem + + + io.github.hakky54 + sslcontext-kickstart + + + io.github.hakky54 + sslcontext-kickstart-for-netty + + + com.squareup.okhttp3 + okhttp-tls + 5.0.0-alpha.14 + test + diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/GRPCAuthInterceptor.java b/sdk/src/main/java/io/opentdf/platform/sdk/GRPCAuthInterceptor.java index bfb04d4c..5a796489 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/GRPCAuthInterceptor.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/GRPCAuthInterceptor.java @@ -22,6 +22,7 @@ import io.grpc.ForwardingClientCall; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import nl.altindag.ssl.SSLFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +41,7 @@ class GRPCAuthInterceptor implements ClientInterceptor { private final ClientAuthentication clientAuth; private final RSAKey rsaKey; private final URI tokenEndpointURI; - + private SSLFactory sslFactory; private static final Logger logger = LoggerFactory.getLogger(GRPCAuthInterceptor.class); @@ -49,11 +50,13 @@ class GRPCAuthInterceptor implements ClientInterceptor { * * @param clientAuth the client authentication to be used by the interceptor * @param rsaKey the RSA key to be used by the interceptor + * @param sslFactory Optional SSLFactory for Requests */ - public GRPCAuthInterceptor(ClientAuthentication clientAuth, RSAKey rsaKey, URI tokenEndpointURI) { + public GRPCAuthInterceptor(ClientAuthentication clientAuth, RSAKey rsaKey, URI tokenEndpointURI, SSLFactory sslFactory) { this.clientAuth = clientAuth; this.rsaKey = rsaKey; this.tokenEndpointURI = tokenEndpointURI; + this.sslFactory = sslFactory; } /** @@ -114,6 +117,9 @@ private synchronized AccessToken getToken() { TokenRequest tokenRequest = new TokenRequest(this.tokenEndpointURI, clientAuth, clientGrant, null); HTTPRequest httpRequest = tokenRequest.toHTTPRequest(); + if(sslFactory!=null){ + httpRequest.setSSLSocketFactory(sslFactory.getSslSocketFactory()); + } DPoPProofFactory dpopFactory = new DefaultDPoPProofFactory(rsaKey, JWSAlgorithm.RS256); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java index e56c2941..bb329304 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java @@ -11,17 +11,23 @@ import com.nimbusds.oauth2.sdk.id.ClientID; import com.nimbusds.oauth2.sdk.id.Issuer; import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Status; -import io.grpc.StatusRuntimeException; +import io.grpc.*; import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationRequest; import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse; import io.opentdf.platform.wellknownconfiguration.WellKnownServiceGrpc; +import nl.altindag.ssl.SSLFactory; +import nl.altindag.ssl.pem.util.PemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; /** @@ -32,6 +38,7 @@ public class SDKBuilder { private String platformEndpoint = null; private ClientAuthentication clientAuth = null; private Boolean usePlainText; + private SSLFactory sslFactory; private static final Logger logger = LoggerFactory.getLogger(SDKBuilder.class); @@ -44,6 +51,47 @@ public static SDKBuilder newBuilder() { return builder; } + public SDKBuilder sslFactory(SSLFactory sslFactory) { + this.sslFactory = sslFactory; + return this; + } + + /** + * Add SSL Context with trusted certs from certDirPath + * @param certsDirPath Path to a directory containing .pem or .crt trusted certs + * @return + */ + public SDKBuilder sslFactoryFromDirectory(String certsDirPath) throws Exception{ + File certsDir = new File(certsDirPath); + File[] certFiles = + certsDir.listFiles((dir, name) -> name.endsWith(".pem") || name.endsWith(".crt")); + logger.info("Loading certificates from: " + certsDir.getAbsolutePath()); + List certStreams = new ArrayList<>(); + for (File certFile : certFiles) { + certStreams.add(new FileInputStream(certFile)); + } + X509ExtendedTrustManager trustManager = + PemUtils.loadTrustMaterial(certStreams.toArray(new InputStream[0])); + this.sslFactory = + SSLFactory.builder().withDefaultTrustMaterial().withSystemTrustMaterial() + .withTrustMaterial(trustManager).build(); + return this; + } + + /** + * Add SSL Context with default system trust material + certs contained in a Java keystore + * @param keystorePath Path to keystore + * @param keystorePassword Password to keystore + * @return + */ + public SDKBuilder sslFactoryFromKeyStore(String keystorePath, String keystorePassword) { + this.sslFactory = + SSLFactory.builder().withDefaultTrustMaterial().withSystemTrustMaterial() + .withTrustMaterial(Path.of(keystorePath), keystorePassword==null ? + "".toCharArray() : keystorePassword.toCharArray()).build(); + return this; + } + public SDKBuilder platformEndpoint(String platformEndpoint) { this.platformEndpoint = platformEndpoint; return this; @@ -104,12 +152,16 @@ private GRPCAuthInterceptor getGrpcAuthInterceptor(RSAKey rsaKey) { Issuer issuer = new Issuer(platformIssuer); OIDCProviderMetadata providerMetadata; try { - providerMetadata = OIDCProviderMetadata.resolve(issuer); + providerMetadata = OIDCProviderMetadata.resolve(issuer, httpRequest -> { + if (sslFactory!=null) { + httpRequest.setSSLSocketFactory(sslFactory.getSslSocketFactory()); + } + }); } catch (IOException | GeneralException e) { throw new SDKException("Error resolving the OIDC provider metadata", e); } - return new GRPCAuthInterceptor(clientAuth, rsaKey, providerMetadata.getTokenEndpointURI()); + return new GRPCAuthInterceptor(clientAuth, rsaKey, providerMetadata.getTokenEndpointURI(), sslFactory); } SDK.Services buildServices() { @@ -141,8 +193,13 @@ public SDK build() { * @return {@type ManagedChannelBuilder} configured with the SDK options */ private ManagedChannelBuilder getManagedChannelBuilder(String endpoint) { - ManagedChannelBuilder channelBuilder = ManagedChannelBuilder - .forTarget(endpoint); + ManagedChannelBuilder channelBuilder; + if (sslFactory != null) { + channelBuilder = Grpc.newChannelBuilder(endpoint, TlsChannelCredentials.newBuilder() + .trustManager(sslFactory.getTrustManager().get()).build()); + }else{ + channelBuilder = ManagedChannelBuilder.forTarget(endpoint); + } if (usePlainText) { channelBuilder = channelBuilder.usePlaintext(); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java index c9cb7e90..d5adefec 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/SDKBuilderTest.java @@ -2,8 +2,6 @@ import com.google.protobuf.Struct; import com.google.protobuf.Value; -import io.grpc.ConnectivityState; -import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.Server; import io.grpc.ServerBuilder; @@ -15,36 +13,114 @@ import io.opentdf.platform.kas.RewrapRequest; import io.opentdf.platform.kas.RewrapResponse; import io.opentdf.platform.policy.namespaces.GetNamespaceRequest; -import io.opentdf.platform.policy.namespaces.GetNamespaceResponse; import io.opentdf.platform.policy.namespaces.NamespaceServiceGrpc; import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationRequest; import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse; import io.opentdf.platform.wellknownconfiguration.WellKnownServiceGrpc; +import nl.altindag.ssl.SSLFactory; +import nl.altindag.ssl.pem.util.PemUtils; +import nl.altindag.ssl.util.KeyStoreUtils; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import okhttp3.tls.HandshakeCertificates; +import okhttp3.tls.HeldCertificate; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; -import java.io.IOException; +import java.io.*; +import java.net.InetAddress; import java.net.ServerSocket; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Base64; import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + public class SDKBuilderTest { + final String EXAMPLE_COM_PEM="-----BEGIN CERTIFICATE-----\n" + + "MIIBqTCCARKgAwIBAgIIT0xFd/5uogEwDQYJKoZIhvcNAQEFBQAwFjEUMBIGA1UEAxMLZXhhbXBs\n" + + "ZS5jb20wIBcNMTcwMTIwMTczOTIwWhgPOTk5OTEyMzEyMzU5NTlaMBYxFDASBgNVBAMTC2V4YW1w\n" + + "bGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2Tl2MdaUFmjAaYwmEwgEVRfVqwJO4\n" + + "Y+7Vxm4UqQRKNucpGUwUBo9FSvuQACpnJwHsK2WhiuSpVkunhmSx5Qb4KVSH2RT2vHBUsA3t12S2\n" + + "1Vkskiya3E7QR91zZGVxZyB4gSBVhvSVXeP9+RogLLziki/VDXXKT4TIuyML1eUQ2QIDAQABMA0G\n" + + "CSqGSIb3DQEBBQUAA4GBAGfw0xavZSJXxuFAwxCZBtne9BAtk+SmfKkTI21v8Tx6w/p5Yt0IIvF3\n" + + "0wCES7YVZ+zUc8vtVVyk1q3f1ZqXqVvzRCjzLzQnu6VVLBaiZPH9SYNX6j0pHhBvx1ZUMopJPr2D\n" + + "avTXCTSHY5JoX20KEwfu8QQXQRDUzyc0QKn9SiE3\n" + + "-----END CERTIFICATE-----"; + @Test + void testDirCertsSSLContext() throws Exception{ + Path certDirPath = Files.createTempDirectory("certs"); + File pemFile = new File(certDirPath.toAbsolutePath().toString(), "ca.pem"); + FileOutputStream fos = new FileOutputStream(pemFile); + IOUtils.write(EXAMPLE_COM_PEM,fos); + fos.close(); + SDKBuilder builder = SDKBuilder.newBuilder().sslFactoryFromDirectory(certDirPath.toAbsolutePath().toString()); + SSLFactory sslFactory = builder.getSslFactory(); + assertNotNull(sslFactory); + X509Certificate[] acceptedIssuers= sslFactory.getTrustManager().get().getAcceptedIssuers(); + assertEquals(1, Arrays.stream(acceptedIssuers).filter(x->x.getIssuerX500Principal().getName() + .equals("CN=example.com")).count()); + } + + @Test + void testKeystoreSSLContext() throws Exception{ + KeyStore keystore = KeyStoreUtils.createKeyStore(); + keystore.setCertificateEntry("example.com", PemUtils.parseCertificate(EXAMPLE_COM_PEM).get(0)); + Path keyStorePath = Files.createTempFile("ca", "jks"); + keystore.store(new FileOutputStream(keyStorePath.toAbsolutePath().toString()), "foo".toCharArray()); + SDKBuilder builder = SDKBuilder.newBuilder().sslFactoryFromKeyStore(keyStorePath.toAbsolutePath().toString(), "foo"); + SSLFactory sslFactory = builder.getSslFactory(); + assertNotNull(sslFactory); + X509Certificate[] acceptedIssuers= sslFactory.getTrustManager().get().getAcceptedIssuers(); + assertEquals(1, Arrays.stream(acceptedIssuers).filter(x->x.getIssuerX500Principal().getName() + .equals("CN=example.com")).count()); + + } + @Test - void testCreatingSDKServices() throws IOException, InterruptedException { + void testSDKServicesWithTruststore() throws Exception{ + sdkServicesSetup(true); + } + + @Test + void testCreatingSDKServicesPlainText() throws Exception { + sdkServicesSetup(false); + } + + void sdkServicesSetup(boolean useSSL) throws Exception{ + + HeldCertificate rootCertificate = new HeldCertificate.Builder() + .certificateAuthority(0) + .build(); + String localhost = InetAddress.getByName("localhost").getCanonicalHostName(); + HeldCertificate serverCertificate = new HeldCertificate.Builder() + .addSubjectAlternativeName(localhost) + .commonName("CN=localhost") + .signedBy(rootCertificate) + .build(); + + HandshakeCertificates serverHandshakeCertificates = new HandshakeCertificates.Builder() + .heldCertificate(serverCertificate, rootCertificate.certificate()) + .build(); + Server platformServicesServer = null; Server kasServer = null; // we use the HTTP server for two things: // * it returns the OIDC configuration we use at bootstrapping time // * it fakes out being an IDP and returns an access token when need to retrieve an access token try (MockWebServer httpServer = new MockWebServer()) { + if (useSSL){ + httpServer.useHttps(serverHandshakeCertificates.sslSocketFactory(), false); + } String oidcConfig; try (var in = SDKBuilderTest.class.getResourceAsStream("/oidc-config.json")) { oidcConfig = new String(in.readAllBytes(), StandardCharsets.UTF_8); @@ -86,7 +162,7 @@ public void getWellKnownConfiguration(GetWellKnownConfigurationRequest request, // we use the server in two different ways. the first time we use it to actually return // issuer for bootstrapping. the second time we use the interception functionality in order // to make sure that we are including a DPoP proof and an auth header - platformServicesServer = ServerBuilder + ServerBuilder platformServicesServerBuilder = ServerBuilder .forPort(getRandomPort()) .directExecutor() .addService(wellKnownService) @@ -98,12 +174,17 @@ public ServerCall.Listener interceptCall(ServerCall kasServerBuilder = ServerBuilder .forPort(getRandomPort()) .directExecutor() .addService(new AccessServiceGrpc.AccessServiceImplBase() { @@ -120,15 +201,29 @@ public ServerCall.Listener interceptCall(ServerCall ServerCall.Listener interceptCall(ServerCall ServerCall.Listener interceptCall(ServerCall Date: Wed, 29 May 2024 07:20:42 -0700 Subject: [PATCH 2/2] fix rebase error --- sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java index bb329304..4b620be8 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java @@ -206,4 +206,8 @@ private ManagedChannelBuilder getManagedChannelBuilder(String endpoint) { } return channelBuilder; } + + SSLFactory getSslFactory(){ + return this.sslFactory; + } }