001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the 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
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.io.crypto.tls;
019
020import static java.nio.charset.StandardCharsets.US_ASCII;
021import static java.util.Base64.getMimeDecoder;
022import static java.util.regex.Pattern.CASE_INSENSITIVE;
023import static javax.crypto.Cipher.DECRYPT_MODE;
024
025import java.io.ByteArrayInputStream;
026import java.io.File;
027import java.io.IOException;
028import java.nio.file.Files;
029import java.security.GeneralSecurityException;
030import java.security.KeyFactory;
031import java.security.KeyStore;
032import java.security.KeyStoreException;
033import java.security.PrivateKey;
034import java.security.PublicKey;
035import java.security.cert.Certificate;
036import java.security.cert.CertificateException;
037import java.security.cert.CertificateFactory;
038import java.security.cert.X509Certificate;
039import java.security.spec.InvalidKeySpecException;
040import java.security.spec.PKCS8EncodedKeySpec;
041import java.security.spec.X509EncodedKeySpec;
042import java.util.ArrayList;
043import java.util.List;
044import java.util.regex.Matcher;
045import java.util.regex.Pattern;
046import javax.crypto.Cipher;
047import javax.crypto.EncryptedPrivateKeyInfo;
048import javax.crypto.SecretKey;
049import javax.crypto.SecretKeyFactory;
050import javax.crypto.spec.PBEKeySpec;
051import javax.security.auth.x500.X500Principal;
052
053/**
054 * This file has been copied from the Apache ZooKeeper project.
055 * @see <a href=
056 *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/util/PemReader.java">Base
057 *      revision</a>
058 */
059final class PemReader {
060  private static final Pattern CERT_PATTERN =
061    Pattern.compile("-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" + // Header
062      "([a-z0-9+/=\\r\\n]+)" + // Base64 text
063      "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer
064      CASE_INSENSITIVE);
065
066  private static final Pattern PRIVATE_KEY_PATTERN =
067    Pattern.compile("-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
068      "([a-z0-9+/=\\r\\n]+)" + // Base64 text
069      "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer
070      CASE_INSENSITIVE);
071
072  private static final Pattern PUBLIC_KEY_PATTERN =
073    Pattern.compile("-+BEGIN\\s+.*PUBLIC\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
074      "([a-z0-9+/=\\r\\n]+)" + // Base64 text
075      "-+END\\s+.*PUBLIC\\s+KEY[^-]*-+", // Footer
076      CASE_INSENSITIVE);
077
078  private PemReader() {
079  }
080
081  public static KeyStore loadTrustStore(File certificateChainFile)
082    throws IOException, GeneralSecurityException {
083    KeyStore keyStore = KeyStore.getInstance("JKS");
084    keyStore.load(null, null);
085
086    List<X509Certificate> certificateChain = readCertificateChain(certificateChainFile);
087    for (X509Certificate certificate : certificateChain) {
088      X500Principal principal = certificate.getSubjectX500Principal();
089      keyStore.setCertificateEntry(principal.getName("RFC2253"), certificate);
090    }
091    return keyStore;
092  }
093
094  public static KeyStore loadKeyStore(File certificateChainFile, File privateKeyFile,
095    char[] keyPassword) throws IOException, GeneralSecurityException {
096    PrivateKey key = loadPrivateKey(privateKeyFile, keyPassword);
097
098    List<X509Certificate> certificateChain = readCertificateChain(certificateChainFile);
099    if (certificateChain.isEmpty()) {
100      throw new CertificateException(
101        "Certificate file does not contain any certificates: " + certificateChainFile);
102    }
103
104    KeyStore keyStore = KeyStore.getInstance("JKS");
105    keyStore.load(null, null);
106    keyStore.setKeyEntry("key", key, keyPassword, certificateChain.toArray(new Certificate[0]));
107    return keyStore;
108  }
109
110  public static List<X509Certificate> readCertificateChain(File certificateChainFile)
111    throws IOException, GeneralSecurityException {
112    String contents = new String(Files.readAllBytes(certificateChainFile.toPath()), US_ASCII);
113    return readCertificateChain(contents);
114  }
115
116  public static List<X509Certificate> readCertificateChain(String certificateChain)
117    throws CertificateException {
118    Matcher matcher = CERT_PATTERN.matcher(certificateChain);
119    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
120    List<X509Certificate> certificates = new ArrayList<>();
121
122    int start = 0;
123    while (matcher.find(start)) {
124      byte[] buffer = base64Decode(matcher.group(1));
125      certificates.add(
126        (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(buffer)));
127      start = matcher.end();
128    }
129
130    return certificates;
131  }
132
133  public static PrivateKey loadPrivateKey(File privateKeyFile, char[] keyPassword)
134    throws IOException, GeneralSecurityException {
135    String privateKey = new String(Files.readAllBytes(privateKeyFile.toPath()), US_ASCII);
136    return loadPrivateKey(privateKey, keyPassword);
137  }
138
139  public static PrivateKey loadPrivateKey(String privateKey, char[] keyPassword)
140    throws IOException, GeneralSecurityException {
141    Matcher matcher = PRIVATE_KEY_PATTERN.matcher(privateKey);
142    if (!matcher.find()) {
143      throw new KeyStoreException("did not find a private key");
144    }
145    byte[] encodedKey = base64Decode(matcher.group(1));
146
147    PKCS8EncodedKeySpec encodedKeySpec;
148    if (keyPassword != null && keyPassword.length > 0) {
149      EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey);
150      SecretKeyFactory keyFactory =
151        SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
152      SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(keyPassword));
153
154      Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName());
155      cipher.init(DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters());
156
157      encodedKeySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
158    } else {
159      encodedKeySpec = new PKCS8EncodedKeySpec(encodedKey);
160    }
161
162    // this code requires a key in PKCS8 format which is not the default openssl format
163    // to convert to the PKCS8 format you use : openssl pkcs8 -topk8 ...
164    try {
165      KeyFactory keyFactory = KeyFactory.getInstance("RSA");
166      return keyFactory.generatePrivate(encodedKeySpec);
167    } catch (InvalidKeySpecException ignore) {
168      // ignore
169    }
170
171    try {
172      KeyFactory keyFactory = KeyFactory.getInstance("EC");
173      return keyFactory.generatePrivate(encodedKeySpec);
174    } catch (InvalidKeySpecException ignore) {
175      // ignore
176    }
177
178    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
179    return keyFactory.generatePrivate(encodedKeySpec);
180  }
181
182  public static PublicKey loadPublicKey(File publicKeyFile)
183    throws IOException, GeneralSecurityException {
184    String publicKey = new String(Files.readAllBytes(publicKeyFile.toPath()), US_ASCII);
185    return loadPublicKey(publicKey);
186  }
187
188  public static PublicKey loadPublicKey(String publicKey) throws GeneralSecurityException {
189    Matcher matcher = PUBLIC_KEY_PATTERN.matcher(publicKey);
190    if (!matcher.find()) {
191      throw new KeyStoreException("did not find a public key");
192    }
193    String data = matcher.group(1);
194    byte[] encodedKey = base64Decode(data);
195
196    X509EncodedKeySpec encodedKeySpec = new X509EncodedKeySpec(encodedKey);
197    try {
198      KeyFactory keyFactory = KeyFactory.getInstance("RSA");
199      return keyFactory.generatePublic(encodedKeySpec);
200    } catch (InvalidKeySpecException ignore) {
201      // ignore
202    }
203
204    try {
205      KeyFactory keyFactory = KeyFactory.getInstance("EC");
206      return keyFactory.generatePublic(encodedKeySpec);
207    } catch (InvalidKeySpecException ignore) {
208      // ignore
209    }
210
211    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
212    return keyFactory.generatePublic(encodedKeySpec);
213  }
214
215  private static byte[] base64Decode(String base64) {
216    return getMimeDecoder().decode(base64.getBytes(US_ASCII));
217  }
218}