Skip to content

Commit a40fb71

Browse files
jmcrawford45normanmaurerchrisvest
authored
Support boringssl SSLCredential API (#15919)
Motivation: Historically, BoringSSL lacked a built-in method to select between RSA and ECDSA certificates. The selection process, especially at TLS 1.2, is quite complex, as detailed in [this link](https://boringssl.googlesource.com/boringssl/+/5a3faaa2d50b2540c6973531841723f633f388cd/ssl/test/runner/runner.go#19669). TLS 1.3 simplifies this process significantly. Additionally, within ECDSA, there are different curves to consider, and future developments will introduce post-quantum key types. The SSL Credential API was introduced to BoringSSL to address this and a variety of other certificate negotiation decisions, such as: Different kinds of credentials ([delegate credentials](https://www.rfc-editor.org/rfc/rfc9345.html), [raw public keys](https://www.rfc-editor.org/rfc/rfc7250.html), [external PSKs](https://www.rfc-editor.org/rfc/rfc9258.html), and more [future innovations](https://davidben.github.io/merkle-tree-certs/draft-davidben-tls-merkle-tree-certs.html). [Negotiation for trust anchors](https://github.com/davidben/tls-trust-expressions/blob/main/explainer.md) to aid in PQ transitions and PKI agility. Modification: Introduce high level APIs leveraging the most useful bindings introduced in netty/netty-tcnative#935 ```java OpenSslCredential credential = OpenSslCredentialBuilder.newX509(privateKey, chain) .trustAnchorId(anchorId) .mustMatchIssuer(true) .build(); ``` Result: There are two main immediate use cases to this API First, it is now possible to delegate all the complexity of EC/RSA serving to BoringSSL. ```java OpenSslCredential ecdsaCred = buildEcdsaCredential(); SslContext ctx = SslContextBuilder.forServer(rsaKey, rsaCert) .sslProvider(SslProvider.OPENSSL_REFCNT) .credential(ecdsaCred) .build(); ``` This mechanism will also be useful to allow clients to negotiate trust anchors via https://github.com/tlswg/tls-trust-anchor-ids. For example, a modern client may request for a more efficient or more secure chain while legacy clients can still receive the less secure / less efficient fallback cert. depends on netty/netty-tcnative#949 --------- Co-authored-by: Norman Maurer <norman_maurer@apple.com> Co-authored-by: Chris Vest <christianvest_hansen@apple.com>
1 parent b880fed commit a40fb71

19 files changed

Lines changed: 972 additions & 36 deletions

common/src/main/java/io/netty/util/internal/EmptyArrays.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.ByteBuffer;
2222
import java.security.cert.Certificate;
2323
import java.security.cert.X509Certificate;
24+
import java.util.Map;
2425

2526
public final class EmptyArrays {
2627

@@ -36,6 +37,8 @@ public final class EmptyArrays {
3637
public static final Certificate[] EMPTY_CERTIFICATES = {};
3738
public static final X509Certificate[] EMPTY_X509_CERTIFICATES = {};
3839
public static final javax.security.cert.X509Certificate[] EMPTY_JAVAX_X509_CERTIFICATES = {};
40+
@SuppressWarnings("rawtypes")
41+
public static final Map.Entry[] EMPTY_MAP_ENTRY = {};
3942

4043
public static final Throwable[] EMPTY_THROWABLES = {};
4144

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2026 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.ssl;
17+
18+
import io.netty.internal.tcnative.SSLCredential;
19+
import io.netty.util.AbstractReferenceCounted;
20+
import io.netty.util.IllegalReferenceCountException;
21+
import io.netty.util.ResourceLeakDetector;
22+
import io.netty.util.ResourceLeakDetectorFactory;
23+
import io.netty.util.ResourceLeakTracker;
24+
25+
/**
26+
* Default implementation of {@link OpenSslCredential}.
27+
*
28+
* <p>This class manages the lifecycle of a native BoringSSL {@code SSL_CREDENTIAL} object.
29+
*/
30+
final class DefaultOpenSslCredential extends AbstractReferenceCounted implements OpenSslCredentialPointer {
31+
32+
private static final ResourceLeakDetector<DefaultOpenSslCredential> leakDetector =
33+
ResourceLeakDetectorFactory.instance().newResourceLeakDetector(DefaultOpenSslCredential.class);
34+
35+
private final ResourceLeakTracker<DefaultOpenSslCredential> leak;
36+
private final CredentialType type;
37+
private long credential;
38+
39+
/**
40+
* Creates a new credential instance.
41+
*
42+
* @param credential the native SSL_CREDENTIAL pointer
43+
* @param type the credential type
44+
*/
45+
DefaultOpenSslCredential(long credential, CredentialType type) {
46+
this.credential = credential;
47+
this.type = type;
48+
this.leak = leakDetector.track(this);
49+
}
50+
51+
@Override
52+
public long credentialAddress() {
53+
if (refCnt() <= 0) {
54+
throw new IllegalReferenceCountException();
55+
}
56+
return credential;
57+
}
58+
59+
@Override
60+
public CredentialType type() {
61+
return type;
62+
}
63+
64+
@Override
65+
protected void deallocate() {
66+
try {
67+
SSLCredential.free(credential);
68+
} catch (Exception e) {
69+
throw new IllegalStateException("Failed to free SSL_CREDENTIAL", e);
70+
} finally {
71+
credential = 0;
72+
if (leak != null) {
73+
boolean closed = leak.close(this);
74+
assert closed;
75+
}
76+
}
77+
}
78+
79+
@Override
80+
public DefaultOpenSslCredential retain() {
81+
if (leak != null) {
82+
leak.record();
83+
}
84+
super.retain();
85+
return this;
86+
}
87+
88+
@Override
89+
public DefaultOpenSslCredential retain(int increment) {
90+
if (leak != null) {
91+
leak.record();
92+
}
93+
super.retain(increment);
94+
return this;
95+
}
96+
97+
@Override
98+
public DefaultOpenSslCredential touch() {
99+
if (leak != null) {
100+
leak.record();
101+
}
102+
super.touch();
103+
return this;
104+
}
105+
106+
@Override
107+
public DefaultOpenSslCredential touch(Object hint) {
108+
if (leak != null) {
109+
leak.record(hint);
110+
}
111+
return this;
112+
}
113+
114+
@Override
115+
public boolean release() {
116+
if (leak != null) {
117+
leak.record();
118+
}
119+
return super.release();
120+
}
121+
122+
@Override
123+
public boolean release(int decrement) {
124+
if (leak != null) {
125+
leak.record();
126+
}
127+
return super.release(decrement);
128+
}
129+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2026 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.ssl;
17+
18+
import io.netty.util.AbstractReferenceCounted;
19+
import io.netty.util.IllegalReferenceCountException;
20+
21+
/**
22+
* A non-owning wrapper for an {@link OpenSslCredential} pointer.
23+
*
24+
* <p>This class is used when we need to expose an SSL_CREDENTIAL pointer that is managed
25+
* by OpenSSL itself (e.g., the credential selected during the handshake). Unlike
26+
* {@link DefaultOpenSslCredential}, this wrapper does not free the underlying credential
27+
* when its reference count reaches zero, as the lifetime is managed externally.
28+
*
29+
* <p>This is a BoringSSL-specific feature.
30+
*/
31+
final class NonOwnedOpenSslCredential extends AbstractReferenceCounted implements OpenSslCredentialPointer {
32+
33+
private final long credential;
34+
private final CredentialType type;
35+
private volatile boolean released;
36+
37+
/**
38+
* Creates a new non-owning credential wrapper.
39+
*
40+
* @param credential the native SSL_CREDENTIAL pointer (must not be 0)
41+
* @param type the credential type
42+
*/
43+
NonOwnedOpenSslCredential(long credential, CredentialType type) {
44+
if (credential == 0) {
45+
throw new IllegalArgumentException("credential pointer must not be 0");
46+
}
47+
this.credential = credential;
48+
this.type = type;
49+
}
50+
51+
@Override
52+
public long credentialAddress() {
53+
if (released) {
54+
throw new IllegalReferenceCountException();
55+
}
56+
return credential;
57+
}
58+
59+
@Override
60+
public CredentialType type() {
61+
return type;
62+
}
63+
64+
@Override
65+
public OpenSslCredential retain() {
66+
return (OpenSslCredential) super.retain();
67+
}
68+
69+
@Override
70+
public OpenSslCredential retain(int increment) {
71+
return (OpenSslCredential) super.retain(increment);
72+
}
73+
74+
@Override
75+
public OpenSslCredential touch() {
76+
return (OpenSslCredential) super.touch();
77+
}
78+
79+
@Override
80+
public OpenSslCredential touch(Object hint) {
81+
return this;
82+
}
83+
84+
@Override
85+
protected void deallocate() {
86+
released = true;
87+
}
88+
}

handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import javax.net.ssl.TrustManagerFactory;
3232

3333
import static io.netty.handler.ssl.ReferenceCountedOpenSslClientContext.newSessionContext;
34+
import static io.netty.util.internal.EmptyArrays.EMPTY_MAP_ENTRY;
3435

3536
/**
3637
* A client-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation.
@@ -180,7 +181,7 @@ public OpenSslClientContext(File trustCertCollectionFile, TrustManagerFactory tr
180181
this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory,
181182
toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword),
182183
keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, null, sessionCacheSize,
183-
sessionTimeout, false, KeyStore.getDefaultType(), null, null, null);
184+
sessionTimeout, false, KeyStore.getDefaultType(), null, null, null, EMPTY_MAP_ENTRY, null);
184185
}
185186

186187
OpenSslClientContext(X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory,
@@ -190,10 +191,11 @@ public OpenSslClientContext(File trustCertCollectionFile, TrustManagerFactory tr
190191
long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStore,
191192
String endpointIdentificationAlgorithm, List<SNIServerName> serverNames,
192193
ResumptionController resumptionController,
193-
Map.Entry<SslContextOption<?>, Object>... options)
194+
Map.Entry<SslContextOption<?>, Object>[] options,
195+
List<OpenSslCredential> credentials)
194196
throws SSLException {
195197
super(ciphers, cipherFilter, apn, SSL.SSL_MODE_CLIENT, keyCertChain, ClientAuth.NONE, protocols, false,
196-
endpointIdentificationAlgorithm, enableOcsp, serverNames, resumptionController, options);
198+
endpointIdentificationAlgorithm, enableOcsp, serverNames, resumptionController, options, credentials);
197199
boolean success = false;
198200
boolean supportJdkSignatureFallback = isJdkSignatureFallbackEnabled(options);
199201
try {

handler/src/main/java/io/netty/handler/ssl/OpenSslContext.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,24 @@ public abstract class OpenSslContext extends ReferenceCountedOpenSslContext {
3434
int mode, Certificate[] keyCertChain,
3535
ClientAuth clientAuth, String[] protocols, boolean startTls, String endpointIdentificationAlgorithm,
3636
boolean enableOcsp, List<SNIServerName> serverNames, ResumptionController resumptionController,
37-
Map.Entry<SslContextOption<?>, Object>... options)
37+
Map.Entry<SslContextOption<?>, Object>[] options,
38+
List<OpenSslCredential> credentials)
3839
throws SSLException {
3940
super(ciphers, cipherFilter, toNegotiator(apnCfg), mode, keyCertChain,
4041
clientAuth, protocols, startTls, endpointIdentificationAlgorithm, enableOcsp, false,
41-
serverNames, resumptionController, options);
42+
serverNames, resumptionController, options, credentials);
4243
}
4344

4445
OpenSslContext(Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn,
4546
int mode, Certificate[] keyCertChain,
4647
ClientAuth clientAuth, String[] protocols, boolean startTls, boolean enableOcsp,
4748
List<SNIServerName> serverNames, ResumptionController resumptionController,
48-
Map.Entry<SslContextOption<?>, Object>... options)
49+
Map.Entry<SslContextOption<?>, Object>[] options,
50+
List<OpenSslCredential> credentials)
4951
throws SSLException {
5052
super(ciphers, cipherFilter, apn, mode, keyCertChain,
51-
clientAuth, protocols, startTls, null, enableOcsp, false, serverNames, resumptionController, options);
53+
clientAuth, protocols, startTls, null, enableOcsp, false, serverNames, resumptionController, options,
54+
credentials);
5255
}
5356

5457
@Override
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2026 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://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, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.ssl;
17+
18+
import io.netty.util.ReferenceCounted;
19+
20+
/**
21+
* Represents an OpenSSL/BoringSSL {@code SSL_CREDENTIAL} object.
22+
*
23+
* <p>SSL credentials provide a more flexible alternative to traditional certificate/key configuration,
24+
* supporting features like:
25+
* <ul>
26+
* <li>Multiple credentials per context (e.g., RSA + ECDSA)</li>
27+
* <li>Delegated credentials</li>
28+
* <li>OCSP stapling per credential</li>
29+
* <li>Signed Certificate Timestamps (SCT)</li>
30+
* <li>Trust anchor identifiers</li>
31+
* <li>Per-credential signing algorithm preferences</li>
32+
* </ul>
33+
*
34+
* <p>This is a BoringSSL-specific feature. Use {@link #isAvailable()} to check availability.
35+
*
36+
* <p>Instances are reference counted and must be released when no longer needed.
37+
*
38+
* @see <a href="https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CREDENTIAL_free">
39+
* BoringSSL SSL_CREDENTIAL Documentation</a>
40+
*/
41+
public interface OpenSslCredential extends ReferenceCounted {
42+
/**
43+
* Check if the credentials API is supported.
44+
* @return {@code true} if the credentials API is supported, otherwise {@code false}.
45+
*/
46+
static boolean isAvailable() {
47+
return OpenSsl.isAvailable() && OpenSsl.isBoringSSL();
48+
}
49+
50+
/**
51+
* Returns the type of this credential.
52+
*
53+
* @return the credential type
54+
*/
55+
CredentialType type();
56+
57+
@Override
58+
OpenSslCredential retain();
59+
60+
@Override
61+
OpenSslCredential retain(int increment);
62+
63+
@Override
64+
OpenSslCredential touch();
65+
66+
@Override
67+
OpenSslCredential touch(Object hint);
68+
69+
/**
70+
* The type of SSL credential.
71+
*/
72+
enum CredentialType {
73+
/**
74+
* Standard X.509 certificate credential created with {@code SSL_CREDENTIAL_new_x509()}.
75+
*/
76+
X509,
77+
78+
/**
79+
* Delegated credential created with {@code SSL_CREDENTIAL_new_delegated()}.
80+
*
81+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9345">RFC 9345 - Delegated Credentials for TLS</a>
82+
*/
83+
DELEGATED
84+
}
85+
}

0 commit comments

Comments
 (0)