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}