Support routing based on TLS client certificate presence/signer for mixed-auth endpoints on shared port#1769
Conversation
|
|
||
| /** | ||
| * Resolves the named trusted certificate entries from the vault by alias. | ||
| * <p> | ||
| * Used by callers that need to identify which configured trust alias signed a peer | ||
| * certificate (e.g. for routing decisions), without exposing the underlying certificate | ||
| * contents through configuration. Implementations should return only the aliases that | ||
| * resolve to a trusted certificate entry in the vault; aliases that do not resolve are | ||
| * omitted from the returned map. | ||
| * </p> | ||
| * | ||
| * @param certRefs list of vault entry names identifying the trusted certificates to resolve | ||
| * @return a map from resolved alias to the corresponding X.509 certificate, or {@code null} | ||
| * if no aliases could be resolved | ||
| */ | ||
| default Map<String, X509Certificate> resolveTrust( | ||
| List<String> certRefs) | ||
| { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
I'd like us to try to solve this without changing the VaultHandler abstraction if possible.
See comments below.
|
|
||
| boolean clientCertPresent = false; | ||
| List<String> clientCertTrustAliases = null; | ||
| try | ||
| { | ||
| Certificate[] peerCerts = tlsSession.getPeerCertificates(); | ||
| if (peerCerts.length > 0 && binding != null) | ||
| { | ||
| X509Certificate leaf = (X509Certificate) peerCerts[0]; | ||
| clientCertPresent = true; | ||
| clientCertTrustAliases = binding.resolveTrustAliases(leaf.getIssuerX500Principal()); | ||
| } | ||
| } | ||
| catch (SSLPeerUnverifiedException ex) | ||
| { | ||
| // no client cert presented under mutual: requested | ||
| } | ||
|
|
||
| final TlsRouteConfig route = binding != null | ||
| ? binding.resolve(authorization, tlsHostname, tlsProtocol, port, clientCertPresent, clientCertTrustAliases) | ||
| : null; |
There was a problem hiding this comment.
The current VaultHandler abstract maps from aliases to TrustManagerFactory which we use to configure the SSLEngine.
Once that handshake completes successfully, we then want to know if we trust the client certificate to follow a specific route.
I think we can do that by creating a different TrustManagerFactory instance (via the VaultHandler) per route, for just the trusted aliases on that route. Then we can assess if the client certificate is trusted by using the TrustManager[] for that route, iterating over it to find the X509TrustManager instances, and checking if the client is trusted, if so follow the route.
For the mutual:none case, we just need to check that the client did not present a peer certificate chain.
So in terms of APIs, we can probably just pass the client certificate chain to binding.resolve(...) and let it figure out which routes are valid using the TrustManagerFactory for the trusted routes (mutual:required), and verifying no client certificate chain for the mutual:none routes.
| Certificate[] clientCerts = null; | ||
| try | ||
| { | ||
| clientCerts = tlsSession.getPeerCertificates(); | ||
| } | ||
| catch (SSLPeerUnverifiedException ex) | ||
| { | ||
| // no client cert presented under mutual: requested | ||
| } |
There was a problem hiding this comment.
Lets move this to a static method to reduce the noise here regarding handling of the exception.
There was a problem hiding this comment.
Also, let's only attempt to get the peer certificates if the options mutual is not none, i.e. if it is required or requested. This will avoid triggering a predictable exception in the common case when mutual TLS is not configured.
| @Test | ||
| @Configuration("server.mutual.signer.routed.yaml") | ||
| @Specification({ | ||
| "${net}/server.mutual.auth/client", | ||
| "${app}/server.mutual.auth/server"}) | ||
| public void shouldRouteByClientCertSigner() throws Exception | ||
| { | ||
| k3po.finish(); | ||
| } | ||
|
|
||
| @Test | ||
| @Configuration("server.mutual.signer.routed.yaml") | ||
| @Specification({ | ||
| "${net}/server.mutual.cert.absent/client", | ||
| "${app}/server.mutual.cert.absent/server"}) | ||
| public void shouldRouteWhenClientCertAbsent() throws Exception | ||
| { | ||
| k3po.finish(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Let's also add a negative test, where the client certificate is not one of the trusted aliases, so even though the TLS handshake succeeds, we reject because there is no valid route.
| return TlsState.closed(state) ? NULL_STREAM : stream; | ||
| } | ||
|
|
||
| private static Certificate[] peerCertificates( |
There was a problem hiding this comment.
Rename to clientCertificates and move private static method after private non-static methods.
| @@ -0,0 +1,30 @@ | |||
| # | |||
There was a problem hiding this comment.
This should have a peer script, and a corresponding spec IT method.
Fixes #1697