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 *  @version 2020-02-22
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         * Parses a DER-encoded X.509 certificate.
051         *
052         * @param derEncodedCert The DER-encoded X.509 certificate, as a byte
053         *                       array. May be {@code null}.
054         *
055         * @return The X.509 certificate, {@code null} if not specified or
056         *         parsing failed.
057         */
058        public static X509Certificate parse(final byte[] derEncodedCert) {
059
060                try {
061                        return parseWithException(derEncodedCert);
062                } catch (CertificateException e) {
063                        return null;
064                }
065        }
066
067
068        /**
069         * Parses a DER-encoded X.509 certificate with exception handling.
070         *
071         * @param derEncodedCert The DER-encoded X.509 certificate, as a byte
072         *                       array. Empty or {@code null} if not specified.
073         *
074         * @return The X.509 certificate, {@code null} if not specified.
075         *
076         * @throws CertificateException If parsing failed.
077         */
078        public static X509Certificate parseWithException(final byte[] derEncodedCert)
079                throws CertificateException {
080
081                if (derEncodedCert == null || derEncodedCert.length == 0) {
082                        return null;
083                }
084
085                CertificateFactory cf = CertificateFactory.getInstance("X.509");
086                final Certificate cert = cf.generateCertificate(new ByteArrayInputStream(derEncodedCert));
087
088                if (! (cert instanceof X509Certificate)) {
089                        throw new CertificateException("Not a X.509 certificate: " + cert.getType());
090                }
091
092                return (X509Certificate)cert;
093        }
094
095
096        /**
097         * Parses a PEM-encoded X.509 certificate.
098         *
099         * @param pemEncodedCert The PEM-encoded X.509 certificate, as a
100         *                       string. Empty or {@code null} if not
101         *                       specified.
102         *
103         * @return The X.509 certificate, {@code null} if parsing failed.
104         */
105        public static X509Certificate parse(final String pemEncodedCert) {
106
107                if (pemEncodedCert == null || pemEncodedCert.isEmpty()) {
108                        return null;
109                }
110
111                final int markerStart = pemEncodedCert.indexOf(PEM_BEGIN_MARKER);
112
113                if (markerStart < 0) {
114                        return null;
115                }
116
117                String buf = pemEncodedCert.substring(markerStart + PEM_BEGIN_MARKER.length());
118
119                final int markerEnd = buf.indexOf(PEM_END_MARKER);
120
121                if (markerEnd < 0) {
122                        return null;
123                }
124
125                buf = buf.substring(0, markerEnd);
126
127                buf = buf.replaceAll("\\s", "");
128
129                return parse(new Base64(buf).decode());
130        }
131
132
133        /**
134         * Parses a PEM-encoded X.509 certificate with exception handling.
135         *
136         * @param pemEncodedCert The PEM-encoded X.509 certificate, as a
137         *                       string. Empty or {@code null} if not
138         *                       specified.
139         *
140         * @return The X.509 certificate, {@code null} if parsing failed.
141         */
142        public static X509Certificate parseWithException(final String pemEncodedCert)
143                throws CertificateException {
144
145                if (pemEncodedCert == null || pemEncodedCert.isEmpty()) {
146                        return null;
147                }
148
149                final int markerStart = pemEncodedCert.indexOf(PEM_BEGIN_MARKER);
150
151                if (markerStart < 0) {
152                        throw new CertificateException("PEM begin marker not found");
153                }
154
155                String buf = pemEncodedCert.substring(markerStart + PEM_BEGIN_MARKER.length());
156
157                final int markerEnd = buf.indexOf(PEM_END_MARKER);
158
159                if (markerEnd < 0) {
160                        throw new CertificateException("PEM end marker not found");
161                }
162
163                buf = buf.substring(0, markerEnd);
164
165                buf = buf.replaceAll("\\s", "");
166
167                return parseWithException(new Base64(buf).decode());
168        }
169        
170        
171        /**
172         * Returns the specified X.509 certificate as PEM-encoded string.
173         *
174         * @param cert The X.509 certificate. Must not be {@code null}.
175         *
176         * @return The PEM-encoded X.509 certificate, {@code null} if encoding
177         *         failed.
178         */
179        public static String toPEMString(final X509Certificate cert) {
180        
181                return toPEMString(cert, true);
182        }
183        
184        
185        /**
186         * Returns the specified X.509 certificate as PEM-encoded string.
187         *
188         * @param cert           The X.509 certificate. Must not be
189         *                       {@code null}.
190         * @param withLineBreaks {@code false} to suppress line breaks.
191         *
192         * @return The PEM-encoded X.509 certificate, {@code null} if encoding
193         *         failed.
194         */
195        public static String toPEMString(final X509Certificate cert, final boolean withLineBreaks) {
196        
197                StringBuilder sb = new StringBuilder();
198                sb.append(PEM_BEGIN_MARKER);
199                
200                if (withLineBreaks)
201                        sb.append('\n');
202                
203                try {
204                        sb.append(Base64.encode(cert.getEncoded()).toString());
205                } catch (CertificateEncodingException e) {
206                        return null;
207                }
208                
209                if (withLineBreaks)
210                        sb.append('\n');
211                
212                sb.append(PEM_END_MARKER);
213                return sb.toString();
214        }
215        
216        
217        /**
218         * Computes the X.509 certificate SHA-256 thumbprint ({@code x5t#S256}).
219         *
220         * @param cert The X.509 certificate. Must not be {@code null}.
221         *
222         * @return The SHA-256 thumbprint, BASE64URL-encoded, {@code null} if
223         *         a certificate encoding exception is encountered.
224         */
225        public static Base64URL computeSHA256Thumbprint(final X509Certificate cert) {
226        
227                try {
228                        byte[] derEncodedCert = cert.getEncoded();
229                        MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
230                        return Base64URL.encode(sha256.digest(derEncodedCert));
231                } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
232                        return null;
233                }
234        }
235        
236        
237        /**
238         * Stores a private key with its associated X.509 certificate in a
239         * Java key store. The name (alias) for the stored entry is a given a
240         * random UUID.
241         *
242         * @param keyStore    The key store. Must be initialised and not
243         *                    {@code null}.
244         * @param privateKey  The private key. Must not be {@code null}.
245         * @param keyPassword The password to protect the private key, empty
246         *                    array for none. Must not be {@code null}.
247         * @param cert        The X.509 certificate, its public key and the
248         *                    private key should form a pair. Must not be
249         *                    {@code null}.
250         *
251         * @return The UUID for the stored entry.
252         */
253        public static UUID store(final KeyStore keyStore,
254                                 final PrivateKey privateKey,
255                                 final char[] keyPassword,
256                                 final X509Certificate cert)
257                throws KeyStoreException {
258                
259                UUID alias = UUID.randomUUID();
260                keyStore.setKeyEntry(alias.toString(), privateKey, keyPassword, new Certificate[]{cert});
261                return alias;
262        }
263}