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