-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
base: master
Are you sure you want to change the base?
Conversation
} | ||
if (!peerVerified) { | ||
return new FailingClientStream(Status.INTERNAL.withDescription( | ||
"Peer hostname verification failed for authority"), tracers); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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())); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
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] |
JLS == Java Language Specification The relevant part of the style guide: |
peerVerified = false; | ||
} | ||
authoritiesAllowedForPeer.put(authority, peerVerified); | ||
if (authoritiesAllowedForPeer.size() > MAX_AUTHORITIES_CACHE_SIZE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Override removeEldestEntry()
?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This argument is unused.
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ex
is being lost
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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'.", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
netty/build.gradle
Outdated
@@ -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'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Package-private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
} | ||
|
||
public void setSslEngine(SSLEngine sslEngine) { | ||
this.sslEngine = sslEngine; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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'", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
No description provided.