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}