All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cassandra.security.PEMReader Maven / Gradle / Ivy

Go to download

The Apache Cassandra Project develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model.

There is a newer version: 5.0.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.cassandra.security;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.util.regex.Pattern.CASE_INSENSITIVE;

/**
 * This is a helper class to read private keys and X509 certifificates encoded based on PEM (RFC 1421)
 * format. It can read Password Based Encrypted (PBE henceforth) private keys as well as non-encrypted private keys
 * along with the X509 certificates/cert-chain based on the textual encoding defined in the RFC 7468
 * 

* The input private key must be in PKCS#8 format. *

* It returns PKCS#8 formatted private key and X509 certificates. */ public final class PEMReader { /** * The private key can be with any of these algorithms in order for this read to successfully parse it. * Currently, supported algorithms are, *

     *     RSA, DSA or EC
     * 
* The first one to be evaluated is RSA, being the most common for private keys. */ public static final Set SUPPORTED_PRIVATE_KEY_ALGORITHMS = ImmutableSet.of("RSA", "DSA", "EC"); private static final Logger logger = LoggerFactory.getLogger(PEMReader.class); private static final Pattern CERT_PATTERN = Pattern.compile("-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+([a-z0-9+/=\\r\\n]+)-+END\\s+.*CERTIFICATE[^-]*-+", CASE_INSENSITIVE); private static final Pattern KEY_PATTERN = Pattern.compile("-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+([a-z0-9+/=\\r\\n]+)-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", CASE_INSENSITIVE); /** * Extracts private key from the PEM content for the private key, assuming its not PBE. * * @param unencryptedPEMKey private key stored as PEM content * @return {@link PrivateKey} upon successful reading of the private key * @throws IOException in case PEM reading fails * @throws GeneralSecurityException in case any issue encountered while reading the private key */ public static PrivateKey extractPrivateKey(String unencryptedPEMKey) throws IOException, GeneralSecurityException { return extractPrivateKey(unencryptedPEMKey, null); } /** * Extracts private key from the Password Based Encrypted PEM content for the private key. * * @param pemKey PBE private key stored as PEM content * @param keyPassword password to be used for the private key decryption * @return {@link PrivateKey} upon successful reading of the private key * @throws IOException in case PEM reading fails * @throws GeneralSecurityException in case any issue encountered while reading the private key */ public static PrivateKey extractPrivateKey(String pemKey, String keyPassword) throws IOException, GeneralSecurityException { PKCS8EncodedKeySpec keySpec; String base64EncodedKey = extractBase64EncodedKey(pemKey); byte[] derKeyBytes = decodeBase64(base64EncodedKey); if (!StringUtils.isEmpty(keyPassword)) { logger.debug("Encrypted key's length: {}, key's password length: {}", derKeyBytes.length, keyPassword.length()); EncryptedPrivateKeyInfo epki = new EncryptedPrivateKeyInfo(derKeyBytes); logger.debug("Encrypted private key info's algorithm name: {}", epki.getAlgName()); AlgorithmParameters params = epki.getAlgParameters(); PBEKeySpec pbeKeySpec = new PBEKeySpec(keyPassword.toCharArray()); Key encryptionKey = SecretKeyFactory.getInstance(epki.getAlgName()).generateSecret(pbeKeySpec); pbeKeySpec.clearPassword(); logger.debug("Key algorithm: {}, key format: {}", encryptionKey.getAlgorithm(), encryptionKey.getFormat()); Cipher cipher = Cipher.getInstance(epki.getAlgName()); cipher.init(Cipher.DECRYPT_MODE, encryptionKey, params); byte[] rawKeyBytes; try { rawKeyBytes = cipher.doFinal(epki.getEncryptedData()); } catch (BadPaddingException e) { throw new GeneralSecurityException("Failed to decrypt the private key data. Either the password " + "provided for the key is wrong or the private key data is " + "corrupted. msg=" + e.getMessage(), e); } logger.debug("Decrypted private key's length: {}", rawKeyBytes.length); keySpec = new PKCS8EncodedKeySpec(rawKeyBytes); } else { logger.debug("Key length: {}", derKeyBytes.length); keySpec = new PKCS8EncodedKeySpec(derKeyBytes); } PrivateKey privateKey = null; /* * Ideally we can inspect the OID (Object Identifier) from the private key with ASN.1 parser and identify the * actual algorithm of the private key. For doing that, we have to use some special library like BouncyCastle. * However in the absence of that, below brute-force approach can work- that is to try out all the supported * private key algorithms given that there are only three major algorithms to verify against. */ for (String privateKeyAlgorithm : SUPPORTED_PRIVATE_KEY_ALGORITHMS) { try { privateKey = KeyFactory.getInstance(privateKeyAlgorithm).generatePrivate(keySpec); logger.info("Parsing for the private key finished with {} algorithm.", privateKeyAlgorithm); return privateKey; } catch (Exception e) { logger.debug("Failed to parse the private key with {} algorithm. Will try the other supported " + "algorithms.", privateKeyAlgorithm); } } throw new GeneralSecurityException("The given private key could not be parsed with any of the supported " + "algorithms. Please see PEMReader#SUPPORTED_PRIVATE_KEY_ALGORITHMS."); } /** * Extracts the certificates/cert-chain from the PEM content. * * @param pemCerts certificates/cert-chain stored as PEM content * @return X509 certiificate list * @throws GeneralSecurityException in case any issue encountered while reading the certificates */ public static Certificate[] extractCertificates(String pemCerts) throws GeneralSecurityException { List certificateList = new ArrayList<>(); List base64EncodedCerts = extractBase64EncodedCerts(pemCerts); for (String base64EncodedCertificate : base64EncodedCerts) { certificateList.add(generateCertificate(base64EncodedCertificate)); } Certificate[] certificates = certificateList.toArray(new Certificate[0]); return certificates; } /** * Generates the X509 certificate object given the base64 encoded PEM content. * * @param base64Certificate base64 encoded PEM content for the certificate * @return X509 certificate * @throws GeneralSecurityException in case any issue encountered while reading the certificate */ private static Certificate generateCertificate(String base64Certificate) throws GeneralSecurityException { byte[] decodedCertificateBytes = decodeBase64(base64Certificate); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(decodedCertificateBytes)); logCertificateDetails(certificate); return certificate; } /** * Logs X509 certificate details for the debugging purpose with {@code INFO} level log. * Namely, it prints - Subject DN, Issuer DN, Certificate serial number and the certificate expiry date which * could be very valuable for debugging any certificate related issues. * * @param certificate certificate to log */ private static void logCertificateDetails(X509Certificate certificate) { assert certificate != null; logger.info("*********** Certificate Details *****************"); logger.info("Subject DN: {}", certificate.getSubjectDN()); logger.info("Issuer DN: {}", certificate.getIssuerDN()); logger.info("Serial Number: {}", certificate.getSerialNumber()); logger.info("Expiry: {}", certificate.getNotAfter()); } /** * Parses the PEM formatted private key based on the standard pattern specified by the RFC 7468. * * @param pemKey private key stored as PEM content * @return base64 string contained within the defined encapsulation boundaries by the above RFC * @throws GeneralSecurityException in case any issue encountered while parsing the key */ private static String extractBase64EncodedKey(String pemKey) throws GeneralSecurityException { Matcher matcher = KEY_PATTERN.matcher(pemKey); if (matcher.find()) { return matcher.group(1).replaceAll("\\s", ""); } else { throw new GeneralSecurityException("Invalid private key format"); } } /** * Parses the PEM formatted certificate/public-key based on the standard pattern specified by the * RFC 7468. * * @param pemCerts certificate/public-key stored as PEM content * @return list of base64 encoded certificates within the defined encapsulation boundaries by the above RFC * @throws GeneralSecurityException in case any issue encountered parsing the certificate */ private static List extractBase64EncodedCerts(String pemCerts) throws GeneralSecurityException { List certificateList = new ArrayList<>(); Matcher matcher = CERT_PATTERN.matcher(pemCerts); if (!matcher.find()) { throw new GeneralSecurityException("Invalid certificate format"); } for (int start = 0; matcher.find(start); start = matcher.end()) { String certificate = matcher.group(1).replaceAll("\\s", ""); certificateList.add(certificate); } return certificateList; } /** * Decodes given input in Base64 format. * * @param base64Input input to be decoded * @return byte[] containing decoded bytes * @throws GeneralSecurityException in case it fails to decode the given base64 input */ private static byte[] decodeBase64(String base64Input) throws GeneralSecurityException { try { return Base64.getDecoder().decode(base64Input); } catch (IllegalArgumentException e) { throw new GeneralSecurityException("Failed to decode given base64 input. msg=" + e.getMessage(), e); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy