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 org.junit.jupiter.api.Assertions.assertArrayEquals;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertFalse;
023import static org.junit.jupiter.api.Assertions.assertNull;
024import static org.junit.jupiter.api.Assertions.assertThrows;
025import static org.junit.jupiter.api.Assertions.assertTrue;
026import static org.junit.jupiter.api.Assumptions.assumeTrue;
027import static org.mockito.Mockito.mock;
028
029import java.security.Security;
030import java.util.Arrays;
031import java.util.Collections;
032import org.apache.hadoop.conf.Configuration;
033import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate;
034import org.apache.hadoop.hbase.exceptions.KeyManagerException;
035import org.apache.hadoop.hbase.exceptions.SSLContextException;
036import org.apache.hadoop.hbase.exceptions.TrustManagerException;
037import org.apache.hadoop.hbase.testclassification.SecurityTests;
038import org.apache.hadoop.hbase.testclassification.SmallTests;
039import org.junit.jupiter.api.Tag;
040import org.junit.jupiter.api.TestTemplate;
041
042import org.apache.hbase.thirdparty.io.netty.buffer.ByteBufAllocator;
043import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslContext;
044
045/**
046 * This file has been copied from the Apache ZooKeeper project.
047 * @see <a href=
048 *      "https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/test/java/org/apache/zookeeper/common/X509UtilTest.java">Base
049 *      revision</a>
050 */
051@Tag(SecurityTests.TAG)
052@Tag(SmallTests.TAG)
053@HBaseParameterizedTestTemplate(name = "{index}: caKeyType={0}, certKeyType={1}, keyPassword={2}")
054public class TestX509Util extends AbstractTestX509Parameterized {
055
056  private static final char[] EMPTY_CHAR_ARRAY = new char[0];
057
058  public TestX509Util(X509KeyType caKeyType, X509KeyType certKeyType, char[] keyPassword) {
059    super(caKeyType, certKeyType, keyPassword);
060  }
061
062  @TestTemplate
063  public void testCreateSSLContextWithClientAuthDefault() throws Exception {
064    SslContext sslContext = X509Util.createSslContextForServer(conf);
065    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
066    assertTrue(sslContext.newEngine(byteBufAllocatorMock).getNeedClientAuth());
067  }
068
069  @TestTemplate
070  public void testCreateSSLContextWithClientAuthNEED() throws Exception {
071    conf.set(X509Util.HBASE_SERVER_NETTY_TLS_CLIENT_AUTH_MODE, X509Util.ClientAuth.NEED.name());
072    SslContext sslContext = X509Util.createSslContextForServer(conf);
073    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
074    assertTrue(sslContext.newEngine(byteBufAllocatorMock).getNeedClientAuth());
075  }
076
077  @TestTemplate
078  public void testCreateSSLContextWithClientAuthWANT() throws Exception {
079    conf.set(X509Util.HBASE_SERVER_NETTY_TLS_CLIENT_AUTH_MODE, X509Util.ClientAuth.WANT.name());
080    SslContext sslContext = X509Util.createSslContextForServer(conf);
081    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
082    assertTrue(sslContext.newEngine(byteBufAllocatorMock).getWantClientAuth());
083  }
084
085  @TestTemplate
086  public void testCreateSSLContextWithClientAuthNONE() throws Exception {
087    conf.set(X509Util.HBASE_SERVER_NETTY_TLS_CLIENT_AUTH_MODE, X509Util.ClientAuth.NONE.name());
088    SslContext sslContext = X509Util.createSslContextForServer(conf);
089    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
090    assertFalse(sslContext.newEngine(byteBufAllocatorMock).getNeedClientAuth());
091    assertFalse(sslContext.newEngine(byteBufAllocatorMock).getWantClientAuth());
092  }
093
094  @TestTemplate
095  public void testCreateSSLContextWithoutCustomProtocol() throws Exception {
096    SslContext sslContext = X509Util.createSslContextForClient(conf);
097    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
098    assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2" },
099      sslContext.newEngine(byteBufAllocatorMock).getEnabledProtocols());
100  }
101
102  @TestTemplate
103  public void testCreateTcNativeSSLContextWithoutCustomProtocol() throws Exception {
104    conf.set(X509Util.TLS_USE_OPENSSL, "true");
105    SslContext sslContext = X509Util.createSslContextForClient(conf);
106    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
107    assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2" },
108      sslContext.newEngine(byteBufAllocatorMock).getEnabledProtocols());
109  }
110
111  @TestTemplate
112  public void testCreateSSLContextWithCustomProtocol() throws Exception {
113    final String protocol = "TLSv1.1";
114    conf.set(X509Util.TLS_CONFIG_PROTOCOL, protocol);
115    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
116    SslContext sslContext = X509Util.createSslContextForServer(conf);
117    assertEquals(Collections.singletonList(protocol),
118      Arrays.asList(sslContext.newEngine(byteBufAllocatorMock).getEnabledProtocols()));
119  }
120
121  @TestTemplate
122  public void testCreateSSLContextWithoutKeyStoreLocationServer() {
123    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
124    assertThrows(SSLContextException.class, () -> {
125      X509Util.createSslContextForServer(conf);
126    });
127  }
128
129  @TestTemplate
130  public void testCreateSSLContextWithoutKeyStoreLocationClient() throws Exception {
131    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
132    X509Util.createSslContextForClient(conf);
133  }
134
135  @TestTemplate
136  public void testCreateSSLContextWithoutTrustStoreLocationClient() throws Exception {
137    conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_LOCATION);
138    X509Util.createSslContextForClient(conf);
139  }
140
141  @TestTemplate
142  public void testCreateSSLContextWithoutTrustStoreLocationServer() throws Exception {
143    conf.unset(X509Util.TLS_CONFIG_TRUSTSTORE_LOCATION);
144    X509Util.createSslContextForServer(conf);
145  }
146
147  // It would be great to test the value of PKIXBuilderParameters#setRevocationEnabled,
148  // but it does not appear to be possible
149  @TestTemplate
150  public void testCRLEnabled() throws Exception {
151    conf.setBoolean(X509Util.TLS_CONFIG_CLR, true);
152    X509Util.createSslContextForServer(conf);
153    assertTrue(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation")));
154    assertTrue(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP")));
155    assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable")));
156  }
157
158  @TestTemplate
159  public void testCRLDisabled() throws Exception {
160    X509Util.createSslContextForServer(conf);
161    assertFalse(Boolean.valueOf(System.getProperty("com.sun.net.ssl.checkRevocation")));
162    assertFalse(Boolean.valueOf(System.getProperty("com.sun.security.enableCRLDP")));
163    assertFalse(Boolean.valueOf(Security.getProperty("ocsp.enable")));
164  }
165
166  @TestTemplate
167  public void testLoadPEMKeyStore() throws Exception {
168    // Make sure we can instantiate a key manager from the PEM file on disk
169    X509Util.createKeyManager(
170      x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
171      x509TestContext.getKeyStorePassword(), KeyStoreFileType.PEM.getPropertyValue());
172  }
173
174  @TestTemplate
175  public void testLoadPEMKeyStoreNullPassword() throws Exception {
176    assumeTrue(Arrays.equals(x509TestContext.getKeyStorePassword(), EMPTY_CHAR_ARRAY));
177    // Make sure that empty password and null password are treated the same
178    X509Util.createKeyManager(
179      x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), null,
180      KeyStoreFileType.PEM.getPropertyValue());
181  }
182
183  @TestTemplate
184  public void testLoadPEMKeyStoreAutodetectStoreFileType() throws Exception {
185    // Make sure we can instantiate a key manager from the PEM file on disk
186    X509Util.createKeyManager(
187      x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
188      x509TestContext.getKeyStorePassword(),
189      null /* null StoreFileType means 'autodetect from file extension' */);
190  }
191
192  @TestTemplate
193  public void testLoadPEMKeyStoreWithWrongPassword() {
194    assertThrows(KeyManagerException.class, () -> {
195      // Attempting to load with the wrong key password should fail
196      X509Util.createKeyManager(
197        x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
198        "wrong password".toCharArray(), // intentionally use the wrong password
199        KeyStoreFileType.PEM.getPropertyValue());
200    });
201  }
202
203  @TestTemplate
204  public void testLoadPEMTrustStore() throws Exception {
205    // Make sure we can instantiate a trust manager from the PEM file on disk
206    X509Util.createTrustManager(
207      x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
208      x509TestContext.getTrustStorePassword(), KeyStoreFileType.PEM.getPropertyValue(), false,
209      false, true, true);
210  }
211
212  @TestTemplate
213  public void testLoadPEMTrustStoreNullPassword() throws Exception {
214    assumeTrue(Arrays.equals(x509TestContext.getTrustStorePassword(), EMPTY_CHAR_ARRAY));
215    // Make sure that empty password and null password are treated the same
216    X509Util.createTrustManager(
217      x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(), null,
218      KeyStoreFileType.PEM.getPropertyValue(), false, false, true, true);
219  }
220
221  @TestTemplate
222  public void testLoadPEMTrustStoreAutodetectStoreFileType() throws Exception {
223    // Make sure we can instantiate a trust manager from the PEM file on disk
224    X509Util.createTrustManager(
225      x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath(),
226      x509TestContext.getTrustStorePassword(), null, // null StoreFileType means 'autodetect from
227                                                     // file extension'
228      false, false, true, true);
229  }
230
231  @TestTemplate
232  public void testLoadJKSKeyStore() throws Exception {
233    // Make sure we can instantiate a key manager from the JKS file on disk
234    X509Util.createKeyManager(
235      x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
236      x509TestContext.getKeyStorePassword(), KeyStoreFileType.JKS.getPropertyValue());
237  }
238
239  @TestTemplate
240  public void testLoadJKSKeyStoreNullPassword() throws Exception {
241    assumeTrue(Arrays.equals(x509TestContext.getKeyStorePassword(), EMPTY_CHAR_ARRAY));
242    // Make sure that empty password and null password are treated the same
243    X509Util.createKeyManager(
244      x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), null,
245      KeyStoreFileType.JKS.getPropertyValue());
246  }
247
248  @TestTemplate
249  public void testLoadJKSKeyStoreAutodetectStoreFileType() throws Exception {
250    // Make sure we can instantiate a key manager from the JKS file on disk
251    X509Util.createKeyManager(
252      x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
253      x509TestContext.getKeyStorePassword(),
254      null /* null StoreFileType means 'autodetect from file extension' */);
255  }
256
257  @TestTemplate
258  public void testLoadJKSKeyStoreWithWrongPassword() {
259    assertThrows(KeyManagerException.class, () -> {
260      // Attempting to load with the wrong key password should fail
261      X509Util.createKeyManager(
262        x509TestContext.getKeyStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
263        "wrong password".toCharArray(), KeyStoreFileType.JKS.getPropertyValue());
264    });
265  }
266
267  @TestTemplate
268  public void testLoadJKSTrustStore() throws Exception {
269    // Make sure we can instantiate a trust manager from the JKS file on disk
270    X509Util.createTrustManager(
271      x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
272      x509TestContext.getTrustStorePassword(), KeyStoreFileType.JKS.getPropertyValue(), true, true,
273      true, true);
274  }
275
276  @TestTemplate
277  public void testLoadJKSTrustStoreNullPassword() throws Exception {
278    assumeTrue(Arrays.equals(x509TestContext.getTrustStorePassword(), EMPTY_CHAR_ARRAY));
279    // Make sure that empty password and null password are treated the same
280    X509Util.createTrustManager(
281      x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(), null,
282      KeyStoreFileType.JKS.getPropertyValue(), false, false, true, true);
283  }
284
285  @TestTemplate
286  public void testLoadJKSTrustStoreAutodetectStoreFileType() throws Exception {
287    // Make sure we can instantiate a trust manager from the JKS file on disk
288    // null StoreFileType means 'autodetect from file extension'
289    X509Util.createTrustManager(
290      x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
291      x509TestContext.getTrustStorePassword(), null, true, true, true, true);
292  }
293
294  @TestTemplate
295  public void testLoadJKSTrustStoreWithWrongPassword() {
296    assertThrows(TrustManagerException.class, () -> {
297      // Attempting to load with the wrong key password should fail
298      X509Util.createTrustManager(
299        x509TestContext.getTrustStoreFile(KeyStoreFileType.JKS).getAbsolutePath(),
300        "wrong password".toCharArray(), KeyStoreFileType.JKS.getPropertyValue(), true, true, true,
301        true);
302    });
303  }
304
305  @TestTemplate
306  public void testLoadPKCS12KeyStore() throws Exception {
307    // Make sure we can instantiate a key manager from the PKCS12 file on disk
308    X509Util.createKeyManager(
309      x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
310      x509TestContext.getKeyStorePassword(), KeyStoreFileType.PKCS12.getPropertyValue());
311  }
312
313  @TestTemplate
314  public void testLoadPKCS12KeyStoreNullPassword() throws Exception {
315    assumeTrue(Arrays.equals(x509TestContext.getKeyStorePassword(), EMPTY_CHAR_ARRAY));
316    // Make sure that empty password and null password are treated the same
317    X509Util.createKeyManager(
318      x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(), null,
319      KeyStoreFileType.PKCS12.getPropertyValue());
320  }
321
322  @TestTemplate
323  public void testLoadPKCS12KeyStoreAutodetectStoreFileType() throws Exception {
324    // Make sure we can instantiate a key manager from the PKCS12 file on disk
325    X509Util.createKeyManager(
326      x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
327      x509TestContext.getKeyStorePassword(),
328      null /* null StoreFileType means 'autodetect from file extension' */);
329  }
330
331  @TestTemplate
332  public void testLoadPKCS12KeyStoreWithWrongPassword() {
333    assertThrows(KeyManagerException.class, () -> {
334      // Attempting to load with the wrong key password should fail
335      X509Util.createKeyManager(
336        x509TestContext.getKeyStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
337        "wrong password".toCharArray(), KeyStoreFileType.PKCS12.getPropertyValue());
338    });
339  }
340
341  @TestTemplate
342  public void testLoadPKCS12TrustStore() throws Exception {
343    // Make sure we can instantiate a trust manager from the PKCS12 file on disk
344    X509Util.createTrustManager(
345      x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
346      x509TestContext.getTrustStorePassword(), KeyStoreFileType.PKCS12.getPropertyValue(), true,
347      true, true, true);
348  }
349
350  @TestTemplate
351  public void testLoadPKCS12TrustStoreNullPassword() throws Exception {
352    assumeTrue(Arrays.equals(x509TestContext.getTrustStorePassword(), EMPTY_CHAR_ARRAY));
353    // Make sure that empty password and null password are treated the same
354    X509Util.createTrustManager(
355      x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(), null,
356      KeyStoreFileType.PKCS12.getPropertyValue(), false, false, true, true);
357  }
358
359  @TestTemplate
360  public void testLoadPKCS12TrustStoreAutodetectStoreFileType() throws Exception {
361    // Make sure we can instantiate a trust manager from the PKCS12 file on disk
362    X509Util.createTrustManager(
363      x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
364      x509TestContext.getTrustStorePassword(), null, // null StoreFileType means 'autodetect from
365                                                     // file extension'
366      true, true, true, true);
367  }
368
369  @TestTemplate
370  public void testLoadPKCS12TrustStoreWithWrongPassword() {
371    assertThrows(TrustManagerException.class, () -> {
372      // Attempting to load with the wrong key password should fail
373      X509Util.createTrustManager(
374        x509TestContext.getTrustStoreFile(KeyStoreFileType.PKCS12).getAbsolutePath(),
375        "wrong password".toCharArray(), KeyStoreFileType.PKCS12.getPropertyValue(), true, true,
376        true, true);
377    });
378  }
379
380  // ---------------------------------------------------------------------------
381  // Role-scoped configuration resolution (single-EKU certificate support)
382  // ---------------------------------------------------------------------------
383
384  @TestTemplate
385  public void testCreateSSLContextForClientUsesRoleScopedKeystoreWhenSet() throws Exception {
386    // Move the legacy keystore values to the client-scoped keys and clear the legacy keys, so
387    // that a successful context build proves the client-scoped keys were consulted.
388    String location = conf.get(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
389    String password = conf.get(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD);
390    String type = conf.get(X509Util.TLS_CONFIG_KEYSTORE_TYPE);
391    conf.set(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_LOCATION, location);
392    conf.set(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_PASSWORD, password);
393    conf.set(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_TYPE, type);
394    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
395    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD);
396    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_TYPE);
397
398    SslContext sslContext = X509Util.createSslContextForClient(conf);
399    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
400    // Handshake would fail if the key manager weren't wired; smoke-test that engine creation works.
401    assertTrue(
402      sslContext.newEngine(byteBufAllocatorMock).getSSLParameters().getProtocols().length > 0);
403  }
404
405  @TestTemplate
406  public void testCreateSSLContextForServerUsesRoleScopedKeystoreWhenSet() throws Exception {
407    String location = conf.get(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
408    String password = conf.get(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD);
409    String type = conf.get(X509Util.TLS_CONFIG_KEYSTORE_TYPE);
410    conf.set(X509Util.TLS_CONFIG_SERVER_KEYSTORE_LOCATION, location);
411    conf.set(X509Util.TLS_CONFIG_SERVER_KEYSTORE_PASSWORD, password);
412    conf.set(X509Util.TLS_CONFIG_SERVER_KEYSTORE_TYPE, type);
413    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
414    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_PASSWORD);
415    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_TYPE);
416
417    SslContext sslContext = X509Util.createSslContextForServer(conf);
418    ByteBufAllocator byteBufAllocatorMock = mock(ByteBufAllocator.class);
419    assertTrue(
420      sslContext.newEngine(byteBufAllocatorMock).getSSLParameters().getProtocols().length > 0);
421  }
422
423  @TestTemplate
424  public void testCreateSSLContextForServerThrowsWhenNeitherKeystoreSet() {
425    conf.unset(X509Util.TLS_CONFIG_KEYSTORE_LOCATION);
426    conf.unset(X509Util.TLS_CONFIG_SERVER_KEYSTORE_LOCATION);
427    SSLContextException ex =
428      assertThrows(SSLContextException.class, () -> X509Util.createSslContextForServer(conf));
429    // The error should name both keys so the operator knows what to set.
430    assertTrue(ex.getMessage().contains(X509Util.TLS_CONFIG_SERVER_KEYSTORE_LOCATION),
431      "message should mention role-scoped key, got: " + ex.getMessage());
432    assertTrue(ex.getMessage().contains(X509Util.TLS_CONFIG_KEYSTORE_LOCATION),
433      "message should mention legacy key, got: " + ex.getMessage());
434  }
435
436  @TestTemplate
437  public void testValidateClientAuthTrustStoreRejectsNeedWithoutTrustStore() {
438    IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
439      () -> X509Util.validateClientAuthTrustStore(X509Util.ClientAuth.NEED, null, "the.mode.key",
440        "the.truststore.key"));
441    assertTrue(ex.getMessage().contains("the.mode.key"), ex.getMessage());
442    assertTrue(ex.getMessage().contains("the.truststore.key"), ex.getMessage());
443  }
444
445  @TestTemplate
446  public void testValidateClientAuthTrustStoreAllowsNoneWithoutTrustStore() {
447    // NONE never requests a peer certificate, so a missing truststore is not a misconfiguration.
448    X509Util.validateClientAuthTrustStore(X509Util.ClientAuth.NONE, null, "mode.key", "ts.key");
449  }
450
451  // A role-scoped password with no role-scoped location would be applied to the legacy keystore.
452  @TestTemplate
453  public void testCreateSSLContextForClientRejectsRoleKeyWithoutRoleLocation() {
454    // The base conf carries a complete legacy keystore, so only the role-scoped location is
455    // missing.
456    conf.set(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_PASSWORD, "orphan-pw");
457    IllegalArgumentException ex =
458      assertThrows(IllegalArgumentException.class, () -> X509Util.createSslContextForClient(conf));
459    assertTrue(ex.getMessage().contains(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_PASSWORD),
460      ex.getMessage());
461    assertTrue(ex.getMessage().contains(X509Util.TLS_CONFIG_CLIENT_KEYSTORE_LOCATION),
462      ex.getMessage());
463  }
464
465  @TestTemplate
466  public void testResolveStoreRejectsRoleKeyWithoutRoleLocation() {
467    Configuration c = new Configuration(false);
468    c.set("p.role.keystore.password", "pw2");
469    c.set("p.keystore.location", "/old.jks");
470    IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
471      () -> TLSStore.resolve(c, "p.role.", "p.", TLSStore.Keys.RPC_KEYSTORE));
472    assertTrue(ex.getMessage().contains("p.role.keystore.password"), ex.getMessage());
473    assertTrue(ex.getMessage().contains("p.role.keystore.location"), ex.getMessage());
474  }
475
476  /**
477   * A store is a unit: the prefix that supplies the location supplies every other attribute too.
478   * Resolving per key would open the role-scoped file with the legacy password and type.
479   */
480  @TestTemplate
481  public void testResolveStoreNeverMixesPrefixes() throws Exception {
482    Configuration c = new Configuration(false);
483    c.set("p.role.keystore.location", "/new.p12");
484    c.set("p.keystore.location", "/old.jks");
485    c.set("p.keystore.password", "legacy-pw");
486    c.set("p.keystore.type", "JKS");
487
488    TLSStore store = TLSStore.resolve(c, "p.role.", "p.", TLSStore.Keys.RPC_KEYSTORE);
489    assertEquals("/new.p12", store.getLocation());
490    assertNull(store.getPassword(), "password must not be taken from the legacy prefix");
491    assertEquals("", store.getType(), "type must not be taken from the legacy prefix");
492  }
493
494  // Deployments that never adopt the role-scoped prefix keep resolving from the legacy keys.
495  @TestTemplate
496  public void testResolveStoreFallsBackToLegacyPrefix() throws Exception {
497    Configuration c = new Configuration(false);
498    c.set("p.keystore.location", "/old.jks");
499    c.set("p.keystore.password", "pw");
500    c.set("p.keystore.type", "JKS");
501
502    TLSStore store = TLSStore.resolve(c, "p.role.", "p.", TLSStore.Keys.RPC_KEYSTORE);
503    assertEquals("/old.jks", store.getLocation());
504    assertEquals("pw", store.getPassword());
505    assertEquals("JKS", store.getType());
506  }
507}