View Javadoc
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.buffer.UnpooledByteBufAllocator;
19  import io.netty.internal.tcnative.SSL;
20  import io.netty.internal.tcnative.SSLCredential;
21  import io.netty.util.internal.ObjectUtil;
22  
23  import java.security.PrivateKey;
24  import java.security.cert.X509Certificate;
25  
26  import static io.netty.handler.ssl.OpenSslCredential.CredentialType;
27  import static io.netty.util.internal.ObjectUtil.checkNotNull;
28  
29  /**
30   * Builder for creating {@link OpenSslCredential} instances.
31   *
32   * <p>This builder provides a fluent API for configuring SSL credentials with support for:
33   * <ul>
34   *   <li>X.509 credentials</li>
35   *   <li>Certificate chains and private keys</li>
36   *   <li>Trust anchor identifiers (optional)</li>
37   * </ul>
38   *
39   * <p>Example usage:
40   * <pre>
41   * // Create credential with trust anchor (optional)
42   * ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier("1.3.6.1.4.1.11129.9.10"); // Google's taiWE1
43   * byte[] trustAnchorBytes = oid.getEncoded();
44   *
45   * OpenSslCredential credential = OpenSslCredentialBuilder.forX509(privateKey, cert1, cert2, cert3)
46   *     .trustAnchorId(trustAnchorBytes)  // optional
47   *     .build();
48   * </pre>
49   *
50   * <p>This is a BoringSSL-specific feature.
51   */
52  public final class OpenSslCredentialBuilder {
53  
54      private PrivateKey privateKey;
55      private OpenSslPrivateKey openSslPrivateKey;
56      private X509Certificate[] certificateChain;
57      private byte[] trustAnchorId;
58      private boolean mustMatchIssuer;
59  
60      private OpenSslCredentialBuilder(PrivateKey privateKey, X509Certificate[] certificateChain) {
61          this.privateKey = checkNotNull(privateKey, "privateKey");
62          this.certificateChain = checkNotNull(certificateChain, "certificateChain").clone();
63          ObjectUtil.checkNonEmpty(this.certificateChain, "certificateChain");
64      }
65  
66      private OpenSslCredentialBuilder(OpenSslPrivateKey openSslPrivateKey, X509Certificate[] certificateChain) {
67          this.openSslPrivateKey = checkNotNull(openSslPrivateKey, "privateKey");
68          this.certificateChain = checkNotNull(certificateChain, "certificateChain").clone();
69          ObjectUtil.checkNonEmpty(this.certificateChain, "certificateChain");
70      }
71  
72      /**
73       * Creates a new builder for an X.509 credential with a Java PrivateKey.
74       *
75       * @param privateKey the private key (required)
76       * @param certificateChain the certificate chain, starting with the leaf certificate (required)
77       * @return a new builder instance
78       */
79      public static OpenSslCredentialBuilder forX509(PrivateKey privateKey, X509Certificate... certificateChain) {
80          return new OpenSslCredentialBuilder(privateKey, certificateChain);
81      }
82  
83      /**
84       * Sets the trust anchor identifier for this credential.
85       *
86       * <p>The trust anchor identifier should be ASN.1 DER encoded bytes.
87       * To convert from an OID string, use BouncyCastle's ASN1Encodable:
88       * <pre>
89       * // Example: Google's taiWE1 OID from https://pki.goog/oids/index.html
90       * ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier("1.3.6.1.4.1.11129.9.10");
91       * byte[] encoded = oid.getEncoded();
92       * credential.trustAnchorId(encoded);
93       * </pre>
94       *
95       * @param trustAnchorId the trust anchor identifier as ASN.1 DER encoded bytes
96       * @return this builder for chaining
97       */
98      public OpenSslCredentialBuilder trustAnchorId(byte[] trustAnchorId) {
99          this.trustAnchorId = checkNotNull(trustAnchorId, "trustAnchorId").clone();
100         return this;
101     }
102 
103     /**
104      * Sets whether the issuer must match for this credential.
105      *
106      * @param mustMatchIssuer {@code true} if issuer must match
107      * @return this builder for chaining
108      */
109     public OpenSslCredentialBuilder mustMatchIssuer(boolean mustMatchIssuer) {
110         this.mustMatchIssuer = mustMatchIssuer;
111         return this;
112     }
113 
114     /**
115      * Builds the {@link OpenSslCredential} instance.
116      *
117      * @return a new credential instance
118      * @throws IllegalStateException if an error occurs during credential creation
119      */
120     public OpenSslCredential build() {
121         OpenSsl.ensureAvailability();
122 
123         if (!OpenSslCredential.isAvailable()) {
124             throw new UnsupportedOperationException("SSL_CREDENTIAL API is not supported");
125         }
126 
127         long credentialPtr = 0;
128         long certChainPtr = 0;
129         long privateKeyPtr = 0;
130 
131         try {
132             // Create the credential
133             credentialPtr = createCredential();
134 
135             // Set private key (guaranteed to be present via constructor)
136             privateKeyPtr = getPrivateKeyPointer();
137             SSLCredential.setPrivateKey(credentialPtr, privateKeyPtr);
138 
139             // Set certificate chain (guaranteed to be present via constructor)
140             certChainPtr = createCertChainPointer();
141             SSLCredential.setCertChain(credentialPtr, certChainPtr);
142 
143             // Set optional properties
144             if (trustAnchorId != null) {
145                 SSLCredential.setTrustAnchorId(credentialPtr, trustAnchorId);
146             }
147 
148             if (mustMatchIssuer) {
149                 SSLCredential.setMustMatchIssuer(credentialPtr, true);
150             }
151 
152             // Success - create the wrapper object
153             long finalPtr = credentialPtr;
154             credentialPtr = 0; // Don't free on cleanup
155             return new DefaultOpenSslCredential(finalPtr, CredentialType.X509);
156         } catch (Exception e) {
157             throw new IllegalStateException("Failed to build SSL credential", e);
158         } finally {
159             // Cleanup on error
160             if (credentialPtr != 0) {
161                 try {
162                     SSLCredential.free(credentialPtr);
163                 } catch (Exception e) {
164                     // Ignore cleanup errors
165                 }
166             }
167             if (certChainPtr != 0) {
168                 SSL.freeX509Chain(certChainPtr);
169             }
170             if (privateKeyPtr != 0 && privateKey != null) {
171                 // Only free if we created it from a Java PrivateKey
172                 SSL.freePrivateKey(privateKeyPtr);
173             }
174         }
175     }
176 
177     private long createCredential() throws Exception {
178         return SSLCredential.newX509();
179     }
180 
181     private long getPrivateKeyPointer() throws Exception {
182         if (openSslPrivateKey != null) {
183             return openSslPrivateKey.privateKeyAddress();
184         }
185 
186         if (privateKey == null) {
187             throw new IllegalStateException("No private key specified");
188         }
189 
190         // Convert Java PrivateKey to OpenSSL EVP_PKEY
191         long bio = ReferenceCountedOpenSslContext.toBIO(
192                 UnpooledByteBufAllocator.DEFAULT, privateKey);
193         try {
194             return SSL.parsePrivateKey(bio, null);
195         } finally {
196             SSL.freeBIO(bio);
197         }
198     }
199 
200     private long createCertChainPointer() throws Exception {
201         // Convert certificate chain to PEM format and parse
202         try {
203             long bio = ReferenceCountedOpenSslContext.toBIO(
204                     UnpooledByteBufAllocator.DEFAULT, certificateChain);
205             try {
206                 return SSL.parseX509Chain(bio);
207             } finally {
208                 SSL.freeBIO(bio);
209             }
210         } catch (Exception e) {
211             throw new IllegalStateException("Failed to encode certificate chain", e);
212         }
213     }
214 }