From c27322bc9b9c4f952b8f74a3f5dc3f8e0f63024d Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Tue, 16 Jun 2026 19:37:51 -0400 Subject: [PATCH 1/2] Generate gRPC test TLS material at runtime instead of committing it The gRPC transport tests shipped a self-signed localhost certificate, its private key, and a PKCS12 truststore under src/test/resources/grpc-tls/. Committing a PEM private key + PKCS12 bundle trips secret scanning (SEC101/013 PemPrivateKey, SEC101/055 Pkcs12CertificatePrivateKeyBundle) and hard-blocks the internal ADO mirror push (VS403654 NonbypassableBlock). Replace the committed files with TestTlsMaterial, which generates an ephemeral CN=localhost keypair, certificate, and matching truststore at test runtime using the JDK's keytool. keytool ships with every JDK (8-25), so this stays portable across the CI Java matrix without adding a certificate-generation dependency (e.g. Bouncy Castle) and without needing --add-exports for sun.security internals. - TestTlsMaterial: keytool-driven generator, lazily initialized once per JVM, writing to a temp dir that is deleted on exit. - FunctionsTestHost: builds the server SslContext from the generated PKCS12 keystore (PrivateKey + X509Certificate) rather than PEM resource files. - GrpcTransportTest: points the client truststore at the generated PKCS12 truststore instead of the committed resource. - Delete src/test/resources/grpc-tls/* and add .gitignore guards so keys and keystores cannot be re-committed. Note: this removes the files from the branch tip only. Purging them from the history that the mirror replays (commit 079a4d40) still requires a separate history rewrite or a 1ES exception. Verified: mvn test -Dtest=GrpcTransportTest -> 3 passed (plaintext, trusted HTTPS/TLS, and HTTPS-no-plaintext-downgrade). --- .gitignore | 7 + .../functional/tests/GrpcTransportTest.java | 22 +-- .../test/utilities/FunctionsTestHost.java | 38 ++-- .../test/utilities/TestTlsMaterial.java | 176 ++++++++++++++++++ .../resources/grpc-tls/localhost-cert.pem | 19 -- src/test/resources/grpc-tls/localhost-key.pem | 28 --- .../grpc-tls/localhost-truststore.p12 | Bin 1190 -> 0 bytes 7 files changed, 210 insertions(+), 80 deletions(-) create mode 100644 src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java delete mode 100644 src/test/resources/grpc-tls/localhost-cert.pem delete mode 100644 src/test/resources/grpc-tls/localhost-key.pem delete mode 100644 src/test/resources/grpc-tls/localhost-truststore.p12 diff --git a/.gitignore b/.gitignore index 68ad7d7..b9fec80 100644 --- a/.gitignore +++ b/.gitignore @@ -291,3 +291,10 @@ __pycache__/ # Docker test packages dockertests/app-packages/ + +# Test TLS material is generated at runtime by keytool (see TestTlsMaterial); +# never commit private keys or keystores. +src/test/resources/grpc-tls/ +*.p12 +*.pfx +*-key.pem diff --git a/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java b/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java index 95c6d81..4b7e76c 100644 --- a/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java +++ b/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java @@ -1,6 +1,7 @@ package com.microsoft.azure.functions.worker.functional.tests; import java.net.*; +import java.nio.file.*; import java.util.concurrent.*; import javax.net.ssl.*; @@ -12,8 +13,6 @@ public class GrpcTransportTest extends FunctionsTestBase { private static final String RETURN_VALUE = "transport-ok"; - private static final String TRUSTSTORE_RESOURCE = "grpc-tls/localhost-truststore.p12"; - private static final String TRUSTSTORE_PASSWORD = "changeit"; public String ReturnStringFunction() { return RETURN_VALUE; @@ -33,7 +32,7 @@ public void legacyPlaintextTransportStillWorks() throws Exception { @Test public void trustedHttpsFunctionsUriConnectsToTlsHost() throws Exception { try (SkipTestingScope ignored = SkipTestingScope.enable(); - TrustStoreScope ignoredTrustStore = TrustStoreScope.use(TRUSTSTORE_RESOURCE, TRUSTSTORE_PASSWORD, "PKCS12"); + TrustStoreScope ignoredTrustStore = TrustStoreScope.use(); FunctionsTestHost host = new FunctionsTestHost(FunctionsTestHost.ServerTransport.TLS, FunctionsTestHost.ClientTransport.HTTPS)) { InvocationResponse response = this.invokeReturnString(host, "tls-function", "tls-request"); @@ -80,22 +79,15 @@ private TrustStoreScope(String originalTrustStore, String originalTrustStorePass this.originalTrustStoreType = originalTrustStoreType; } - static TrustStoreScope use(String resourceName, String password, String storeType) { + static TrustStoreScope use() { String originalTrustStore = System.getProperty("javax.net.ssl.trustStore"); String originalTrustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword"); String originalTrustStoreType = System.getProperty("javax.net.ssl.trustStoreType"); - URL resource = GrpcTransportTest.class.getClassLoader().getResource(resourceName); - if (resource == null) { - throw new IllegalStateException("Missing TLS truststore resource: " + resourceName); - } - try { - System.setProperty("javax.net.ssl.trustStore", new java.io.File(resource.toURI()).getAbsolutePath()); - } catch (URISyntaxException ex) { - throw new IllegalStateException("Invalid TLS truststore resource path: " + resourceName, ex); - } - System.setProperty("javax.net.ssl.trustStorePassword", password); - System.setProperty("javax.net.ssl.trustStoreType", storeType); + Path trustStore = TestTlsMaterial.getInstance().trustStorePath(); + System.setProperty("javax.net.ssl.trustStore", trustStore.toAbsolutePath().toString()); + System.setProperty("javax.net.ssl.trustStorePassword", TestTlsMaterial.PASSWORD); + System.setProperty("javax.net.ssl.trustStoreType", TestTlsMaterial.STORE_TYPE); return new TrustStoreScope(originalTrustStore, originalTrustStorePassword, originalTrustStoreType); } diff --git a/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java index 78d73a0..54b137b 100644 --- a/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java +++ b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java @@ -3,6 +3,8 @@ import java.io.*; import java.net.*; import java.nio.file.*; +import java.security.*; +import java.security.cert.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; @@ -17,6 +19,7 @@ import com.microsoft.azure.functions.rpc.messages.*; import io.grpc.*; import io.grpc.netty.shaded.io.grpc.netty.*; +import io.grpc.netty.shaded.io.netty.handler.ssl.*; import io.grpc.stub.*; import org.apache.commons.lang3.tuple.*; @@ -34,9 +37,6 @@ public enum ServerTransport { private static final int RESPONSE_TIMEOUT_SECONDS = 10; private static final long RESPONSE_POLL_MILLIS = 100L; - private static final String TLS_RESOURCE_ROOT = "grpc-tls/"; - private static final String TLS_CERTIFICATE_RESOURCE = TLS_RESOURCE_ROOT + "localhost-cert.pem"; - private static final String TLS_PRIVATE_KEY_RESOURCE = TLS_RESOURCE_ROOT + "localhost-key.pem"; private int port; public FunctionsTestHost() throws Exception { @@ -67,16 +67,30 @@ private int populatePort() { } @PostConstruct - private void initializeServer() throws IOException { + private void initializeServer() throws Exception { ServerBuilder builder = this.serverTransport == ServerTransport.TLS - ? NettyServerBuilder.forPort(this.getPort()) - .sslContext(GrpcSslContexts.forServer(getTlsResource(TLS_CERTIFICATE_RESOURCE), getTlsResource(TLS_PRIVATE_KEY_RESOURCE)).build()) + ? NettyServerBuilder.forPort(this.getPort()).sslContext(buildServerSslContext()) : ServerBuilder.forPort(this.getPort()); this.grpcHost = new HostGrpcImplementation(); this.server = builder.addService(this.grpcHost).build(); this.server.start(); } + private static SslContext buildServerSslContext() throws Exception { + TestTlsMaterial material = TestTlsMaterial.getInstance(); + KeyStore keyStore = KeyStore.getInstance(TestTlsMaterial.STORE_TYPE); + try (InputStream in = Files.newInputStream(material.serverKeyStorePath())) { + keyStore.load(in, TestTlsMaterial.PASSWORD.toCharArray()); + } + PrivateKey privateKey = (PrivateKey) keyStore.getKey(TestTlsMaterial.ALIAS, TestTlsMaterial.PASSWORD.toCharArray()); + java.security.cert.Certificate[] chain = keyStore.getCertificateChain(TestTlsMaterial.ALIAS); + X509Certificate[] certificates = new X509Certificate[chain.length]; + for (int i = 0; i < chain.length; i++) { + certificates[i] = (X509Certificate) chain[i]; + } + return GrpcSslContexts.configure(SslContextBuilder.forServer(privateKey, certificates)).build(); + } + @PostConstruct private void initializeClient() throws Exception { this.client = new JavaWorkerClient(this); @@ -158,18 +172,6 @@ private void closeQuietly() { } } - private static File getTlsResource(String resourcePath) { - URL resource = FunctionsTestHost.class.getClassLoader().getResource(resourcePath); - if (resource == null) { - throw new IllegalStateException("Missing test TLS resource: " + resourcePath); - } - try { - return Paths.get(resource.toURI()).toFile(); - } catch (URISyntaxException ex) { - throw new IllegalStateException("Invalid test TLS resource path: " + resourcePath, ex); - } - } - private void throwIfListeningFailed() throws ExecutionException, InterruptedException { if (this.listeningTask != null && this.listeningTask.isDone()) { this.listeningTask.get(); diff --git a/src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java new file mode 100644 index 0000000..52defbc --- /dev/null +++ b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java @@ -0,0 +1,176 @@ +package com.microsoft.azure.functions.worker.test.utilities; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * Generates ephemeral TLS material (a self-signed {@code CN=localhost} certificate, + * its private key, and a matching truststore) for the gRPC transport tests. + * + *

The material is produced at runtime with the JDK's {@code keytool} so that no + * private keys are committed to the repository. {@code keytool} ships with every + * JDK (8 through 25), which keeps this portable across the CI Java matrix without + * pulling in a certificate-generation dependency such as Bouncy Castle.

+ * + *

A single instance is shared across the test server and client so that the + * server's certificate is trusted by the client. All files are written to a + * temporary directory that is deleted when the JVM exits.

+ */ +public final class TestTlsMaterial { + + /** Fixed alias/password; this is throwaway, per-run material with no security value. */ + public static final String ALIAS = "localhost"; + public static final String PASSWORD = "changeit"; + public static final String STORE_TYPE = "PKCS12"; + + private static final long KEYTOOL_TIMEOUT_SECONDS = 60; + + private static volatile TestTlsMaterial instance; + + private final Path serverKeyStore; + private final Path trustStore; + + private TestTlsMaterial() { + try { + Path directory = Files.createTempDirectory("grpc-tls-test"); + directory.toFile().deleteOnExit(); + + this.serverKeyStore = directory.resolve("server.p12"); + this.trustStore = directory.resolve("truststore.p12"); + Path certificate = directory.resolve("localhost-cert.pem"); + + generateKeyPair(this.serverKeyStore); + exportCertificate(this.serverKeyStore, certificate); + importCertificate(certificate, this.trustStore); + + this.serverKeyStore.toFile().deleteOnExit(); + this.trustStore.toFile().deleteOnExit(); + certificate.toFile().deleteOnExit(); + } catch (IOException | InterruptedException ex) { + if (ex instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new IllegalStateException("Failed to generate test TLS material", ex); + } + } + + public static TestTlsMaterial getInstance() { + TestTlsMaterial local = instance; + if (local == null) { + synchronized (TestTlsMaterial.class) { + local = instance; + if (local == null) { + local = new TestTlsMaterial(); + instance = local; + } + } + } + return local; + } + + /** Path to the PKCS12 keystore holding the server's private key and certificate. */ + public Path serverKeyStorePath() { + return this.serverKeyStore; + } + + /** Path to the PKCS12 truststore holding the server's certificate (for the client). */ + public Path trustStorePath() { + return this.trustStore; + } + + private static void generateKeyPair(Path keyStore) throws IOException, InterruptedException { + runKeytool( + "-genkeypair", + "-alias", ALIAS, + "-keyalg", "RSA", + "-keysize", "2048", + "-validity", "7300", + "-dname", "CN=localhost", + "-ext", "san=dns:localhost,ip:127.0.0.1", + "-keystore", keyStore.toString(), + "-storetype", STORE_TYPE, + "-storepass", PASSWORD, + "-keypass", PASSWORD); + } + + private static void exportCertificate(Path keyStore, Path certificate) throws IOException, InterruptedException { + runKeytool( + "-exportcert", + "-rfc", + "-alias", ALIAS, + "-keystore", keyStore.toString(), + "-storetype", STORE_TYPE, + "-storepass", PASSWORD, + "-file", certificate.toString()); + } + + private static void importCertificate(Path certificate, Path trustStore) throws IOException, InterruptedException { + runKeytool( + "-importcert", + "-noprompt", + "-alias", ALIAS, + "-file", certificate.toString(), + "-keystore", trustStore.toString(), + "-storetype", STORE_TYPE, + "-storepass", PASSWORD); + } + + private static void runKeytool(String... arguments) throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add(keytoolPath()); + for (String argument : arguments) { + command.add(argument); + } + + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + + // Drain output on a background thread so a misbehaving keytool cannot block + // us indefinitely; the timeout below then governs the overall wait. + StringBuilder output = new StringBuilder(); + Thread drainer = new Thread(() -> { + try (InputStream in = process.getInputStream()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int read; + while ((read = in.read(chunk)) != -1) { + buffer.write(chunk, 0, read); + } + synchronized (output) { + output.append(new String(buffer.toByteArray(), StandardCharsets.UTF_8)); + } + } catch (IOException ignored) { + // Output is best-effort; failures are surfaced via the exit code below. + } + }); + drainer.setDaemon(true); + drainer.start(); + + if (!process.waitFor(KEYTOOL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + process.destroyForcibly(); + throw new IllegalStateException("keytool timed out running: " + arguments[0]); + } + drainer.join(TimeUnit.SECONDS.toMillis(5)); + + if (process.exitValue() != 0) { + synchronized (output) { + throw new IllegalStateException( + "keytool " + arguments[0] + " failed (exit " + process.exitValue() + "):\n" + output); + } + } + } + + private static String keytoolPath() { + String javaHome = System.getProperty("java.home"); + boolean windows = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win"); + return Paths.get(javaHome, "bin", windows ? "keytool.exe" : "keytool").toString(); + } +} diff --git a/src/test/resources/grpc-tls/localhost-cert.pem b/src/test/resources/grpc-tls/localhost-cert.pem deleted file mode 100644 index 4d9d070..0000000 --- a/src/test/resources/grpc-tls/localhost-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDHzCCAgegAwIBAgIUUT0dnmqskpKcIS3wt+Fq+obG5GEwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDUxMjE3MjExMFoXDTM2MDUw -OTE3MjExMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAmRbV1NPyunri/+EN14nfIQhyY7grIRmUS5XTvsJhVQMg -/vP0rZ1H1I8f7mzkmyfDsB0i5putmz01lAHD5/Wfa0JEhVgmnwK20RsufjsWhwpY -tVpbfzgY5LJILSwz5Qw//Q2VDFiFXK7plVkfnmwrutUQ4AXuk55IuLDHlHxAI7vt -v/MxxlCri+EeoZcmTTVkLeMvtTf7Pw3upc8PBNNDYJOJvld4DXxz4e93ZhFmQGck -QMVeGGPsL+jmhu78pdfW+TcwlEJTvKBZ8xYTGaQbks6ZwXslYvuL0mI7XEajTSrM -Pw7vJeaJH85fETtZNfz3/IR4or88pyQQN9Cv0fY3xQIDAQABo2kwZzAdBgNVHQ4E -FgQUQgk8tVR0g3rgA6MRktL39pScY9UwHwYDVR0jBBgwFoAUQgk8tVR0g3rgA6MR -ktL39pScY9UwDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTALgglsb2NhbGhvc3Qw -DQYJKoZIhvcNAQELBQADggEBAJg1IQoiIV8qoLpVLbY5pvgkeClmFQsVwFTB0rLS -lU4ZkVkoIiYowQnb10WFpGdyhfI+WhZcLev2Dmm7PJ9Jp0H0vC5U0ebNLzMJdwjr -HjJcIuueYJDJ3YelEIzO2qa4R977PCvBPLHEi7/KRdSbAOz6lf6Yp+OPe46gJHP1 -XwaKt628oPd7A4FdBTF7lUfcLdPUnW9Glhg7VfB6aX6o5+Mup+QEt96xlXg6Ua2L -xYw8AJ75rAmQLwu05uzyaTGlHE7BXtsP+bEBAz4CXVv1z5i1UJfB6N/aWB1VOHKN -2r1x1fMUCNLpsoTdtmkwAmkmzx4bfyWtIIK+hAxTm7bmjGk= ------END CERTIFICATE----- diff --git a/src/test/resources/grpc-tls/localhost-key.pem b/src/test/resources/grpc-tls/localhost-key.pem deleted file mode 100644 index 976e744..0000000 --- a/src/test/resources/grpc-tls/localhost-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZFtXU0/K6euL/ -4Q3Xid8hCHJjuCshGZRLldO+wmFVAyD+8/StnUfUjx/ubOSbJ8OwHSLmm62bPTWU -AcPn9Z9rQkSFWCafArbRGy5+OxaHCli1Wlt/OBjkskgtLDPlDD/9DZUMWIVcrumV -WR+ebCu61RDgBe6Tnki4sMeUfEAju+2/8zHGUKuL4R6hlyZNNWQt4y+1N/s/De6l -zw8E00Ngk4m+V3gNfHPh73dmEWZAZyRAxV4YY+wv6OaG7vyl19b5NzCUQlO8oFnz -FhMZpBuSzpnBeyVi+4vSYjtcRqNNKsw/Du8l5okfzl8RO1k1/Pf8hHiivzynJBA3 -0K/R9jfFAgMBAAECggEANvM/Zdl+MvmRKY+6zDcs5EqH5Mtij8sCs+7fxoU3MrCg -02L13Kur8Nw+9fIYTKkFUN3kfSo8MpDR/oJzs3sy8ekjd0mg80qiHITJN342I9rO -5Km+Vffo14424iAPsJOpFEgfzAKqPA58waLv+omRWMrJ99+pN0uFhuXNfbrruudX -zgCMWtw7gqciTEOrcy2GLyKOClfg6AIn/9F1Z3yrQBrHW7TZjPhyHZi3PeIeXAz4 -hVjj5zHP8GI8P4O/kP6gFfB5x/1PXGErGOSl3HOieKse7JTMu7NCtgLEEshFAjOd -z4EsAGzJb0FdyH1eX9+WhsvVkbTTnek3fhBCMIJ/MQKBgQDLvpbTbRLdbxH6pDIx -I53jBpDU1x+QU5j1lvLZJtvdMnsJFA43zamSqwmIzuIAs9POzEGIP586TCr5PXBi -NrAz9mYZC1ye59ZFq0JVundbCWEO+YxuR5glgqjubWdfwFzFynCyuuZTqmPozGnY -wSOjIE7g0Vw6aJVXJq0eXG1EAwKBgQDAWlUSyXSVZ7INXFr+Dh0gd12HhQMYmxEz -twtXEfZTGwHqlxZOo0/G/LYVWPdIqkUw86aaKorlfgg5yzbJ6tfgxiE71W7VPiBR -X9Cefz16th2rQfM6Y1fszimV1Dpf3HU5TuN/ZK4tlEqsfK01yfjofPCjNL1c2BQr -sHYkUi5elwKBgBy8LZN2D7IRRyzdWYLarhrlwylxia8WSz1f47JCq8GfrACUxoiS -Rfc8jiSwYOmOczH4Vsm7h152fZ0XUDFZ2zII709a7d4vfmXnCH0Exm6dfQXapjar -fEbWDbNK1MiJXcw7h/d9KpzkLCEaK1d5regE13sXq/VE6MMY3lOo33Q3AoGAamsP -mh8+ktIV3fJ0nQ3t62JeqnVaayiPcc8ZRQi5AO12N/Vy7/rGTk7N5i2cUeVx9k02 -pSBYS/NYVbEqFLgKy16SUGoasXt3oc2iu62ls9hBvdf02x7PLEI7G5uY2CQ97oDI -uFhZTPo3/gnUQmgFf4pwD7tD8LPTJQCxvBKDeO0CgYEApeC2lxH4FekJEA4iIBG+ -InRXX20QCLqI2d4xMOHHDEWaulhDEkjs9HEErA+3HpkK5hOTsEIU6GENrpc3P2JY -iFaO/gJiFEErkXsSaDRRaROTKI27qA0JPWzrfDHH4G59qpVVH6KWIYJvWbsHHo49 -MSFfMNCXhE9polVRU6EyWb0= ------END PRIVATE KEY----- diff --git a/src/test/resources/grpc-tls/localhost-truststore.p12 b/src/test/resources/grpc-tls/localhost-truststore.p12 deleted file mode 100644 index cb4c21c18330c0fd5991d06ff34fa81dbacbe18e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1190 zcmV;X1X=qqf&`)h0Ru3C1WX1ADuzgg_YDCD0ic2eJp_UTIWU3*H86q%F$M`LhDe6@ z4FLxRpn?P=^2`Yw2hW8Bt2LUi<1_>&LNQU+thDZTr0|Wso1P}T|3~Qx<7z|1E9~_2KxM{%$%h>p6br4%#iBZ z9>^IC`s}0w3*pw}2*0GS+Pg$k1cQ6T3~ZPTZga(wVR#^eabs;yFrZBWr-a~X4#Q&^ z*xgZGuVN}%PkWvTr;&a!g3?)Ep(6LRG-4{4MR$?ghA>9~27u=7e#|x`sxjkw;ztkY zMp}q|j!wH4-E%63!iy0Oq?zYWpxPzpeDfP0(G3XdpI*5Ot7lF!As2!RY6}`fXQO)G zN^Dn_uGftZt;w+RO9mGD{4~qtWKEEv9?2h=YKTaIyEUt_5&k7DN$~@z%5;gfZivt0yxoDHnVp^Qf<~0bMcWl4xO8b= zdE8$k;JWql^=mq!I94O^RZ`L z%JG>yIo$M;)K%6Iq=b}gv#)nslLAF^Q%Rldllm(D@GUc(i&VEFh8F8%Lt$slK*GO7 z;-P)ECm*_hm%kI$ndqOdWL39Bjh-PlUJT#ueySut3OE1fwx~cWe`-_`2|5>T2A1L+ zR|XlzzEmw(;h)aL^)kFEWRRN&OUFBC9|}N89YRe~H!mXoh>)IPFQjfhp&tnIhm7d7 zlFFc$8Lic8I^RaTvHQD1n{O_nzn`GQTvKJEuD?-l7p^vVmLY- zJsN=r=U9(3bcJgLpjw{ZW|$Pt&DP!L>rjtlpq*KEH z8IXTkEbWn2&rj)4N`#eW%0thzq=ae=Rl|o~K%pcCh{jOW2~3py0{cak)V$qfK+isB z+Xi1>ZUg>-4|cAJmUWwPCra1*=tlA2)~9A`hF;M1)u7DKM^Pp#$u?#xdyGM!;^ zSRDR``~ED`6<>1a-^CPs!r^B@fxXrt>s&BRFflL<1_@w>NC9O71OfpC00bZ&iU)@1 zB8W)-l)jyF{HrQ|py-ygpLWE_SdD6&EzOt&6n&+Fu>{$dsaX)(AZM2D64dXK!2$v& E5NL)SEC2ui From 2cfd9ff1f651d21c64c8eb9c923be7fab4b727ba Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Fri, 19 Jun 2026 12:32:15 -0400 Subject: [PATCH 2/2] Address review feedback on gRPC test TLS material - Replace wildcard imports introduced by this PR with explicit imports (FunctionsTestHost, GrpcTransportTest); drop a now-unused java.net import. - Make buildServerSslContext an instance method. - Narrow initializeServer/buildServerSslContext throws to GeneralSecurityException and IOException instead of Exception. - Reduce the generated cert validity from 7300 days to 2; it only needs to outlive a single test run. - Throw IOException (not IllegalStateException) on keytool timeout/non-zero exit so the constructor's catch wraps all failures in one message. - Validate keytool is present and executable before invoking it. --- .../functional/tests/GrpcTransportTest.java | 3 +-- .../worker/test/utilities/FunctionsTestHost.java | 13 ++++++++----- .../worker/test/utilities/TestTlsMaterial.java | 15 ++++++++++----- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java b/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java index 4b7e76c..8f2e82e 100644 --- a/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java +++ b/src/test/java/com/microsoft/azure/functions/worker/functional/tests/GrpcTransportTest.java @@ -1,7 +1,6 @@ package com.microsoft.azure.functions.worker.functional.tests; -import java.net.*; -import java.nio.file.*; +import java.nio.file.Path; import java.util.concurrent.*; import javax.net.ssl.*; diff --git a/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java index 54b137b..f6c4748 100644 --- a/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java +++ b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/FunctionsTestHost.java @@ -3,8 +3,10 @@ import java.io.*; import java.net.*; import java.nio.file.*; -import java.security.*; -import java.security.cert.*; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; @@ -19,7 +21,8 @@ import com.microsoft.azure.functions.rpc.messages.*; import io.grpc.*; import io.grpc.netty.shaded.io.grpc.netty.*; -import io.grpc.netty.shaded.io.netty.handler.ssl.*; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; import io.grpc.stub.*; import org.apache.commons.lang3.tuple.*; @@ -67,7 +70,7 @@ private int populatePort() { } @PostConstruct - private void initializeServer() throws Exception { + private void initializeServer() throws GeneralSecurityException, IOException { ServerBuilder builder = this.serverTransport == ServerTransport.TLS ? NettyServerBuilder.forPort(this.getPort()).sslContext(buildServerSslContext()) : ServerBuilder.forPort(this.getPort()); @@ -76,7 +79,7 @@ private void initializeServer() throws Exception { this.server.start(); } - private static SslContext buildServerSslContext() throws Exception { + private SslContext buildServerSslContext() throws GeneralSecurityException, IOException { TestTlsMaterial material = TestTlsMaterial.getInstance(); KeyStore keyStore = KeyStore.getInstance(TestTlsMaterial.STORE_TYPE); try (InputStream in = Files.newInputStream(material.serverKeyStorePath())) { diff --git a/src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java index 52defbc..8f0ac0a 100644 --- a/src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java +++ b/src/test/java/com/microsoft/azure/functions/worker/test/utilities/TestTlsMaterial.java @@ -93,7 +93,8 @@ private static void generateKeyPair(Path keyStore) throws IOException, Interrupt "-alias", ALIAS, "-keyalg", "RSA", "-keysize", "2048", - "-validity", "7300", + // Short-lived: the material only needs to outlive a single test run. + "-validity", "2", "-dname", "CN=localhost", "-ext", "san=dns:localhost,ip:127.0.0.1", "-keystore", keyStore.toString(), @@ -156,21 +157,25 @@ private static void runKeytool(String... arguments) throws IOException, Interrup if (!process.waitFor(KEYTOOL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { process.destroyForcibly(); - throw new IllegalStateException("keytool timed out running: " + arguments[0]); + throw new IOException("keytool timed out running: " + arguments[0]); } drainer.join(TimeUnit.SECONDS.toMillis(5)); if (process.exitValue() != 0) { synchronized (output) { - throw new IllegalStateException( + throw new IOException( "keytool " + arguments[0] + " failed (exit " + process.exitValue() + "):\n" + output); } } } - private static String keytoolPath() { + private static String keytoolPath() throws IOException { String javaHome = System.getProperty("java.home"); boolean windows = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win"); - return Paths.get(javaHome, "bin", windows ? "keytool.exe" : "keytool").toString(); + Path keytool = Paths.get(javaHome, "bin", windows ? "keytool.exe" : "keytool"); + if (!Files.isExecutable(keytool)) { + throw new IOException("keytool not found or not executable at: " + keytool); + } + return keytool.toString(); } }