|
| 1 | +// Copyright (c) 2019 Pivotal Software, Inc. All rights reserved. |
| 2 | +// |
| 3 | +// This software, the RabbitMQ Java client library, is triple-licensed under the |
| 4 | +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 |
| 5 | +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see |
| 6 | +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, |
| 7 | +// please see LICENSE-APACHE2. |
| 8 | +// |
| 9 | +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, |
| 10 | +// either express or implied. See the LICENSE file for specific language governing |
| 11 | +// rights and limitations of this software. |
| 12 | +// |
| 13 | +// If you have any questions regarding licensing, please contact us at |
| 14 | + |
| 15 | + |
| 16 | +package com.rabbitmq.client.impl; |
| 17 | + |
| 18 | +import org.slf4j.Logger; |
| 19 | +import org.slf4j.LoggerFactory; |
| 20 | + |
| 21 | +import javax.net.ssl.SSLSession; |
| 22 | +import java.security.cert.Certificate; |
| 23 | +import java.security.cert.CertificateParsingException; |
| 24 | +import java.security.cert.X509Certificate; |
| 25 | +import java.util.*; |
| 26 | +import java.util.function.BiFunction; |
| 27 | +import java.util.stream.Collectors; |
| 28 | + |
| 29 | +/** |
| 30 | + * Utility to extract information from X509 certificates. |
| 31 | + * |
| 32 | + * @since 5.7.0 |
| 33 | + */ |
| 34 | +public class TlsUtils { |
| 35 | + |
| 36 | + private static final Logger LOGGER = LoggerFactory.getLogger(TlsUtils.class); |
| 37 | + private static final List<String> KEY_USAGE = Collections.unmodifiableList(Arrays.asList( |
| 38 | + "digitalSignature", "nonRepudiation", "keyEncipherment", |
| 39 | + "dataEncipherment", "keyAgreement", "keyCertSign", |
| 40 | + "cRLSign", "encipherOnly", "decipherOnly" |
| 41 | + )); |
| 42 | + private static final Map<String, String> EXTENDED_KEY_USAGE = Collections.unmodifiableMap(new HashMap<String, String>() {{ |
| 43 | + put("1.3.6.1.5.5.7.3.1", "TLS Web server authentication"); |
| 44 | + put("1.3.6.1.5.5.7.3.2", "TLS Web client authentication"); |
| 45 | + put("1.3.6.1.5.5.7.3.3", "Signing of downloadable executable code"); |
| 46 | + put("1.3.6.1.5.5.7.3.4", "E-mail protection"); |
| 47 | + put("1.3.6.1.5.5.7.3.8", "Binding the hash of an object to a time from an agreed-upon time"); |
| 48 | + }}); |
| 49 | + private static String PARSING_ERROR = "<parsing-error>"; |
| 50 | + private static final Map<String, BiFunction<byte[], X509Certificate, String>> EXTENSIONS = Collections.unmodifiableMap( |
| 51 | + new HashMap<String, BiFunction<byte[], X509Certificate, String>>() {{ |
| 52 | + put("2.5.29.14", (v, c) -> "SubjectKeyIdentifier = " + octetStringHexDump(v)); |
| 53 | + put("2.5.29.15", (v, c) -> "KeyUsage = " + keyUsageBitString(c.getKeyUsage(), v)); |
| 54 | + put("2.5.29.16", (v, c) -> "PrivateKeyUsage = " + hexDump(0, v)); |
| 55 | + put("2.5.29.17", (v, c) -> { |
| 56 | + try { |
| 57 | + return "SubjectAlternativeName = " + sans(c, "/"); |
| 58 | + } catch (CertificateParsingException e) { |
| 59 | + return "SubjectAlternativeName = " + PARSING_ERROR; |
| 60 | + } |
| 61 | + }); |
| 62 | + put("2.5.29.18", (v, c) -> "IssuerAlternativeName = " + hexDump(0, v)); |
| 63 | + put("2.5.29.19", (v, c) -> "BasicConstraints = " + basicConstraints(v)); |
| 64 | + put("2.5.29.30", (v, c) -> "NameConstraints = " + hexDump(0, v)); |
| 65 | + put("2.5.29.33", (v, c) -> "PolicyMappings = " + hexDump(0, v)); |
| 66 | + put("2.5.29.35", (v, c) -> "AuthorityKeyIdentifier = " + authorityKeyIdentifier(v)); |
| 67 | + put("2.5.29.36", (v, c) -> "PolicyConstraints = " + hexDump(0, v)); |
| 68 | + put("2.5.29.37", (v, c) -> "ExtendedKeyUsage = " + extendedKeyUsage(v, c)); |
| 69 | + }}); |
| 70 | + |
| 71 | + /** |
| 72 | + * Log details on peer certificate and certification chain. |
| 73 | + * <p> |
| 74 | + * The log level is debug. Common X509 extensions are displayed in a best-effort |
| 75 | + * fashion, a hexadecimal dump is made for less commonly used extensions. |
| 76 | + * |
| 77 | + * @param session the {@link SSLSession} to extract the certificates from |
| 78 | + */ |
| 79 | + public static void logPeerCertificateInfo(SSLSession session) { |
| 80 | + if (LOGGER.isDebugEnabled()) { |
| 81 | + try { |
| 82 | + Certificate[] peerCertificates = session.getPeerCertificates(); |
| 83 | + if (peerCertificates != null && peerCertificates.length > 0) { |
| 84 | + LOGGER.debug(peerCertificateInfo(peerCertificates[0], "Peer's leaf certificate")); |
| 85 | + for (int i = 1; i < peerCertificates.length; i++) { |
| 86 | + LOGGER.debug(peerCertificateInfo(peerCertificates[i], "Peer's certificate chain entry")); |
| 87 | + } |
| 88 | + } |
| 89 | + } catch (Exception e) { |
| 90 | + LOGGER.debug("Error while logging peer certificate info: {}", e.getMessage()); |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + /** |
| 96 | + * Get a string representation of certificate info. |
| 97 | + * |
| 98 | + * @param certificate the certificate to analyze |
| 99 | + * @param prefix the line prefix |
| 100 | + * @return information about the certificate |
| 101 | + */ |
| 102 | + public static String peerCertificateInfo(Certificate certificate, String prefix) { |
| 103 | + X509Certificate c = (X509Certificate) certificate; |
| 104 | + try { |
| 105 | + return String.format("%s subject: %s, subject alternative names: %s, " + |
| 106 | + "issuer: %s, not valid after: %s, X.509 usage extensions: %s", |
| 107 | + prefix, c.getSubjectDN().getName(), sans(c, ","), c.getIssuerDN().getName(), |
| 108 | + c.getNotAfter(), extensions(c)); |
| 109 | + } catch (Exception e) { |
| 110 | + return "Error while retrieving " + prefix + " certificate information"; |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + private static String sans(X509Certificate c, String separator) throws CertificateParsingException { |
| 115 | + return String.join(separator, Optional.ofNullable(c.getSubjectAlternativeNames()) |
| 116 | + .orElse(new ArrayList<>()) |
| 117 | + .stream() |
| 118 | + .map(v -> v.toString()) |
| 119 | + .collect(Collectors.toList())); |
| 120 | + } |
| 121 | + |
| 122 | + /** |
| 123 | + * Human-readable representation of an X509 certificate extension. |
| 124 | + * <p> |
| 125 | + * Common extensions are supported in a best-effort fashion, less commonly |
| 126 | + * used extensions are displayed as an hexadecimal dump. |
| 127 | + * <p> |
| 128 | + * Extensions come encoded as a DER Octet String, which itself can contain |
| 129 | + * other DER-encoded objects, making a comprehensive support in this utility |
| 130 | + * impossible. |
| 131 | + * |
| 132 | + * @param oid extension OID |
| 133 | + * @param derOctetString the extension value as a DER octet string |
| 134 | + * @param certificate the certificate |
| 135 | + * @return the OID and the value |
| 136 | + * @see <a href="http://luca.ntop.org/Teaching/Appunti/asn1.html">A Layman's Guide to a Subset of ASN.1, BER, and DER</a> |
| 137 | + * @see <a href="https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-der-encoding-of-asn-1-types">DER Encoding of ASN.1 Types</a> |
| 138 | + */ |
| 139 | + public static String extensionPrettyPrint(String oid, byte[] derOctetString, X509Certificate certificate) { |
| 140 | + try { |
| 141 | + return EXTENSIONS.getOrDefault(oid, (v, c) -> oid + " = " + hexDump(0, derOctetString)) |
| 142 | + .apply(derOctetString, certificate); |
| 143 | + } catch (Exception e) { |
| 144 | + return oid + " = " + PARSING_ERROR; |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + private static String extensions(X509Certificate certificate) { |
| 149 | + List<String> extensions = new ArrayList<>(); |
| 150 | + for (String oid : certificate.getCriticalExtensionOIDs()) { |
| 151 | + extensions.add(extensionPrettyPrint(oid, certificate.getExtensionValue(oid), certificate) + " (critical)"); |
| 152 | + } |
| 153 | + for (String oid : certificate.getNonCriticalExtensionOIDs()) { |
| 154 | + extensions.add(extensionPrettyPrint(oid, certificate.getExtensionValue(oid), certificate) + " (non-critical)"); |
| 155 | + } |
| 156 | + return String.join(", ", extensions); |
| 157 | + } |
| 158 | + |
| 159 | + private static String octetStringHexDump(byte[] derOctetString) { |
| 160 | + // this is an octet string in a octet string, [4 total_length 4 length ...] |
| 161 | + if (derOctetString.length > 4 && derOctetString[0] == 4 && derOctetString[2] == 4) { |
| 162 | + return hexDump(4, derOctetString); |
| 163 | + } else { |
| 164 | + return hexDump(0, derOctetString); |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + private static String hexDump(int start, byte[] derOctetString) { |
| 169 | + List<String> hexs = new ArrayList<>(); |
| 170 | + for (int i = start; i < derOctetString.length; i++) { |
| 171 | + hexs.add(String.format("%02X", derOctetString[i])); |
| 172 | + } |
| 173 | + return String.join(":", hexs); |
| 174 | + } |
| 175 | + |
| 176 | + private static String keyUsageBitString(boolean[] keyUsage, byte[] derOctetString) { |
| 177 | + if (keyUsage != null) { |
| 178 | + List<String> usage = new ArrayList<>(); |
| 179 | + for (int i = 0; i < keyUsage.length; i++) { |
| 180 | + if (keyUsage[i]) { |
| 181 | + usage.add(KEY_USAGE.get(i)); |
| 182 | + } |
| 183 | + } |
| 184 | + return String.join("/", usage); |
| 185 | + } else { |
| 186 | + return hexDump(0, derOctetString); |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + private static String basicConstraints(byte[] derOctetString) { |
| 191 | + if (derOctetString.length == 4 && derOctetString[3] == 0) { |
| 192 | + // e.g. 04:02:30:00 [octet_string length sequence size] |
| 193 | + return "CA:FALSE"; |
| 194 | + } else if (derOctetString.length >= 7 && derOctetString[2] == 48 && derOctetString[4] == 1) { |
| 195 | + // e.g. 04:05:30:03:01:01:FF [octet_string length sequence boolean length boolean_value] |
| 196 | + return "CA:" + (derOctetString[6] == 0 ? "FALSE" : "TRUE"); |
| 197 | + } else { |
| 198 | + return hexDump(0, derOctetString); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + private static String authorityKeyIdentifier(byte[] derOctetString) { |
| 203 | + if (derOctetString.length == 26 && derOctetString[0] == 04) { |
| 204 | + // e.g. 04:18:30:16:80:14:FB:D2:7C:63:DF:7F:D4:A4:8E:9A:20:43:F5:DC:75:6F:B6:D8:51:6F |
| 205 | + // [octet_string length sequence ?? ?? key_length key] |
| 206 | + return "keyid:" + hexDump(6, derOctetString); |
| 207 | + } else { |
| 208 | + return hexDump(0, derOctetString); |
| 209 | + } |
| 210 | + |
| 211 | + } |
| 212 | + |
| 213 | + private static String extendedKeyUsage(byte[] derOctetString, X509Certificate certificate) { |
| 214 | + List<String> extendedKeyUsage = null; |
| 215 | + try { |
| 216 | + extendedKeyUsage = certificate.getExtendedKeyUsage(); |
| 217 | + if (extendedKeyUsage == null) { |
| 218 | + return hexDump(0, derOctetString); |
| 219 | + } else { |
| 220 | + return String.join("/", extendedKeyUsage.stream() |
| 221 | + .map(oid -> EXTENDED_KEY_USAGE.getOrDefault(oid, oid)) |
| 222 | + .collect(Collectors.toList())); |
| 223 | + } |
| 224 | + } catch (CertificateParsingException e) { |
| 225 | + return PARSING_ERROR; |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | +} |
0 commit comments