Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrieve the peer certificate from the certificate array #1079

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions grpc-server-spring-boot-starter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

}
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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.ManagedChannel;
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 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);
jazdw marked this conversation as resolved.
Show resolved Hide resolved
this.client = certificateHelper.leafCertificate("CN=Client", root);
this.clientWithIntermediate = certificateHelper.leafCertificate("CN=ClientWithIntermediate", intermediate);
}

@Test
void readAuthentication() throws Exception {
var serverKeyManager = new AdvancedTlsX509KeyManager();
serverKeyManager.updateIdentityCredentials(server.keyPair().getPrivate(),
new X509Certificate[] {server.certificate(), intermediate.certificate()});

var clientKeyManager = new AdvancedTlsX509KeyManager();
clientKeyManager.updateIdentityCredentials(client.keyPair().getPrivate(),
new X509Certificate[] {client.certificate()});

var authentication = readAuthentication(serverKeyManager, clientKeyManager);
assertNotNull(authentication);
assertInstanceOf(X509Certificate.class, authentication.getCredentials());
X509Certificate certificate = (X509Certificate) authentication.getCredentials();
assertEquals("CN=Client", certificate.getSubjectX500Principal().toString());
}

@Test
void readAuthenticationWithIntermediateCertificate() throws Exception {
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)
.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();
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<Void>();
client.check(HealthCheckRequest.getDefaultInstance(), new FutureStreamObserver(clientCallComplete));
clientCallComplete.get();

return interceptor.authenticationFuture().get();
} finally {
if (channel != null) {
channel.shutdownNow();
}
server.shutdownNow();
}
}

record FutureStreamObserver(CompletableFuture<Void> clientCallComplete) implements StreamObserver<HealthCheckResponse> {

@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<Authentication> authenticationFuture) implements ServerInterceptor {

public AuthenticationReaderServerInterceptor() {
this(new SSLContextGrpcAuthenticationReader(), new CompletableFuture<>());
}

@Override
public <ReqT, RespT> Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
var authentication = reader.readAuthentication(call, headers);
authenticationFuture.complete(authentication);
return next.startCall(call, headers);
}
}}