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

netty: Per-rpc call option authority verification against peer cert subject names #11724

Open
wants to merge 25 commits into
base: master
Choose a base branch
from

Conversation

kannanjgithub
Copy link
Contributor

No description provided.

@kannanjgithub kannanjgithub requested a review from ejona86 December 4, 2024 13:23
examples/example-tls/build.gradle Outdated Show resolved Hide resolved
util/src/main/java/io/grpc/util/CertificateUtils.java Outdated Show resolved Hide resolved
}
if (!peerVerified) {
return new FailingClientStream(Status.INTERNAL.withDescription(
"Peer hostname verification failed for authority"), tracers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the very least, we will want to include the failed authority in this error message. It'd probably be good to include e from that exception above as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -105,6 +112,8 @@ class NettyClientTransport implements ConnectionClientTransport {
private final ChannelLogger channelLogger;
private final boolean useGetForSafeMethods;
private final Ticker ticker;
private final ConcurrentHashMap<String, Boolean> authoritiesAllowedForPeer =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want to limit the maximum size of this map. That may mean we swap it to a LinkedHashMap and protect it with a lock, so we can drop LRU.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the max size be hardcoded to 10000? We don't need it to be configurable via channel params, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think hard-coding it to 100 would be fine. 10 is probably sufficient, so 100 should be plenty. If it is exceeded, it is almost certainly an unbounded number of authorities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

x509ExtendedTrustManager = tlsCreds.getTrustManagers().stream().filter(
trustManager -> trustManager instanceof X509ExtendedTrustManager).findFirst();
} else if (tlsCreds.getRootCertificates() != null) {
builder.trustManager(new ByteArrayInputStream(tlsCreds.getRootCertificates()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very wary of assuming we're using the same implementation as the builder. I suggest have all these cases produce TrustManager[] tms, then after the if-elses call builder.trustManager(tms) and filter tms for x509ExtendedTrustManager.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand this comment. When you say assuming same implementation as the builder are you referring to this line

builder.trustManager(new ByteArrayInputStream(tlsCreds.getRootCertificates()));

since the other 2 lines don't reference the builder. This is an existing statement. Implementation of TrustManager as the builder?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builder.trustManager() is having the builder use some certs for its trust. But then you use a different trust manager implementation for the per-RPC verification. I'm wary of them being different. There could be a performance reason to do so; I've not checked recently if tcnative has a fast path for simple file-based verification (to use boringssl/openssl instead of calling back into Java). But in general, creating two different instances (the code you added, and the existing builder.trustManager()) and hoping they behave the same is worrisome.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@kannanjgithub
Copy link
Contributor Author

I don't know what this error is about:

java/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java:621:18: 'public' modifier out of order with the JLS suggestions. [ModifierOrder]

@ejona86
Copy link
Member

ejona86 commented Dec 9, 2024

synchronized public boolean mayBeVerifyAuthority should have "public" first: public synchronized boolean mayBeVerifyAuthority.

JLS == Java Language Specification

The relevant part of the style guide:
https://google.github.io/styleguide/javaguide.html#s4.8.7-modifiers
But checkstyle also links to some useful things:
https://checkstyle.sourceforge.io/checks/modifier/modifierorder.html

peerVerified = false;
}
authoritiesAllowedForPeer.put(authority, peerVerified);
if (authoritiesAllowedForPeer.size() > MAX_AUTHORITIES_CACHE_SIZE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* @throws CertificateException if certificates have a problem
*/
default boolean mayBeVerifyAuthority(String authority)
throws SSLPeerUnverifiedException, CertificateException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to pollute this API with SSL-isms. In general, our internal APIs don't use exceptions. It'd be better to return a Status here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

*/
default boolean mayBeVerifyAuthority(String authority)
throws SSLPeerUnverifiedException, CertificateException {
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to have a default, it should fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

} catch (UnsupportedOperationException ex) {
logger.log(Level.FINE,
"Can't allow authority override in rpc when X509ExtendedTrustManager is not available.",
callOptions.getAuthority());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This argument is unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
} catch (UnsupportedOperationException ex) {
logger.log(Level.FINE,
"Can't allow authority override in rpc when X509ExtendedTrustManager is not available.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log doesn't seem to have a purpose, as we're about to fail the RPC itself. So the error is being handled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

logger.log(Level.FINE, errMsg);
return new FailingClientStream(Status.INTERNAL.withDescription(errMsg), tracers);
}
} catch (UnsupportedOperationException ex) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ex is being lost

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer throwing exception.

String errMsg = String.format("Peer hostname verification failed for authority '%s'.",
callOptions.getAuthority());
logger.log(Level.FINE, errMsg);
return new FailingClientStream(Status.INTERNAL.withDescription(errMsg), tracers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNAVAILABLE would be better here. INTERNAL is when we detect a bug in gRPC and can't communicate or such. TLS failures normally show up as UNAVAILABLE.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

try {
if (callOptions.getAuthority() != null && !negotiator.mayBeVerifyAuthority(
callOptions.getAuthority())) {
String errMsg = String.format("Peer hostname verification failed for authority '%s'.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we don't include ending punctuation in status error messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -1105,4 +1209,245 @@ protected final void fireProtocolNegotiationEvent(ChannelHandlerContext ctx) {
ctx.fireUserEventTriggered(pne);
}
}

static final class SslEngineWrapper extends SSLEngine {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of boilerplate. It'd be good to move these to their own files. You can have it just be the "wrapper", and then still override getPeerHost()/getHandshakeSession() in the file here.

It's interesting that only getSSLParameters() uses sslEngine. Maybe the separate file could be a NoopSslEngine?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -18,7 +18,7 @@ tasks.named("jar").configure {
dependencies {
api project(':grpc-api'),
libraries.netty.codec.http2
implementation project(':grpc-core'),
implementation project(':grpc-core'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* A no-op implementation of SslEngine, to facilitate overriding only the required methods in
* specific implementations.
*/
public class NoopSslEngine extends SSLEngine {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package-private

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java Outdated Show resolved Hide resolved
netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java Outdated Show resolved Hide resolved
}

public void setSslEngine(SSLEngine sslEngine) {
this.sslEngine = sslEngine;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all we are using this engine for is SSLParameters, let's copy the parameters here and not try to use the sslEngine from another thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also used for the SSLSession for getting the peer certificate chain.


@Override
public synchronized Status verifyAuthority(@Nonnull String authority) {
if (!canVerifyAuthorityOverride()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline the check here. That makes it more obvious that the error message returned in incorrect some of the time, and it doesn't seem having the check be named is helping anything. If you do want to give the check a name, assign it to a boolean canVerifyAuthorityOverride local variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

try {
verifyAuthorityAllowedForPeerCert(authority);
peerVerificationStatus = Status.OK;
} catch (SSLPeerUnverifiedException | CertificateException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't throw away e. status.withCause(e)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

peerVerificationStatus = Status.OK;
} catch (SSLPeerUnverifiedException | CertificateException e) {
peerVerificationStatus = Status.UNAVAILABLE.withDescription(
String.format("Peer hostname verification failed for authority '%s'",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks the same as what someone might see if the connection failed. Give a hint it was a per-RPC override?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

private void verifyAuthorityAllowedForPeerCert(String authority)
throws SSLPeerUnverifiedException, CertificateException {
SSLEngine sslEngineWrapper = new SslEngineWrapper(sslEngine, authority);
// The typecasting of Certificate to X509Certificate should work because this method will only
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just because a X509ExtendedTrustManager is available, doesn't mean it was used. An array of TrustManagers are available and the implementation used one of them, but maybe not the X509 one.

Now, we do have a good understanding we are using TLS and thus X509. But that's a very different assumption.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
x509ExtendedTrustManager = Arrays.stream(tmf.getTrustManagers())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Netty can be used on Android, although we discourage it. That means no java.util.streams.

That should apply to Optional as well... how is that already a dependency? Ah, it was added recently in #11600. We'll remove that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants