Skip to content

Commit cdab410

Browse files
netty: Per-rpc authority verification against peer cert subject names (grpc#11724)
Per-rpc verification of authority specified via call options or set by the LB API against peer cert's subject names.
1 parent 57124d6 commit cdab410

19 files changed

+1228
-83
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.internal;
18+
19+
import io.grpc.Status;
20+
21+
/** Verifier for the outgoing authority pseudo-header against peer cert. */
22+
public interface AuthorityVerifier {
23+
Status verifyAuthority(String authority);
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2024 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.internal;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.security.GeneralSecurityException;
22+
import java.security.KeyStore;
23+
import java.security.cert.Certificate;
24+
import java.security.cert.CertificateException;
25+
import java.security.cert.CertificateFactory;
26+
import java.security.cert.X509Certificate;
27+
import java.util.Collection;
28+
import javax.net.ssl.TrustManager;
29+
import javax.net.ssl.TrustManagerFactory;
30+
import javax.security.auth.x500.X500Principal;
31+
32+
/**
33+
* Contains certificate/key PEM file utility method(s) for internal usage.
34+
*/
35+
public final class CertificateUtils {
36+
/**
37+
* Creates X509TrustManagers using the provided CA certs.
38+
*/
39+
public static TrustManager[] createTrustManager(InputStream rootCerts)
40+
throws GeneralSecurityException {
41+
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
42+
try {
43+
ks.load(null, null);
44+
} catch (IOException ex) {
45+
// Shouldn't really happen, as we're not loading any data.
46+
throw new GeneralSecurityException(ex);
47+
}
48+
X509Certificate[] certs = CertificateUtils.getX509Certificates(rootCerts);
49+
for (X509Certificate cert : certs) {
50+
X500Principal principal = cert.getSubjectX500Principal();
51+
ks.setCertificateEntry(principal.getName("RFC2253"), cert);
52+
}
53+
54+
TrustManagerFactory trustManagerFactory =
55+
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
56+
trustManagerFactory.init(ks);
57+
return trustManagerFactory.getTrustManagers();
58+
}
59+
60+
private static X509Certificate[] getX509Certificates(InputStream inputStream)
61+
throws CertificateException {
62+
CertificateFactory factory = CertificateFactory.getInstance("X.509");
63+
Collection<? extends Certificate> certs = factory.generateCertificates(inputStream);
64+
return certs.toArray(new X509Certificate[0]);
65+
}
66+
}

core/src/main/java/io/grpc/internal/GrpcAttributes.java

+3
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,8 @@ public final class GrpcAttributes {
4242
public static final Attributes.Key<Attributes> ATTR_CLIENT_EAG_ATTRS =
4343
Attributes.Key.create("io.grpc.internal.GrpcAttributes.clientEagAttrs");
4444

45+
public static final Attributes.Key<AuthorityVerifier> ATTR_AUTHORITY_VERIFIER =
46+
Attributes.Key.create("io.grpc.internal.GrpcAttributes.authorityVerifier");
47+
4548
private GrpcAttributes() {}
4649
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2024 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.internal;
18+
19+
import java.security.Principal;
20+
import java.security.cert.Certificate;
21+
import javax.net.ssl.SSLPeerUnverifiedException;
22+
import javax.net.ssl.SSLSession;
23+
import javax.net.ssl.SSLSessionContext;
24+
25+
/** A no-op ssl session, to facilitate overriding only the required methods in specific
26+
* implementations.
27+
*/
28+
public class NoopSslSession implements SSLSession {
29+
@Override
30+
public byte[] getId() {
31+
return new byte[0];
32+
}
33+
34+
@Override
35+
public SSLSessionContext getSessionContext() {
36+
return null;
37+
}
38+
39+
@Override
40+
@SuppressWarnings("deprecation")
41+
public javax.security.cert.X509Certificate[] getPeerCertificateChain() {
42+
throw new UnsupportedOperationException("This method is deprecated and marked for removal. "
43+
+ "Use the getPeerCertificates() method instead.");
44+
}
45+
46+
@Override
47+
public long getCreationTime() {
48+
return 0;
49+
}
50+
51+
@Override
52+
public long getLastAccessedTime() {
53+
return 0;
54+
}
55+
56+
@Override
57+
public void invalidate() {
58+
}
59+
60+
@Override
61+
public boolean isValid() {
62+
return false;
63+
}
64+
65+
@Override
66+
public void putValue(String s, Object o) {
67+
}
68+
69+
@Override
70+
public Object getValue(String s) {
71+
return null;
72+
}
73+
74+
@Override
75+
public void removeValue(String s) {
76+
}
77+
78+
@Override
79+
public String[] getValueNames() {
80+
return new String[0];
81+
}
82+
83+
@Override
84+
public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
85+
return new Certificate[0];
86+
}
87+
88+
@Override
89+
public Certificate[] getLocalCertificates() {
90+
return new Certificate[0];
91+
}
92+
93+
@Override
94+
public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
95+
return null;
96+
}
97+
98+
@Override
99+
public Principal getLocalPrincipal() {
100+
return null;
101+
}
102+
103+
@Override
104+
public String getCipherSuite() {
105+
return null;
106+
}
107+
108+
@Override
109+
public String getProtocol() {
110+
return null;
111+
}
112+
113+
@Override
114+
public String getPeerHost() {
115+
return null;
116+
}
117+
118+
@Override
119+
public int getPeerPort() {
120+
return 0;
121+
}
122+
123+
@Override
124+
public int getPacketBufferSize() {
125+
return 0;
126+
}
127+
128+
@Override
129+
public int getApplicationBufferSize() {
130+
return 0;
131+
}
132+
}

netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java

+10
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ private GrpcHttp2OutboundHeaders(AsciiString[] preHeaders, byte[][] serializedMe
6666
this.preHeaders = preHeaders;
6767
}
6868

69+
@Override
70+
public CharSequence authority() {
71+
for (int i = 0; i < preHeaders.length / 2; i++) {
72+
if (preHeaders[i * 2].equals(Http2Headers.PseudoHeaderName.AUTHORITY.value())) {
73+
return preHeaders[i * 2 + 1];
74+
}
75+
}
76+
return null;
77+
}
78+
6979
@Override
7080
@SuppressWarnings("ReferenceEquality") // STATUS.value() never changes.
7181
public CharSequence status() {

netty/src/main/java/io/grpc/netty/InternalProtocolNegotiators.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static InternalProtocolNegotiator.ProtocolNegotiator tls(SslContext sslCo
4444
ObjectPool<? extends Executor> executorPool,
4545
Optional<Runnable> handshakeCompleteRunnable) {
4646
final io.grpc.netty.ProtocolNegotiator negotiator = ProtocolNegotiators.tls(sslContext,
47-
executorPool, handshakeCompleteRunnable);
47+
executorPool, handshakeCompleteRunnable, null);
4848
final class TlsNegotiator implements InternalProtocolNegotiator.ProtocolNegotiator {
4949

5050
@Override
@@ -170,7 +170,7 @@ public static ChannelHandler clientTlsHandler(
170170
ChannelHandler next, SslContext sslContext, String authority,
171171
ChannelLogger negotiationLogger) {
172172
return new ClientTlsHandler(next, sslContext, authority, null, negotiationLogger,
173-
Optional.absent());
173+
Optional.absent(), null, null);
174174
}
175175

176176
public static class ProtocolNegotiationHandler

netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ static ProtocolNegotiator createProtocolNegotiatorByType(
652652
case PLAINTEXT_UPGRADE:
653653
return ProtocolNegotiators.plaintextUpgrade();
654654
case TLS:
655-
return ProtocolNegotiators.tls(sslContext, executorPool, Optional.absent());
655+
return ProtocolNegotiators.tls(sslContext, executorPool, Optional.absent(), null);
656656
default:
657657
throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType);
658658
}

netty/src/main/java/io/grpc/netty/NettyClientHandler.java

+62
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.grpc.Attributes;
2929
import io.grpc.ChannelLogger;
3030
import io.grpc.InternalChannelz;
31+
import io.grpc.InternalStatus;
3132
import io.grpc.Metadata;
3233
import io.grpc.Status;
3334
import io.grpc.StatusException;
@@ -83,6 +84,8 @@
8384
import io.perfmark.Tag;
8485
import io.perfmark.TaskCloseable;
8586
import java.nio.channels.ClosedChannelException;
87+
import java.util.LinkedHashMap;
88+
import java.util.Map;
8689
import java.util.concurrent.Executor;
8790
import java.util.logging.Level;
8891
import java.util.logging.Logger;
@@ -94,6 +97,8 @@
9497
*/
9598
class NettyClientHandler extends AbstractNettyHandler {
9699
private static final Logger logger = Logger.getLogger(NettyClientHandler.class.getName());
100+
static boolean enablePerRpcAuthorityCheck =
101+
GrpcUtil.getFlag("GRPC_ENABLE_PER_RPC_AUTHORITY_CHECK", false);
97102

98103
/**
99104
* A message that simply passes through the channel without any real processing. It is useful to
@@ -128,6 +133,13 @@ protected void handleNotInUse() {
128133
lifecycleManager.notifyInUse(false);
129134
}
130135
};
136+
private final Map<String, Status> peerVerificationResults =
137+
new LinkedHashMap<String, Status>() {
138+
@Override
139+
protected boolean removeEldestEntry(Map.Entry<String, Status> eldest) {
140+
return size() > 100;
141+
}
142+
};
131143

132144
private WriteQueue clientWriteQueue;
133145
private Http2Ping ping;
@@ -591,6 +603,56 @@ private void createStream(CreateStreamCommand command, ChannelPromise promise)
591603
return;
592604
}
593605

606+
CharSequence authorityHeader = command.headers().authority();
607+
if (authorityHeader == null) {
608+
Status authorityVerificationStatus = Status.UNAVAILABLE.withDescription(
609+
"Missing authority header");
610+
command.stream().setNonExistent();
611+
command.stream().transportReportStatus(
612+
Status.UNAVAILABLE, RpcProgress.PROCESSED, true, new Metadata());
613+
promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace(
614+
authorityVerificationStatus, null));
615+
return;
616+
}
617+
// No need to verify authority for the rpc outgoing header if it is same as the authority
618+
// for the transport
619+
if (!authority.contentEquals(authorityHeader)) {
620+
Status authorityVerificationStatus = peerVerificationResults.get(
621+
authorityHeader.toString());
622+
if (authorityVerificationStatus == null) {
623+
if (attributes.get(GrpcAttributes.ATTR_AUTHORITY_VERIFIER) == null) {
624+
authorityVerificationStatus = Status.UNAVAILABLE.withDescription(
625+
"Authority verifier not found to verify authority");
626+
command.stream().setNonExistent();
627+
command.stream().transportReportStatus(
628+
authorityVerificationStatus, RpcProgress.PROCESSED, true, new Metadata());
629+
promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace(
630+
authorityVerificationStatus, null));
631+
return;
632+
}
633+
authorityVerificationStatus = attributes.get(GrpcAttributes.ATTR_AUTHORITY_VERIFIER)
634+
.verifyAuthority(authorityHeader.toString());
635+
peerVerificationResults.put(authorityHeader.toString(), authorityVerificationStatus);
636+
if (!authorityVerificationStatus.isOk() && !enablePerRpcAuthorityCheck) {
637+
logger.log(Level.WARNING, String.format("%s.%s",
638+
authorityVerificationStatus.getDescription(),
639+
enablePerRpcAuthorityCheck
640+
? "" : " This will be an error in the future."),
641+
InternalStatus.asRuntimeExceptionWithoutStacktrace(
642+
authorityVerificationStatus, null));
643+
}
644+
}
645+
if (!authorityVerificationStatus.isOk()) {
646+
if (enablePerRpcAuthorityCheck) {
647+
command.stream().setNonExistent();
648+
command.stream().transportReportStatus(
649+
authorityVerificationStatus, RpcProgress.PROCESSED, true, new Metadata());
650+
promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace(
651+
authorityVerificationStatus, null));
652+
return;
653+
}
654+
}
655+
}
594656
// Get the stream ID for the new stream.
595657
int streamId;
596658
try {

netty/src/main/java/io/grpc/netty/NettyClientTransport.java

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class NettyClientTransport implements ConnectionClientTransport {
106106
private final boolean useGetForSafeMethods;
107107
private final Ticker ticker;
108108

109+
109110
NettyClientTransport(
110111
SocketAddress address,
111112
ChannelFactory<? extends Channel> channelFactory,

netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ public static ChannelCredentials create(SslContext sslContext) {
3434
Preconditions.checkArgument(sslContext.isClient(),
3535
"Server SSL context can not be used for client channel");
3636
GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator());
37-
return NettyChannelCredentials.create(ProtocolNegotiators.tlsClientFactory(sslContext));
37+
return NettyChannelCredentials.create(ProtocolNegotiators.tlsClientFactory(sslContext, null));
3838
}
3939
}

0 commit comments

Comments
 (0)