Skip to content

Commit 115457a

Browse files
authored
Support routing based on TLS client certificate presence/signer for mixed-auth endpoints on shared port (#1769)
1 parent e1fc7e6 commit 115457a

35 files changed

Lines changed: 986 additions & 45 deletions

File tree

runtime/binding-tls/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@
117117
<configuration>
118118
<excludes>
119119
<exclude>**/keys</exclude>
120+
<exclude>**/keys2</exclude>
120121
<exclude>**/trust</exclude>
121122
<exclude>**/signers</exclude>
123+
<exclude>**/signers2</exclude>
122124
<exclude>**/*.csr</exclude>
123125
<exclude>**/*.crt</exclude>
124126
</excludes>

runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/config/TlsConditionConfig.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.aklivity.zilla.runtime.binding.tls.config;
1717

18+
import java.util.List;
1819
import java.util.function.Function;
1920

2021
import io.aklivity.zilla.runtime.engine.config.ConditionConfig;
@@ -24,6 +25,8 @@ public final class TlsConditionConfig extends ConditionConfig
2425
public final String authority;
2526
public final String alpn;
2627
public final int[] ports;
28+
public final List<String> trust;
29+
public final TlsMutualConfig mutual;
2730

2831
public static TlsConditionConfigBuilder<TlsConditionConfig> builder()
2932
{
@@ -39,10 +42,14 @@ public static <T> TlsConditionConfigBuilder<T> builder(
3942
TlsConditionConfig(
4043
String authority,
4144
String alpn,
42-
int[] ports)
45+
int[] ports,
46+
List<String> trust,
47+
TlsMutualConfig mutual)
4348
{
4449
this.authority = authority;
4550
this.alpn = alpn;
4651
this.ports = ports;
52+
this.trust = trust;
53+
this.mutual = mutual;
4754
}
4855
}

runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/config/TlsConditionConfigBuilder.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package io.aklivity.zilla.runtime.binding.tls.config;
1717

18+
import java.util.ArrayList;
19+
import java.util.List;
1820
import java.util.function.Function;
1921

2022
import org.agrona.collections.IntArrayList;
@@ -29,6 +31,8 @@ public final class TlsConditionConfigBuilder<T> extends ConfigBuilder<T, TlsCond
2931
private String authority;
3032
private String alpn;
3133
private IntArrayList ports;
34+
private List<String> trust;
35+
private TlsMutualConfig mutual;
3236

3337
TlsConditionConfigBuilder(
3438
Function<ConditionConfig, T> mapper)
@@ -75,10 +79,35 @@ public TlsConditionConfigBuilder<T> ports(
7579
return this;
7680
}
7781

82+
public TlsConditionConfigBuilder<T> trust(
83+
String alias)
84+
{
85+
if (trust == null)
86+
{
87+
trust = new ArrayList<>();
88+
}
89+
trust.add(alias);
90+
return this;
91+
}
92+
93+
public TlsConditionConfigBuilder<T> trust(
94+
List<String> aliases)
95+
{
96+
this.trust = aliases != null ? new ArrayList<>(aliases) : null;
97+
return this;
98+
}
99+
100+
public TlsConditionConfigBuilder<T> mutual(
101+
TlsMutualConfig mutual)
102+
{
103+
this.mutual = mutual;
104+
return this;
105+
}
106+
78107
@Override
79108
public T build()
80109
{
81110
final int[] portsArray = ports != null ? ports.toIntArray() : null;
82-
return mapper.apply(new TlsConditionConfig(authority, alpn, portsArray));
111+
return mapper.apply(new TlsConditionConfig(authority, alpn, portsArray, trust, mutual));
83112
}
84113
}

runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/internal/config/TlsBindingConfig.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import java.security.KeyStore;
2828
import java.security.SecureRandom;
29+
import java.security.cert.Certificate;
2930
import java.util.List;
3031
import java.util.Optional;
3132
import java.util.stream.Collectors;
@@ -128,6 +129,11 @@ public void init(
128129
this.context = context;
129130
this.clientHttpsIdentification = config.clientHttpsIdentification();
130131
this.clientServerNameIndication = config.clientServerNameIndication();
132+
133+
for (TlsRouteConfig route : routes)
134+
{
135+
route.init(vault);
136+
}
131137
}
132138
catch (Exception ex)
133139
{
@@ -148,7 +154,7 @@ public TlsRouteConfig resolve(
148154

149155
int port = resolveDestinationPort(beginEx);
150156

151-
return resolve(authorization, authority, alpn, port);
157+
return resolve(authorization, authority, alpn, port, null);
152158
}
153159

154160
public TlsRouteConfig resolvePortOnly(
@@ -193,10 +199,12 @@ public TlsRouteConfig resolve(
193199
long authorization,
194200
String hostname,
195201
String alpn,
196-
int port)
202+
int port,
203+
Certificate[] clientCerts)
197204
{
198205
return routes.stream()
199-
.filter(r -> r.authorized(authorization) && r.matches(hostname, alpn, port))
206+
.filter(r -> r.authorized(authorization) &&
207+
r.matches(hostname, alpn, port, clientCerts))
200208
.findFirst()
201209
.orElse(null);
202210
}
@@ -364,7 +372,7 @@ private String selectAlpn(
364372
}
365373

366374
if (route.authorized(authorization) &&
367-
route.matches(authority, protocol, port))
375+
route.matchesIgnoringCert(authority, protocol, port))
368376
{
369377
selected = protocol;
370378
break;
@@ -386,7 +394,7 @@ private String selectAlpn(
386394
}
387395

388396
if (route.authorized(authorization) &&
389-
route.matches(null, protocol, port))
397+
route.matchesIgnoringCert(null, protocol, port))
390398
{
391399
selected = protocol;
392400
break;

runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/internal/config/TlsConditionConfigAdapter.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package io.aklivity.zilla.runtime.binding.tls.internal.config;
1717

18+
import java.util.ArrayList;
19+
import java.util.List;
1820
import java.util.stream.IntStream;
1921

2022
import jakarta.json.Json;
@@ -32,6 +34,7 @@
3234

3335
import io.aklivity.zilla.runtime.binding.tls.config.TlsConditionConfig;
3436
import io.aklivity.zilla.runtime.binding.tls.config.TlsConditionConfigBuilder;
37+
import io.aklivity.zilla.runtime.binding.tls.config.TlsMutualConfig;
3538
import io.aklivity.zilla.runtime.binding.tls.internal.TlsBinding;
3639
import io.aklivity.zilla.runtime.engine.config.ConditionConfig;
3740
import io.aklivity.zilla.runtime.engine.config.ConditionConfigAdapterSpi;
@@ -41,6 +44,8 @@ public final class TlsConditionConfigAdapter implements ConditionConfigAdapterSp
4144
private static final String AUTHORITY_NAME = "authority";
4245
private static final String ALPN_NAME = "alpn";
4346
private static final String PORT_NAME = "port";
47+
private static final String TRUST_NAME = "trust";
48+
private static final String MUTUAL_NAME = "mutual";
4449

4550
@Override
4651
public String type()
@@ -84,6 +89,21 @@ public JsonObject adaptToJson(
8489
}
8590
}
8691

92+
if (tlsCondition.trust != null)
93+
{
94+
JsonArrayBuilder trustArray = Json.createArrayBuilder();
95+
for (String alias : tlsCondition.trust)
96+
{
97+
trustArray.add(alias);
98+
}
99+
object.add(TRUST_NAME, trustArray);
100+
}
101+
102+
if (tlsCondition.mutual != null)
103+
{
104+
object.add(MUTUAL_NAME, tlsCondition.mutual.name().toLowerCase());
105+
}
106+
87107
return object.build();
88108
}
89109

@@ -126,6 +146,19 @@ public ConditionConfig adaptFromJson(
126146
tlsCondition.ports(ports);
127147
}
128148

149+
if (object.containsKey(TRUST_NAME))
150+
{
151+
JsonArray trustArray = object.getJsonArray(TRUST_NAME);
152+
List<String> trust = new ArrayList<>(trustArray.size());
153+
trustArray.forEach(value -> trust.add(((JsonString) value).getString()));
154+
tlsCondition.trust(trust);
155+
}
156+
157+
if (object.containsKey(MUTUAL_NAME))
158+
{
159+
tlsCondition.mutual(TlsMutualConfig.valueOf(object.getString(MUTUAL_NAME).toUpperCase()));
160+
}
161+
129162
return tlsCondition.build();
130163
}
131164

runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/internal/config/TlsConditionMatcher.java

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,58 @@
1515
*/
1616
package io.aklivity.zilla.runtime.binding.tls.internal.config;
1717

18+
import java.security.cert.Certificate;
19+
import java.security.cert.CertificateException;
20+
import java.security.cert.X509Certificate;
1821
import java.util.Arrays;
1922
import java.util.regex.Matcher;
2023
import java.util.regex.Pattern;
2124

25+
import javax.net.ssl.TrustManager;
26+
import javax.net.ssl.TrustManagerFactory;
27+
import javax.net.ssl.X509TrustManager;
28+
2229
import org.agrona.collections.IntHashSet;
2330

2431
import io.aklivity.zilla.runtime.binding.tls.config.TlsConditionConfig;
32+
import io.aklivity.zilla.runtime.binding.tls.config.TlsMutualConfig;
2533

2634
public final class TlsConditionMatcher
2735
{
2836
public final Matcher authorityMatch;
2937
public final Matcher alpnMatch;
3038
public final IntHashSet ports;
39+
public final TlsMutualConfig mutual;
40+
public final TrustManagerFactory trustFactory;
3141

3242
public TlsConditionMatcher(
33-
TlsConditionConfig condition)
43+
TlsConditionConfig condition,
44+
TrustManagerFactory trustFactory)
3445
{
3546
this.authorityMatch = condition.authority != null ? asMatcher(condition.authority) : null;
3647
this.alpnMatch = condition.alpn != null ? asMatcher(condition.alpn) : null;
3748
this.ports = condition.ports != null ? asIntHashSet(condition.ports) : null;
49+
// `trust` implies `mutual: required`
50+
this.mutual = condition.mutual != null
51+
? condition.mutual
52+
: condition.trust != null ? TlsMutualConfig.REQUIRED : null;
53+
this.trustFactory = trustFactory;
3854
}
3955

4056
public boolean matches(
57+
String authority,
58+
String alpn,
59+
int port,
60+
Certificate[] clientCerts)
61+
{
62+
return matchesAuthority(authority) &&
63+
matchesAlpn(alpn) &&
64+
matchesPort(port) &&
65+
matchesMutual(clientCerts) &&
66+
matchesTrust(clientCerts);
67+
}
68+
69+
public boolean matchesIgnoringCert(
4170
String authority,
4271
String alpn,
4372
int port)
@@ -71,6 +100,72 @@ private boolean matchesPort(
71100
return ports == null || ports.contains(port);
72101
}
73102

103+
private boolean matchesMutual(
104+
Certificate[] clientCerts)
105+
{
106+
boolean matches = true;
107+
boolean present = clientCerts != null && clientCerts.length > 0;
108+
if (mutual == TlsMutualConfig.REQUIRED)
109+
{
110+
matches = present;
111+
}
112+
else if (mutual == TlsMutualConfig.NONE)
113+
{
114+
matches = !present;
115+
}
116+
return matches;
117+
}
118+
119+
private boolean matchesTrust(
120+
Certificate[] clientCerts)
121+
{
122+
boolean matches = true;
123+
if (trustFactory != null)
124+
{
125+
matches = false;
126+
if (clientCerts != null && clientCerts.length > 0)
127+
{
128+
X509Certificate[] chain = asX509Chain(clientCerts);
129+
if (chain != null)
130+
{
131+
String authType = chain[0].getPublicKey().getAlgorithm();
132+
for (TrustManager tm : trustFactory.getTrustManagers())
133+
{
134+
if (tm instanceof X509TrustManager x509tm)
135+
{
136+
try
137+
{
138+
x509tm.checkClientTrusted(chain, authType);
139+
matches = true;
140+
break;
141+
}
142+
catch (CertificateException ex)
143+
{
144+
// not trusted by this trust manager
145+
}
146+
}
147+
}
148+
}
149+
}
150+
}
151+
return matches;
152+
}
153+
154+
private static X509Certificate[] asX509Chain(
155+
Certificate[] certs)
156+
{
157+
X509Certificate[] chain = new X509Certificate[certs.length];
158+
for (int i = 0; i < certs.length; i++)
159+
{
160+
if (!(certs[i] instanceof X509Certificate x509))
161+
{
162+
return null;
163+
}
164+
chain[i] = x509;
165+
}
166+
return chain;
167+
}
168+
74169
private static Matcher asMatcher(
75170
String wildcard)
76171
{

0 commit comments

Comments
 (0)