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 java.io.IOException; 021import java.util.Arrays; 022import java.util.List; 023import java.util.Set; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.stream.Collectors; 026import org.apache.hadoop.conf.Configuration; 027import org.apache.yetus.audience.InterfaceAudience; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031/** 032 * One keystore or truststore, resolved from configuration with every attribute drawn from a single 033 * key prefix. 034 * <p/> 035 * Each TLS surface exposes two prefixes for the same store: a role-scoped one added for single-EKU 036 * certificate support (e.g. {@code hbase.rpc.tls.server.}) and the historical unscoped one (e.g. 037 * {@code hbase.rpc.tls.}). Resolving attribute by attribute would let a store's location come from 038 * one prefix while its password or type came from the other, which opens either the wrong file or 039 * the right file with the wrong credentials. {@link #resolve} therefore picks the prefix once, from 040 * the location, and reads the rest of the store from that same prefix. 041 */ 042@InterfaceAudience.Private 043public final class TLSStore { 044 045 private static final Logger LOG = LoggerFactory.getLogger(TLSStore.class); 046 047 /** Stores already checked for unused legacy keys, so the scan and the warning happen once. */ 048 private static final Set<String> LOGGED_STORES = ConcurrentHashMap.newKeySet(); 049 050 /** 051 * The key postfixes making up one store, by surface. The RPC keys use {@code .location} while the 052 * servlet surfaces (REST, Thrift) use {@code .store}, and only the latter have a separate 053 * {@code keypassword}. 054 */ 055 public enum Keys { 056 RPC_KEYSTORE("keystore.location", "keystore.password", "keystore.type", null), 057 RPC_TRUSTSTORE("truststore.location", "truststore.password", "truststore.type", null), 058 SERVLET_KEYSTORE("keystore.store", "keystore.password", "keystore.type", 059 "keystore.keypassword"), 060 SERVLET_TRUSTSTORE("truststore.store", "truststore.password", "truststore.type", null); 061 062 private final String location; 063 private final String password; 064 private final String type; 065 private final String keyPassword; 066 067 Keys(String location, String password, String type, String keyPassword) { 068 this.location = location; 069 this.password = password; 070 this.type = type; 071 this.keyPassword = keyPassword; 072 } 073 074 private String[] all() { 075 return keyPassword == null 076 ? new String[] { location, password, type } 077 : new String[] { location, password, type, keyPassword }; 078 } 079 } 080 081 private final String location; 082 private final char[] password; 083 private final char[] keyPassword; 084 private final String type; 085 086 private TLSStore(String location, char[] password, char[] keyPassword, String type) { 087 this.location = location; 088 this.password = password; 089 this.keyPassword = keyPassword; 090 this.type = type; 091 } 092 093 /** 094 * Reads one store from {@code config}. The prefix supplying the location supplies every other 095 * attribute too, so the two prefixes are never combined within a single store. 096 * <p/> 097 * Only the location is mandatory under a prefix: a store may have no password at all, and the 098 * type is auto-detected from the file extension when absent (see 099 * {@link KeyStoreFileType#fromPropertyValueOrFileName}). Keeping the legacy keys in place while 100 * migrating is valid -- the ones left unused are logged once so they can be cleaned up. 101 * @param config the configuration to read from 102 * @param rolePrefix role-scoped prefix, e.g. {@code hbase.rpc.tls.server.} 103 * @param legacyPrefix historical unscoped prefix, e.g. {@code hbase.rpc.tls.} 104 * @param keys which store to read, and under which postfixes 105 * @throws IllegalArgumentException if {@code rolePrefix} supplies any attribute but not the 106 * location, since the location would then be taken from 107 * {@code legacyPrefix} 108 */ 109 public static TLSStore resolve(Configuration config, String rolePrefix, String legacyPrefix, 110 Keys keys) throws IOException { 111 String roleLocationKey = rolePrefix + keys.location; 112 boolean useRole = config.get(roleLocationKey) != null; 113 if (!useRole) { 114 for (String postfix : keys.all()) { 115 if (config.get(rolePrefix + postfix) != null) { 116 throw new IllegalArgumentException( 117 rolePrefix + postfix + " is set, but " + roleLocationKey + " is not. Once any " 118 + rolePrefix + " key is used, that prefix must supply the store location."); 119 } 120 } 121 } 122 String prefix = useRole ? rolePrefix : legacyPrefix; 123 if (useRole) { 124 logOnce(config, rolePrefix, legacyPrefix, keys); 125 } 126 return new TLSStore(config.get(prefix + keys.location, ""), 127 config.getPassword(prefix + keys.password), 128 keys.keyPassword == null ? null : config.getPassword(prefix + keys.keyPassword), 129 config.get(prefix + keys.type, "")); 130 } 131 132 private static void logOnce(Configuration config, String rolePrefix, String legacyPrefix, 133 Keys keys) { 134 if (!LOGGED_STORES.add(rolePrefix + keys.location)) { 135 return; 136 } 137 List<String> unused = Arrays.stream(keys.all()).map(postfix -> legacyPrefix + postfix) 138 .filter(key -> config.get(key) != null).collect(Collectors.toList()); 139 if (!unused.isEmpty()) { 140 LOG.warn("{} supplies this store, so these keys are unused: {}. Remove them once the" 141 + " migration is complete.", rolePrefix, String.join(", ", unused)); 142 } 143 } 144 145 public String getLocation() { 146 return location; 147 } 148 149 public String getPassword() { 150 return password == null ? null : new String(password); 151 } 152 153 /** Falls back to the store password when no separate key password is configured. */ 154 public String getKeyPassword() { 155 char[] effective = getKeyPasswordChars(); 156 return effective == null ? null : new String(effective); 157 } 158 159 /** For the netty paths, whose key/trust manager factories take char[]. */ 160 public char[] getPasswordChars() { 161 return password; 162 } 163 164 /** @see #getKeyPassword() */ 165 public char[] getKeyPasswordChars() { 166 return keyPassword != null ? keyPassword : password; 167 } 168 169 public String getType() { 170 return type; 171 } 172}