/* * Copyright (c) 2013-2014 EMC Corporation * All Rights Reserved */ package com.emc.storageos.security.keystore.impl; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Signature; import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.storageos.security.exceptions.SecurityException; import com.emc.storageos.security.helpers.SecurityUtil; import com.emc.storageos.svcs.errorhandling.resources.APIException; import sun.security.x509.AlgorithmId; import sun.security.x509.AuthorityKeyIdentifierExtension; import sun.security.x509.CertificateAlgorithmId; import sun.security.x509.CertificateExtensions; import sun.security.x509.CertificateSerialNumber; import sun.security.x509.CertificateValidity; import sun.security.x509.CertificateVersion; import sun.security.x509.CertificateX509Key; import sun.security.x509.GeneralName; import sun.security.x509.GeneralNames; import sun.security.x509.IPAddressName; import sun.security.x509.KeyIdentifier; import sun.security.x509.SubjectAlternativeNameExtension; import sun.security.x509.SubjectKeyIdentifierExtension; import sun.security.x509.X500Name; import sun.security.x509.X509CertImpl; import sun.security.x509.X509CertInfo; /** * Class responsible for generating RSA keys and their certificates. */ public class KeyCertificatePairGenerator { private static Logger log = LoggerFactory.getLogger(KeyCertificatePairGenerator.class); public static final String CERTIFICATE_COMMON_NAME_FORMAT = "CN=%s"; public static final String PRIVATE_RSA_KEY_PEM_FORMAT_NAME = "SSLCPKCS1RSAPrivateKeyPEM"; public static final String PRIVATE_RSA_KEY_BER_FORMAT_NAME = "RSAPrivateKeyBER"; public static final String RSA_JAVA_DEVICE_NAME = "Java"; private static final int SUBJECT_ALT_NAME_DNS_NAME = 2; private static final int SUBJECT_ALT_NAME_IP_ADDRESS = 7; private static final int PEM_OUTPUT_LINE_SIZE = 64; public static final String PEM_BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; public static final String PEM_END_CERT = "-----END CERTIFICATE-----"; public static final String PEM_BEGIN_RSA_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----"; public static final String PEM_END_RSA_PRIVATE_KEY = "-----END RSA PRIVATE KEY-----"; private KeyCertificateAlgorithmValuesHolder valuesHolder; public void setKeyCertificateAlgorithmValuesHolder( KeyCertificateAlgorithmValuesHolder valuesHolder) { this.valuesHolder = valuesHolder; } /** * Create a self-signed X.509 Certificate * * @param pair the KeyPair */ private X509Certificate generateCertificate(KeyPair pair) throws GeneralSecurityException, IOException { PublicKey pubKey = loadPublicKeyFromBytes(pair.getPublic().getEncoded()); PrivateKey privkey = pair.getPrivate(); X509CertInfo info = new X509CertInfo(); Date from = getNotBefore(); Date to = new Date(from.getTime() + valuesHolder.getCertificateValidityInDays() * 86400000L); CertificateValidity interval = new CertificateValidity(from, to); BigInteger sn = new BigInteger(64, new SecureRandom()); X500Name owner = new X500Name(String.format(CERTIFICATE_COMMON_NAME_FORMAT, valuesHolder.getCertificateCommonName())); info.set(X509CertInfo.VALIDITY, interval); info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(sn)); info.set(X509CertInfo.SUBJECT, owner); info.set(X509CertInfo.ISSUER, owner); info.set(X509CertInfo.KEY, new CertificateX509Key(pubKey)); info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); AlgorithmId keyAlgo = AlgorithmId .get(KeyCertificateAlgorithmValuesHolder.DEFAULT_KEY_ALGORITHM); info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(keyAlgo)); AlgorithmId signingAlgo = AlgorithmId.get(valuesHolder.getSigningAlgorithm()); info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, signingAlgo); // add extensions CertificateExtensions ext = new CertificateExtensions(); ext.set(SubjectKeyIdentifierExtension.NAME, new SubjectKeyIdentifierExtension(new KeyIdentifier(pubKey).getIdentifier())); // CA public key is the same as our public key (self signed) ext.set(AuthorityKeyIdentifierExtension.NAME, new AuthorityKeyIdentifierExtension(new KeyIdentifier(pubKey), null, null)); ext.set(SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(subjectAltNames())); info.set(X509CertInfo.EXTENSIONS, ext); X509CertImpl cert = new X509CertImpl(info); cert.sign(privkey, valuesHolder.getSigningAlgorithm()); return cert; } private GeneralNames subjectAltNames() throws IOException{ // Consists of VIP and internal node's IPs (IPv4 and IPV6 if have) but no DNS/Host name. GeneralNames subAltNames = new GeneralNames(); for (InetAddress entry : valuesHolder.getAddresses()) { subAltNames.add(new GeneralName(new IPAddressName(entry.getHostAddress()))); } return subAltNames; } /** * Set Not Before to a few days in past just in case system runs into problem due to bad system clock. * * @return */ private Date getNotBefore() { Calendar cal = Calendar.getInstance(); cal.add(Calendar.DATE, valuesHolder.getNotBeforeOffset()); return cal.getTime(); } public KeyCertificateEntry generateKeyCertificatePair() throws SecurityException { KeyPair keyPair = generateKeyPair(); KeyCertificateEntry returnedEntry; try { Certificate cert = generateCertificate(keyPair); Certificate[] chain = { cert }; returnedEntry = new KeyCertificateEntry(keyPair.getPrivate(), chain); } catch (GeneralSecurityException e) { throw SecurityException.fatals.failedToCreateCertificate(e); } catch (IOException e) { throw SecurityException.fatals.failedToCreateCertificate(e); } finally { // Cryptographic objects should be cleared once they are no longer // needed. SecurityUtil.clearSensitiveData(keyPair.getPublic()); SecurityUtil.clearSensitiveData(keyPair.getPrivate()); SecurityUtil.clearSensitiveData(keyPair); } return returnedEntry; } public static PrivateKey loadPrivateKeyFromBytes(byte[] keyBytes) { try { KeyFactory keyFactory = KeyFactory .getInstance(KeyCertificateAlgorithmValuesHolder.DEFAULT_KEY_ALGORITHM); return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); } catch (Exception e) { throw SecurityException.fatals.failedToLoadPrivateKey(e); } } public static PublicKey loadPublicKeyFromBytes(byte[] keyBytes) throws SecurityException, NoSuchAlgorithmException { try { KeyFactory keyFactory = KeyFactory.getInstance(KeyCertificateAlgorithmValuesHolder.DEFAULT_KEY_ALGORITHM); return keyFactory.generatePublic(new X509EncodedKeySpec(keyBytes)); } catch (InvalidKeySpecException e) { throw SecurityException.fatals.failedToLoadPublicKey(e); } } /** * verifies that the specified key matches the specified certificate * * @param entryToVerify * @throws SecurityException if the certificate specified is not x509 certificate, or if validation * fails */ public static void verifyKeyCertificateEntry(KeyCertificateEntry entryToVerify) throws SecurityException { String signThis = "Sign this to verify that the key and certificate match"; Signature signatureFactory = null; byte[] signature; PrivateKey key = null; try { // we only accept x509certificates if (!(entryToVerify.getCertificateChain()[0] instanceof X509Certificate)) { throw SecurityException.fatals.certificateMustBeX509(); } X509Certificate cert = (X509Certificate) entryToVerify.getCertificateChain()[0]; key = loadPrivateKeyFromBytes(entryToVerify.getKey()); signatureFactory = Signature.getInstance(cert.getSigAlgName()); signatureFactory.initSign(key); signatureFactory.update(signThis.getBytes()); signature = signatureFactory.sign(); signatureFactory.initVerify(entryToVerify.getCertificateChain()[0] .getPublicKey()); signatureFactory.update(signThis.getBytes()); if (!signatureFactory.verify(signature)) { throw APIException.badRequests.keyCertificateVerificationFailed(); } } catch (NoSuchAlgorithmException e) { throw APIException.badRequests.keyCertificateVerificationFailed(e); } catch (InvalidKeyException e) { throw APIException.badRequests.keyCertificateVerificationFailed(e); } catch (SignatureException e) { throw APIException.badRequests.keyCertificateVerificationFailed(e); } finally { // all are used here. so clear SecurityUtil.clearSensitiveData(signatureFactory); SecurityUtil.clearSensitiveData(key); } } /** * @param certificateChainString the certificate chain in PEM format or crt format. other formats, such * as DER, p7b and p7c are not supported from BouncyCastle docs: At the * moment this will deal with "-----BEGIN CERTIFICATE-----" to * "-----END CERTIFICATE-----" base 64 encoded certs, as well as the BER * binaries of certificates and some classes of PKCS#7 objects. * @return the certificate chain as Certificate[] * @throws CertificateException if parsing of the certificate chain has failed */ public static Certificate[] getCertificateChainFromString( String certificateChainString) throws CertificateException { InputStream inStream = new ByteArrayInputStream(certificateChainString.getBytes()); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); Collection<? extends Certificate> certs = certFactory.generateCertificates(inStream); return certs.toArray(new Certificate[certs.size()]); } /** * @param certificateStr the certificate chain in PEM format or crt format. other formats, such * as DER, p7b and p7c are not supported from BouncyCastle docs: At the * moment this will deal with "-----BEGIN CERTIFICATE-----" to * "-----END CERTIFICATE-----" base 64 encoded certs, as well as the BER * binaries of certificates and some classes of PKCS#7 objects. * @return the certificate chain as Certificate[] * @throws CertificateException if parsing of the certificate chain has failed */ public static Certificate getCertificateFromString(String certificateStr) throws CertificateException { InputStream inStream = new ByteArrayInputStream(certificateStr.getBytes()); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); return certFactory.generateCertificate(inStream); } /** * @param certChain - the certificate chain to parse to PEM format * @return the specified certificate chain in PEM format * @throws IOException * @throws CertificateEncodingException */ public static String getCertificateChainAsString(Certificate[] certChain) throws CertificateEncodingException { StringBuilder builder = new StringBuilder(); Base64 encoder = new Base64(PEM_OUTPUT_LINE_SIZE); boolean isFirst = true; for (Certificate certificate : certChain) { if (!isFirst) { builder.append(System.lineSeparator()); } builder.append(PEM_BEGIN_CERT); builder.append(System.lineSeparator()); builder.append(encoder.encodeAsString(certificate.getEncoded())); builder.append(PEM_END_CERT); isFirst = false; } return builder.toString(); } /** * @param cert - the certificate to parse to PEM format * @return the specified certificate in PEM format * @throws IOException * @throws CertificateEncodingException */ public static String getCertificateAsString(Certificate cert) throws CertificateEncodingException { Certificate[] chain = { cert }; return getCertificateChainAsString(chain); } /** * Checks if the specified certificate's IPs match the cluste's IPs * * @param cert * @throws IllegalArgumentException when the certificate was not created by this generator */ public boolean isCertificateIPsCorrect(X509Certificate cert) throws IllegalArgumentException { valuesHolder.loadIPsAndNames(); Set<InetAddress> foundIPs = new HashSet<InetAddress>(); try { for (List<?> element : cert.getSubjectAlternativeNames()) { int OID = ((Integer) element.get(0)).intValue(); String name; if (OID == SUBJECT_ALT_NAME_IP_ADDRESS) { name = (String) element.get(1); log.debug("got the following ip from the cert: " + name); foundIPs.add(InetAddress.getByName(name.trim())); } else if (OID != SUBJECT_ALT_NAME_DNS_NAME) { throw new IllegalArgumentException("cert is not self generated"); } } } catch (CertificateParsingException e) { throw new IllegalArgumentException("cert is not self generated"); } catch (UnknownHostException e) { throw new IllegalArgumentException("cert has illegal ip values"); } return valuesHolder.getAddresses().equals(foundIPs); } /** * gets the specified key as its pem representation * * @param keyToParse * @return */ /* * public static String getPrivateKeyAsPEMString(Key keyToParse) * throws IOException, JSAFE_UnimplementedException, * JSAFE_InvalidParameterException, JSAFE_InvalidKeyException { * * JSAFE_PrivateKey privKey = null; * * try { * privKey = JSAFE_PrivateKey.getInstance( * KeyCertificateAlgorithmValuesHolder.DEFAULT_KEY_ALGORITHM, * RSA_JAVA_DEVICE_NAME); * privKey.setKeyData(PRIVATE_RSA_KEY_BER_FORMAT_NAME, * new byte[][]{keyToParse.getEncoded()}); * * byte[][] pemData = privKey.getKeyData(PRIVATE_RSA_KEY_PEM_FORMAT_NAME); * * // this new string and the following substring operations cause key copied in memory and I don't now how to clear. * String pemStr = new String(pemData[0]); * int index = pemStr.indexOf('\n'); * index++; * index += PEM_OUTPUT_LINE_SIZE; * int endIndex = pemStr.indexOf("-----END"); * StringBuilder builder = new StringBuilder(); * builder.append(pemStr.substring(0, index)); * while (index < endIndex) { * builder.append(System.lineSeparator()); * builder.append(pemStr.substring(index, index + PEM_OUTPUT_LINE_SIZE)); * index += PEM_OUTPUT_LINE_SIZE; * } * builder.append(pemStr.substring(index)); * return builder.toString(); * } finally { * if (privKey != null) { * privKey.clearSensitiveData(); * } * } * } */ /** * Loads up the certificate that was set in v1 system property * * @return the certificate if it existed and parsed successfully, null otherwise */ public KeyCertificateEntry tryGetV1Cert() { String pemCertAndKey = valuesHolder.getV1Cert(); KeyCertificateEntry returnedEntry = null; if (!StringUtils.isBlank(pemCertAndKey)) { int pemKeyStart = pemCertAndKey.indexOf(PEM_BEGIN_RSA_PRIVATE_KEY); int pemKeyEnd = pemCertAndKey.indexOf(PEM_END_RSA_PRIVATE_KEY) + PEM_END_RSA_PRIVATE_KEY.length(); int pemCertStart = pemCertAndKey.indexOf(PEM_BEGIN_CERT); int pemCertEnd = pemCertAndKey.lastIndexOf(PEM_END_CERT) + PEM_END_CERT.length(); log.info("pemKeyStart = " + pemKeyStart + ", pemKeyEnd = " + pemKeyEnd + ", pemCertStart = " + pemCertStart + ", pemCertEnd = " + pemCertEnd); if (pemKeyStart != -1 && pemKeyEnd != -1 && pemCertStart != -1 && pemCertEnd != -1) { String pemKey = pemCertAndKey.substring(pemKeyStart, pemKeyEnd); String pemCert = pemCertAndKey.substring(pemCertStart, pemCertEnd); // multiline values are stored in coordinator with "\\n" instead of "\n" pemKey = StringUtils.replace(pemKey, "\\n", "\n"); pemCert = StringUtils.replace(pemCert, "\\n", "\n"); log.info("pemCert = " + pemCert); try { Certificate[] certChain = getCertificateChainFromString(pemCert); // don't support v1 cert for open source version. byte[] keyBytes = SecurityUtil.loadPrivateKeyFromPEMString(pemKey); if (!ArrayUtils.isEmpty(certChain) && !ArrayUtils.isEmpty(keyBytes)) { log.info("parsed key and certificate successfully"); returnedEntry = new KeyCertificateEntry(keyBytes, certChain); } } catch (CertificateException e) { log.error("Could not load v1 certificate chain", e); } catch (Exception e) { log.error("Could not load v1 key", e); } } } return returnedEntry; } /** * Generates a key pair * * @return */ public KeyPair generateKeyPair() { KeyPairGenerator keyGen = null; SecureRandom random = null; try { random = SecureRandom.getInstance(SecurityUtil.getSecuredRandomAlgorithm()); keyGen = KeyPairGenerator.getInstance( KeyCertificateAlgorithmValuesHolder.DEFAULT_KEY_ALGORITHM); keyGen.initialize(valuesHolder.getKeySize(), random); return keyGen.generateKeyPair(); } catch (Exception e) { throw SecurityException.fatals.noSuchAlgorithmException( SecurityUtil.getSecuredRandomAlgorithm(), e); } finally { if (keyGen != null) { SecurityUtil.clearSensitiveData(keyGen); } if (random != null) { SecurityUtil.clearSensitiveData(random); } } } public static void validateKeyAndCertPairing(RSAPrivateKey privateKey, Certificate[] certChain) { KeyCertificateEntry entry = new KeyCertificateEntry(privateKey.getEncoded(), certChain); verifyKeyCertificateEntry(entry); } }