001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2026, Connect2id Ltd.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017package com.nimbusds.jose.util;
018
019
020import java.io.ByteArrayInputStream;
021import java.security.*;
022import java.security.cert.Certificate;
023import java.security.cert.*;
024import java.util.UUID;
025
026
027/**
028 *  X.509 certificate utilities.
029 *
030 *  @author Vladimir Dzhuvinov
031 *  @author Simon Kissane
032 *  @version 2026-04-02
033 */
034public class X509CertUtils {
035
036
037        /**
038         * The PEM start marker.
039         */
040        public static final String PEM_BEGIN_MARKER = "-----BEGIN CERTIFICATE-----";
041
042
043        /**
044         * The PEM end marker.
045         */
046        public static final String PEM_END_MARKER = "-----END CERTIFICATE-----";
047
048
049        /**
050         * The JCA provider to use for certificate operations, {@code null}
051         * implies the default provider.
052         */
053        private static Provider jcaProvider;
054
055
056        /**
057         * Returns the JCA provider to use for certification operations.
058         *
059         * @return The JCA provider to use for certificate operations,
060         *         {@code null} implies the default provider.
061         */
062        public static Provider getProvider() {
063                return jcaProvider;
064        }
065
066
067        /**
068         * Sets the JCA provider to use for certification operations.
069         *
070         * @param provider The JCA provider to use for certificate operations,
071         *                 {@code null} implies the default.
072         */
073        public static void setProvider(final Provider provider) {
074                jcaProvider = provider;
075        }
076
077
078        /**
079         * Parses a DER-encoded X.509 certificate.
080         *
081         * @param derEncodedCert The DER-encoded X.509 certificate, as a byte
082         *                       array. May be {@code null}.
083         *
084         * @return The X.509 certificate, {@code null} if not specified or
085         *         parsing failed.
086         */
087        public static X509Certificate parse(final byte[] derEncodedCert) {
088
089                try {
090                        return parseWithException(derEncodedCert);
091                } catch (CertificateException e) {
092                        return null;
093                }
094        }
095
096
097        /**
098         * Parses a DER-encoded X.509 certificate with exception handling.
099         *
100         * @param derEncodedCert The DER-encoded X.509 certificate, as a byte
101         *                       array. Empty or {@code null} if not specified.
102         *
103         * @return The X.509 certificate, {@code null} if not specified.
104         *
105         * @throws CertificateException If parsing failed.
106         */
107        public static X509Certificate parseWithException(final byte[] derEncodedCert)
108                throws CertificateException {
109
110                if (derEncodedCert == null || derEncodedCert.length == 0) {
111                        return null;
112                }
113
114                CertificateFactory cf = jcaProvider != null ?
115                        CertificateFactory.getInstance("X.509", jcaProvider) :
116                        CertificateFactory.getInstance("X.509");
117                final Certificate cert = cf.generateCertificate(new ByteArrayInputStream(derEncodedCert));
118
119                if (! (cert instanceof X509Certificate)) {
120                        throw new CertificateException("Not a X.509 certificate: " + cert.getType());
121                }
122
123                return (X509Certificate)cert;
124        }
125
126
127        /**
128         * Parses a PEM-encoded X.509 certificate.
129         *
130         * @param pemEncodedCert The PEM-encoded X.509 certificate, as a
131         *                       string. Empty or {@code null} if not
132         *                       specified.
133         *
134         * @return The X.509 certificate, {@code null} if parsing failed.
135         */
136        public static X509Certificate parse(final String pemEncodedCert) {
137
138                if (pemEncodedCert == null || pemEncodedCert.isEmpty()) {
139                        return null;
140                }
141
142                final int markerStart = pemEncodedCert.indexOf(PEM_BEGIN_MARKER);
143
144                if (markerStart < 0) {
145                        return null;
146                }
147
148                String buf = pemEncodedCert.substring(markerStart + PEM_BEGIN_MARKER.length());
149
150                final int markerEnd = buf.indexOf(PEM_END_MARKER);
151
152                if (markerEnd < 0) {
153                        return null;
154                }
155
156                buf = buf.substring(0, markerEnd);
157
158                buf = buf.replaceAll("\\s", "");
159
160                return parse(new Base64(buf).decode());
161        }
162
163
164        /**
165         * Parses a PEM-encoded X.509 certificate with exception handling.
166         *
167         * @param pemEncodedCert The PEM-encoded X.509 certificate, as a
168         *                       string. Empty or {@code null} if not
169         *                       specified.
170         *
171         * @return The X.509 certificate, {@code null} if parsing failed.
172         */
173        public static X509Certificate parseWithException(final String pemEncodedCert)
174                throws CertificateException {
175
176                if (pemEncodedCert == null || pemEncodedCert.isEmpty()) {
177                        return null;
178                }
179
180                final int markerStart = pemEncodedCert.indexOf(PEM_BEGIN_MARKER);
181
182                if (markerStart < 0) {
183                        throw new CertificateException("PEM begin marker not found");
184                }
185
186                String buf = pemEncodedCert.substring(markerStart + PEM_BEGIN_MARKER.length());
187
188                final int markerEnd = buf.indexOf(PEM_END_MARKER);
189
190                if (markerEnd < 0) {
191                        throw new CertificateException("PEM end marker not found");
192                }
193
194                buf = buf.substring(0, markerEnd);
195
196                buf = buf.replaceAll("\\s", "");
197
198                return parseWithException(new Base64(buf).decode());
199        }
200        
201        
202        /**
203         * Returns the specified X.509 certificate as PEM-encoded string.
204         *
205         * @param cert The X.509 certificate. Must not be {@code null}.
206         *
207         * @return The PEM-encoded X.509 certificate, {@code null} if encoding
208         *         failed.
209         */
210        public static String toPEMString(final X509Certificate cert) {
211        
212                return toPEMString(cert, true);
213        }
214        
215        
216        /**
217         * Returns the specified X.509 certificate as PEM-encoded string.
218         *
219         * @param cert           The X.509 certificate. Must not be
220         *                       {@code null}.
221         * @param withLineBreaks {@code false} to suppress line breaks.
222         *
223         * @return The PEM-encoded X.509 certificate, {@code null} if encoding
224         *         failed.
225         */
226        public static String toPEMString(final X509Certificate cert, final boolean withLineBreaks) {
227        
228                StringBuilder sb = new StringBuilder();
229                sb.append(PEM_BEGIN_MARKER);
230                
231                if (withLineBreaks)
232                        sb.append('\n');
233                
234                try {
235                        sb.append(Base64.encode(cert.getEncoded()));
236                } catch (CertificateEncodingException e) {
237                        return null;
238                }
239                
240                if (withLineBreaks)
241                        sb.append('\n');
242                
243                sb.append(PEM_END_MARKER);
244                return sb.toString();
245        }
246        
247        
248        /**
249         * Computes the X.509 certificate SHA-256 thumbprint ({@code x5t#S256}).
250         *
251         * @param cert The X.509 certificate. Must not be {@code null}.
252         *
253         * @return The SHA-256 thumbprint, BASE64URL-encoded, {@code null} if
254         *         a certificate encoding exception is encountered.
255         */
256        public static Base64URL computeSHA256Thumbprint(final X509Certificate cert) {
257        
258                try {
259                        byte[] derEncodedCert = cert.getEncoded();
260                        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
261                        return Base64URL.encode(sha256.digest(derEncodedCert));
262                } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
263                        return null;
264                }
265        }
266
267
268        /**
269         * Computes the X.509 certificate SHA-1 thumbprint ({@code x5t}).
270         *
271         * @param cert The X.509 certificate. Must not be {@code null}.
272         *
273         * @return The SHA-1 thumbprint, BASE64URL-encoded, {@code null} if a
274         *         certificate encoding exception is encountered.
275         */
276        public static Base64URL computeSHA1Thumbprint(final X509Certificate cert) {
277
278                try {
279                        byte[] derEncodedCert = cert.getEncoded();
280                        MessageDigest sha256 = MessageDigest.getInstance("SHA-1");
281                        return Base64URL.encode(sha256.digest(derEncodedCert));
282                } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
283                        return null;
284                }
285        }
286        
287        
288        /**
289         * Stores a private key with its associated X.509 certificate in a
290         * Java key store. The name (alias) for the stored entry is a given a
291         * random UUID.
292         *
293         * @param keyStore    The key store. Must be initialised and not
294         *                    {@code null}.
295         * @param privateKey  The private key. Must not be {@code null}.
296         * @param keyPassword The password to protect the private key, empty
297         *                    array for none. Must not be {@code null}.
298         * @param cert        The X.509 certificate, its public key and the
299         *                    private key should form a pair. Must not be
300         *                    {@code null}.
301         *
302         * @return The UUID for the stored entry.
303         */
304        public static UUID store(final KeyStore keyStore,
305                                 final PrivateKey privateKey,
306                                 final char[] keyPassword,
307                                 final X509Certificate cert)
308                throws KeyStoreException {
309                
310                UUID alias = UUID.randomUUID();
311                keyStore.setKeyEntry(alias.toString(), privateKey, keyPassword, new Certificate[]{cert});
312                return alias;
313        }
314}