From 5c7a296a42a7e0f7e5c44c431249a3a4f80eba15 Mon Sep 17 00:00:00 2001 From: Jared Wiltshire Date: Wed, 27 Mar 2024 08:18:32 -0600 Subject: [PATCH 1/4] Retrieve the peer certificate from the certificate array --- .../authentication/SSLContextGrpcAuthenticationReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java b/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java index 0842218fe..ff825847e 100644 --- a/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java +++ b/grpc-server-spring-boot-starter/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java @@ -53,7 +53,7 @@ public Authentication readAuthentication(final ServerCall call, final Meta log.trace("Peer not verified via certificate", e); return null; } - return fromCertificate(certs[certs.length - 1]); + return fromCertificate(certs[0]); } /** From 6ad616320eb4919087dbd6fa907d545486bff74b Mon Sep 17 00:00:00 2001 From: Jared Wiltshire Date: Fri, 29 Mar 2024 13:34:14 -0600 Subject: [PATCH 2/4] Add tests for SSLContextGrpcAuthenticationReader --- grpc-server-spring-boot-starter/build.gradle | 2 + .../authentication/CertificateHelper.java | 170 ++++++++++++++++ ...SLContextGrpcAuthenticationReaderTest.java | 187 ++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/CertificateHelper.java create mode 100644 grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java diff --git a/grpc-server-spring-boot-starter/build.gradle b/grpc-server-spring-boot-starter/build.gradle index c3f39af1e..f969922ae 100644 --- a/grpc-server-spring-boot-starter/build.gradle +++ b/grpc-server-spring-boot-starter/build.gradle @@ -33,4 +33,6 @@ dependencies { testImplementation 'io.grpc:grpc-testing' testImplementation('org.springframework.boot:spring-boot-starter-test') + testImplementation 'org.bouncycastle:bcpkix-jdk18on:1.77' + } diff --git a/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/CertificateHelper.java b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/CertificateHelper.java new file mode 100644 index 000000000..ab7e40492 --- /dev/null +++ b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/CertificateHelper.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016-2024 The gRPC-Spring Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.devh.boot.grpc.server.security.authentication; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNamesBuilder; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +public class CertificateHelper { + + static final Provider PROVIDER = new BouncyCastleProvider(); + static { + Security.addProvider(PROVIDER); + } + + final SecureRandom secureRandom = new SecureRandom(); + + CertificateAndKeys rootCertificate(String subject) + throws NoSuchAlgorithmException, CertIOException, CertificateException, OperatorCreationException { + var keyPair = keyPair(); + var subjectName = new X500Name(subject); + var certBuilder = buildCertificate(subjectName, subjectName, keyPair.getPublic()); + var extensionUtils = new JcaX509ExtensionUtils(); + + var subjectKeyIdentifier = extensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()); + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, subjectKeyIdentifier); + + var keyUsage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign); + certBuilder.addExtension(Extension.keyUsage, true, keyUsage); + + var constraints = new BasicConstraints(0); + certBuilder.addExtension(Extension.basicConstraints, true, constraints); + + var certificate = signCertificate(certBuilder, keyPair.getPrivate()); + return new CertificateAndKeys(certificate, keyPair); + } + + CertificateAndKeys intermediateCertificate(String subject, CertificateAndKeys issuer) + throws NoSuchAlgorithmException, CertIOException, CertificateException, OperatorCreationException { + var keyPair = keyPair(); + var issuerCertificate = issuer.certificate(); + var certBuilder = buildCertificate(new JcaX509CertificateHolder(issuerCertificate).getSubject(), + new X500Name(subject), keyPair.getPublic()); + var extensionUtils = new JcaX509ExtensionUtils(); + + var subjectKeyIdentifier = extensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()); + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, subjectKeyIdentifier); + + var authorityKeyIdentifier = extensionUtils.createAuthorityKeyIdentifier(issuerCertificate); + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, authorityKeyIdentifier); + + var keyUsage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign); + certBuilder.addExtension(Extension.keyUsage, true, keyUsage); + + var constraints = new BasicConstraints(0); + certBuilder.addExtension(Extension.basicConstraints, true, constraints); + + var certificate = signCertificate(certBuilder, issuer.keyPair().getPrivate()); + return new CertificateAndKeys(certificate, keyPair); + } + + CertificateAndKeys leafCertificate(String subject, CertificateAndKeys issuer) + throws NoSuchAlgorithmException, CertIOException, CertificateException, OperatorCreationException { + var keyPair = keyPair(); + var issuerCertificate = issuer.certificate(); + var certBuilder = buildCertificate(new JcaX509CertificateHolder(issuerCertificate).getSubject(), + new X500Name(subject), keyPair.getPublic()); + var extensionUtils = new JcaX509ExtensionUtils(); + + var subjectKeyIdentifier = extensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()); + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, subjectKeyIdentifier); + + var authorityKeyIdentifier = extensionUtils.createAuthorityKeyIdentifier(issuerCertificate); + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, authorityKeyIdentifier); + + var keyUsage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment); + certBuilder.addExtension(Extension.keyUsage, true, keyUsage); + + var extendedKeyUsage = + new ExtendedKeyUsage(new KeyPurposeId[] {KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}); + certBuilder.addExtension(Extension.extendedKeyUsage, false, extendedKeyUsage); + + certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNamesBuilder() + .addName(new GeneralName(GeneralName.dNSName, "localhost")) + .addName(new GeneralName(GeneralName.iPAddress, "127.0.0.1")) + .build()); + + var certificate = signCertificate(certBuilder, issuer.keyPair().getPrivate()); + return new CertificateAndKeys(certificate, keyPair); + } + + private X509v3CertificateBuilder buildCertificate(X500Name issuer, X500Name subject, PublicKey publicKey) { + var publicKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + var now = ZonedDateTime.now(); + return new X509v3CertificateBuilder( + issuer, + new BigInteger(160, secureRandom), + Date.from(now.toInstant()), + Date.from(now.plusDays(1).toInstant()), + subject, + publicKeyInfo); + } + + private X509Certificate signCertificate(X509v3CertificateBuilder certificateBuilder, PrivateKey privateKey) + throws OperatorCreationException, CertificateException { + var signer = contentSigner(privateKey); + var certificateHolder = certificateBuilder.build(signer); + return new JcaX509CertificateConverter() + .setProvider(PROVIDER) + .getCertificate(certificateHolder); + } + + private KeyPair keyPair() throws NoSuchAlgorithmException { + var keyPairGenerator = KeyPairGenerator.getInstance("RSA", PROVIDER); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } + + private ContentSigner contentSigner(PrivateKey privateKey) throws OperatorCreationException { + return new JcaContentSignerBuilder("SHA256WithRSA") + .setProvider(PROVIDER) + .build(privateKey); + } + + record CertificateAndKeys(X509Certificate certificate, KeyPair keyPair) {} + +} diff --git a/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java new file mode 100644 index 000000000..0812918f4 --- /dev/null +++ b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2016-2024 The gRPC-Spring Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.devh.boot.grpc.server.security.authentication; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.security.cert.X509Certificate; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.Authentication; + +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.TlsChannelCredentials; +import io.grpc.TlsServerCredentials; +import io.grpc.TlsServerCredentials.ClientAuth; +import io.grpc.health.v1.HealthCheckRequest; +import io.grpc.health.v1.HealthCheckResponse; +import io.grpc.health.v1.HealthGrpc; +import io.grpc.protobuf.services.HealthStatusManager; +import io.grpc.stub.StreamObserver; +import io.grpc.util.AdvancedTlsX509KeyManager; +import io.grpc.util.AdvancedTlsX509TrustManager; +import net.devh.boot.grpc.server.security.authentication.CertificateHelper.CertificateAndKeys; + +class SSLContextGrpcAuthenticationReaderTest { + + final CertificateHelper certificateHelper = new CertificateHelper(); + CertificateAndKeys root; + CertificateAndKeys intermediate; + CertificateAndKeys server; + CertificateAndKeys client; + CertificateAndKeys clientNoIntermediate; + + @BeforeEach + void setUp() throws Exception { + this.root = certificateHelper.rootCertificate("CN=Root"); + this.intermediate = certificateHelper.intermediateCertificate("CN=Intermediate", root); + this.server = certificateHelper.leafCertificate("CN=Server", intermediate); + this.client = certificateHelper.leafCertificate("CN=Client", intermediate); + this.clientNoIntermediate = certificateHelper.leafCertificate("CN=NoIntermediate", root); + } + + @Test + void readAuthentication() throws Exception { + var trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + trustManager.updateTrustCredentials(new X509Certificate[] {root.certificate()}); + + var serverKeyManager = new AdvancedTlsX509KeyManager(); + serverKeyManager.updateIdentityCredentials(server.keyPair().getPrivate(), + new X509Certificate[] {server.certificate(), intermediate.certificate()}); + var serverCredentials = TlsServerCredentials.newBuilder() + .trustManager(trustManager) + .keyManager(serverKeyManager) + .clientAuth(ClientAuth.REQUIRE) + .build(); + + var interceptor = new AuthenticationReaderServerInterceptor(); + var healthStatusManager = new HealthStatusManager(); + var server = Grpc.newServerBuilderForPort(0, serverCredentials) + .addService(healthStatusManager.getHealthService()) + .intercept(interceptor) + .build(); + server.start(); + + var clientKeyManager = new AdvancedTlsX509KeyManager(); + clientKeyManager.updateIdentityCredentials(clientNoIntermediate.keyPair().getPrivate(), + new X509Certificate[] {clientNoIntermediate.certificate()}); + var clientCredentials = TlsChannelCredentials.newBuilder() + .trustManager(trustManager) + .keyManager(clientKeyManager) + .build(); + + var channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), clientCredentials).build(); + var client = HealthGrpc.newStub(channel); + + var clientCallComplete = new CompletableFuture(); + client.check(HealthCheckRequest.getDefaultInstance(), new FutureStreamObserver(clientCallComplete)); + clientCallComplete.get(); + + var authentication = interceptor.authenticationFuture().get(); + assertNotNull(authentication); + assertInstanceOf(X509Certificate.class, authentication.getCredentials()); + X509Certificate certificate = (X509Certificate) authentication.getCredentials(); + assertEquals("CN=NoIntermediate", certificate.getSubjectX500Principal().toString()); + } + + @Test + void readAuthenticationWithIntermediateCertificate() throws Exception { + var trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + trustManager.updateTrustCredentials(new X509Certificate[] {root.certificate()}); + + var serverKeyManager = new AdvancedTlsX509KeyManager(); + serverKeyManager.updateIdentityCredentials(server.keyPair().getPrivate(), + new X509Certificate[] {server.certificate(), intermediate.certificate()}); + var serverCredentials = TlsServerCredentials.newBuilder() + .trustManager(trustManager) + .keyManager(serverKeyManager) + .clientAuth(ClientAuth.REQUIRE) + .build(); + + var interceptor = new AuthenticationReaderServerInterceptor(); + var healthStatusManager = new HealthStatusManager(); + var server = Grpc.newServerBuilderForPort(0, serverCredentials) + .addService(healthStatusManager.getHealthService()) + .intercept(interceptor) + .build(); + server.start(); + + var clientKeyManager = new AdvancedTlsX509KeyManager(); + clientKeyManager.updateIdentityCredentials(client.keyPair().getPrivate(), + new X509Certificate[] {client.certificate(), intermediate.certificate()}); + var clientCredentials = TlsChannelCredentials.newBuilder() + .trustManager(trustManager) + .keyManager(clientKeyManager) + .build(); + + var channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), clientCredentials).build(); + var client = HealthGrpc.newStub(channel); + + var clientCallComplete = new CompletableFuture(); + client.check(HealthCheckRequest.getDefaultInstance(), new FutureStreamObserver(clientCallComplete)); + clientCallComplete.get(); + + var authentication = interceptor.authenticationFuture().get(); + assertNotNull(authentication); + assertInstanceOf(X509Certificate.class, authentication.getCredentials()); + X509Certificate certificate = (X509Certificate) authentication.getCredentials(); + assertEquals("CN=Client", certificate.getSubjectX500Principal().toString()); + } + + record FutureStreamObserver(CompletableFuture clientCallComplete) + implements StreamObserver { + + @Override + public void onNext(HealthCheckResponse healthCheckResponse) {} + + @Override + public void onError(Throwable throwable) { + clientCallComplete.completeExceptionally(throwable); + } + + @Override + public void onCompleted() { + clientCallComplete.complete(null); + } + + } + + record AuthenticationReaderServerInterceptor( + SSLContextGrpcAuthenticationReader reader, + CompletableFuture authenticationFuture) implements ServerInterceptor { + + public AuthenticationReaderServerInterceptor() { + this(new SSLContextGrpcAuthenticationReader(), new CompletableFuture<>()); + } + + @Override + public Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + var authentication = reader.readAuthentication(call, headers); + authenticationFuture.complete(authentication); + return next.startCall(call, headers); + } +}} From 3431cc53782bb49eb022458664c4d3f6799d9c5a Mon Sep 17 00:00:00 2001 From: Jared Wiltshire Date: Fri, 29 Mar 2024 13:56:08 -0600 Subject: [PATCH 3/4] Extract common logic into a method, shutdown server and channel --- ...SLContextGrpcAuthenticationReaderTest.java | 109 ++++++++---------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java index 0812918f4..9cc1475ba 100644 --- a/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java +++ b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java @@ -28,6 +28,7 @@ import org.springframework.security.core.Authentication; import io.grpc.Grpc; +import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCall.Listener; @@ -52,69 +53,56 @@ class SSLContextGrpcAuthenticationReaderTest { CertificateAndKeys intermediate; CertificateAndKeys server; CertificateAndKeys client; - CertificateAndKeys clientNoIntermediate; + CertificateAndKeys clientWithIntermediate; @BeforeEach void setUp() throws Exception { this.root = certificateHelper.rootCertificate("CN=Root"); this.intermediate = certificateHelper.intermediateCertificate("CN=Intermediate", root); this.server = certificateHelper.leafCertificate("CN=Server", intermediate); - this.client = certificateHelper.leafCertificate("CN=Client", intermediate); - this.clientNoIntermediate = certificateHelper.leafCertificate("CN=NoIntermediate", root); + this.client = certificateHelper.leafCertificate("CN=Client", root); + this.clientWithIntermediate = certificateHelper.leafCertificate("CN=ClientWithIntermediate", intermediate); } @Test void readAuthentication() throws Exception { - var trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); - trustManager.updateTrustCredentials(new X509Certificate[] {root.certificate()}); - var serverKeyManager = new AdvancedTlsX509KeyManager(); serverKeyManager.updateIdentityCredentials(server.keyPair().getPrivate(), new X509Certificate[] {server.certificate(), intermediate.certificate()}); - var serverCredentials = TlsServerCredentials.newBuilder() - .trustManager(trustManager) - .keyManager(serverKeyManager) - .clientAuth(ClientAuth.REQUIRE) - .build(); - - var interceptor = new AuthenticationReaderServerInterceptor(); - var healthStatusManager = new HealthStatusManager(); - var server = Grpc.newServerBuilderForPort(0, serverCredentials) - .addService(healthStatusManager.getHealthService()) - .intercept(interceptor) - .build(); - server.start(); var clientKeyManager = new AdvancedTlsX509KeyManager(); - clientKeyManager.updateIdentityCredentials(clientNoIntermediate.keyPair().getPrivate(), - new X509Certificate[] {clientNoIntermediate.certificate()}); - var clientCredentials = TlsChannelCredentials.newBuilder() - .trustManager(trustManager) - .keyManager(clientKeyManager) - .build(); - - var channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), clientCredentials).build(); - var client = HealthGrpc.newStub(channel); - - var clientCallComplete = new CompletableFuture(); - client.check(HealthCheckRequest.getDefaultInstance(), new FutureStreamObserver(clientCallComplete)); - clientCallComplete.get(); + clientKeyManager.updateIdentityCredentials(client.keyPair().getPrivate(), + new X509Certificate[] {client.certificate()}); - var authentication = interceptor.authenticationFuture().get(); + var authentication = readAuthentication(serverKeyManager, clientKeyManager); assertNotNull(authentication); assertInstanceOf(X509Certificate.class, authentication.getCredentials()); X509Certificate certificate = (X509Certificate) authentication.getCredentials(); - assertEquals("CN=NoIntermediate", certificate.getSubjectX500Principal().toString()); + assertEquals("CN=Client", certificate.getSubjectX500Principal().toString()); } @Test void readAuthenticationWithIntermediateCertificate() throws Exception { - var trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); - trustManager.updateTrustCredentials(new X509Certificate[] {root.certificate()}); - var serverKeyManager = new AdvancedTlsX509KeyManager(); serverKeyManager.updateIdentityCredentials(server.keyPair().getPrivate(), new X509Certificate[] {server.certificate(), intermediate.certificate()}); + + var clientKeyManager = new AdvancedTlsX509KeyManager(); + clientKeyManager.updateIdentityCredentials(clientWithIntermediate.keyPair().getPrivate(), + new X509Certificate[] {clientWithIntermediate.certificate(), intermediate.certificate()}); + + var authentication = readAuthentication(serverKeyManager, clientKeyManager); + assertNotNull(authentication); + assertInstanceOf(X509Certificate.class, authentication.getCredentials()); + X509Certificate certificate = (X509Certificate) authentication.getCredentials(); + assertEquals("CN=ClientWithIntermediate", certificate.getSubjectX500Principal().toString()); + } + + private Authentication readAuthentication(AdvancedTlsX509KeyManager serverKeyManager, + AdvancedTlsX509KeyManager clientKeyManager) throws Exception { + var trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + trustManager.updateTrustCredentials(new X509Certificate[] {root.certificate()}); + var serverCredentials = TlsServerCredentials.newBuilder() .trustManager(trustManager) .keyManager(serverKeyManager) @@ -128,31 +116,30 @@ void readAuthenticationWithIntermediateCertificate() throws Exception { .intercept(interceptor) .build(); server.start(); - - var clientKeyManager = new AdvancedTlsX509KeyManager(); - clientKeyManager.updateIdentityCredentials(client.keyPair().getPrivate(), - new X509Certificate[] {client.certificate(), intermediate.certificate()}); - var clientCredentials = TlsChannelCredentials.newBuilder() - .trustManager(trustManager) - .keyManager(clientKeyManager) - .build(); - - var channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), clientCredentials).build(); - var client = HealthGrpc.newStub(channel); - - var clientCallComplete = new CompletableFuture(); - client.check(HealthCheckRequest.getDefaultInstance(), new FutureStreamObserver(clientCallComplete)); - clientCallComplete.get(); - - var authentication = interceptor.authenticationFuture().get(); - assertNotNull(authentication); - assertInstanceOf(X509Certificate.class, authentication.getCredentials()); - X509Certificate certificate = (X509Certificate) authentication.getCredentials(); - assertEquals("CN=Client", certificate.getSubjectX500Principal().toString()); + ManagedChannel channel = null; + try { + var clientCredentials = TlsChannelCredentials.newBuilder() + .trustManager(trustManager) + .keyManager(clientKeyManager) + .build(); + + channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), clientCredentials).build(); + var client = HealthGrpc.newStub(channel); + + var clientCallComplete = new CompletableFuture(); + client.check(HealthCheckRequest.getDefaultInstance(), new FutureStreamObserver(clientCallComplete)); + clientCallComplete.get(); + + return interceptor.authenticationFuture().get(); + } finally { + if (channel != null) { + channel.shutdownNow(); + } + server.shutdownNow(); + } } - record FutureStreamObserver(CompletableFuture clientCallComplete) - implements StreamObserver { + record FutureStreamObserver(CompletableFuture clientCallComplete) implements StreamObserver { @Override public void onNext(HealthCheckResponse healthCheckResponse) {} @@ -174,7 +161,7 @@ record AuthenticationReaderServerInterceptor( CompletableFuture authenticationFuture) implements ServerInterceptor { public AuthenticationReaderServerInterceptor() { - this(new SSLContextGrpcAuthenticationReader(), new CompletableFuture<>()); + this(new SSLContextGrpcAuthenticationReader(), new CompletableFuture<>()); } @Override From 6f712a235883c58fea34790a1f64e02aecee2a60 Mon Sep 17 00:00:00 2001 From: Jared Wiltshire Date: Fri, 29 Mar 2024 16:17:59 -0600 Subject: [PATCH 4/4] Sign server certificate with root certificate --- .../SSLContextGrpcAuthenticationReaderTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java index 9cc1475ba..566381187 100644 --- a/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java +++ b/grpc-server-spring-boot-starter/src/test/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReaderTest.java @@ -59,7 +59,7 @@ class SSLContextGrpcAuthenticationReaderTest { void setUp() throws Exception { this.root = certificateHelper.rootCertificate("CN=Root"); this.intermediate = certificateHelper.intermediateCertificate("CN=Intermediate", root); - this.server = certificateHelper.leafCertificate("CN=Server", intermediate); + this.server = certificateHelper.leafCertificate("CN=Server", root); this.client = certificateHelper.leafCertificate("CN=Client", root); this.clientWithIntermediate = certificateHelper.leafCertificate("CN=ClientWithIntermediate", intermediate); } @@ -68,7 +68,7 @@ void setUp() throws Exception { void readAuthentication() throws Exception { var serverKeyManager = new AdvancedTlsX509KeyManager(); serverKeyManager.updateIdentityCredentials(server.keyPair().getPrivate(), - new X509Certificate[] {server.certificate(), intermediate.certificate()}); + new X509Certificate[] {server.certificate()}); var clientKeyManager = new AdvancedTlsX509KeyManager(); clientKeyManager.updateIdentityCredentials(client.keyPair().getPrivate(), @@ -85,7 +85,7 @@ void readAuthentication() throws Exception { void readAuthenticationWithIntermediateCertificate() throws Exception { var serverKeyManager = new AdvancedTlsX509KeyManager(); serverKeyManager.updateIdentityCredentials(server.keyPair().getPrivate(), - new X509Certificate[] {server.certificate(), intermediate.certificate()}); + new X509Certificate[] {server.certificate()}); var clientKeyManager = new AdvancedTlsX509KeyManager(); clientKeyManager.updateIdentityCredentials(clientWithIntermediate.keyPair().getPrivate(),