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.Assert.assertThrows;
021import static org.mockito.Mockito.mock;
022import static org.mockito.Mockito.times;
023import static org.mockito.Mockito.verify;
024import static org.mockito.Mockito.when;
025
026import java.math.BigInteger;
027import java.net.InetAddress;
028import java.net.Socket;
029import java.security.KeyPair;
030import java.security.KeyPairGenerator;
031import java.security.Security;
032import java.security.cert.CertificateException;
033import java.security.cert.X509Certificate;
034import java.util.ArrayList;
035import java.util.Calendar;
036import java.util.Date;
037import java.util.List;
038import java.util.Random;
039import javax.net.ssl.X509ExtendedTrustManager;
040import org.apache.hadoop.hbase.HBaseClassTestRule;
041import org.apache.hadoop.hbase.testclassification.MiscTests;
042import org.apache.hadoop.hbase.testclassification.SmallTests;
043import org.bouncycastle.asn1.x500.X500NameBuilder;
044import org.bouncycastle.asn1.x500.style.BCStyle;
045import org.bouncycastle.asn1.x509.BasicConstraints;
046import org.bouncycastle.asn1.x509.Extension;
047import org.bouncycastle.asn1.x509.GeneralName;
048import org.bouncycastle.asn1.x509.GeneralNames;
049import org.bouncycastle.asn1.x509.KeyUsage;
050import org.bouncycastle.cert.X509v3CertificateBuilder;
051import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
052import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
053import org.bouncycastle.jce.provider.BouncyCastleProvider;
054import org.bouncycastle.operator.ContentSigner;
055import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
056import org.junit.AfterClass;
057import org.junit.Before;
058import org.junit.BeforeClass;
059import org.junit.ClassRule;
060import org.junit.Test;
061import org.junit.experimental.categories.Category;
062import org.mockito.stubbing.Answer;
063
064//
065/**
066 * Test cases taken and adapted from Apache ZooKeeper Project. We can only test calls to
067 * HBaseTrustManager using Sockets (not SSLEngines). This can be fine since the logic is the same.
068 * @see <a href=
069 *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/test/java/org/apache/zookeeper/common/HBaseTrustManagerTest.java">Base
070 *      revision</a>
071 */
072@Category({ MiscTests.class, SmallTests.class })
073public class TestHBaseTrustManager {
074
075  @ClassRule
076  public static final HBaseClassTestRule CLASS_RULE =
077    HBaseClassTestRule.forClass(TestHBaseTrustManager.class);
078
079  private static KeyPair keyPair;
080
081  private X509ExtendedTrustManager mockX509ExtendedTrustManager;
082  private static final String IP_ADDRESS = "127.0.0.1";
083  private static final String HOSTNAME = "localhost";
084
085  private InetAddress mockInetAddressWithoutHostname;
086  private InetAddress mockInetAddressWithHostname;
087  private Socket mockSocketWithoutHostname;
088  private Socket mockSocketWithHostname;
089
090  @BeforeClass
091  public static void createKeyPair() throws Exception {
092    Security.addProvider(new BouncyCastleProvider());
093    KeyPairGenerator keyPairGenerator =
094      KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
095    keyPairGenerator.initialize(4096);
096    keyPair = keyPairGenerator.genKeyPair();
097  }
098
099  @AfterClass
100  public static void removeBouncyCastleProvider() throws Exception {
101    Security.removeProvider("BC");
102  }
103
104  @Before
105  public void setup() throws Exception {
106    mockX509ExtendedTrustManager = mock(X509ExtendedTrustManager.class);
107
108    mockInetAddressWithoutHostname = mock(InetAddress.class);
109    when(mockInetAddressWithoutHostname.getHostAddress())
110      .thenAnswer((Answer<?>) invocationOnMock -> IP_ADDRESS);
111    when(mockInetAddressWithoutHostname.toString())
112      .thenAnswer((Answer<?>) invocationOnMock -> "/" + IP_ADDRESS);
113
114    mockInetAddressWithHostname = mock(InetAddress.class);
115    when(mockInetAddressWithHostname.getHostAddress())
116      .thenAnswer((Answer<?>) invocationOnMock -> IP_ADDRESS);
117    when(mockInetAddressWithHostname.getHostName())
118      .thenAnswer((Answer<?>) invocationOnMock -> HOSTNAME);
119    when(mockInetAddressWithHostname.toString())
120      .thenAnswer((Answer<?>) invocationOnMock -> HOSTNAME + "/" + IP_ADDRESS);
121
122    mockSocketWithoutHostname = mock(Socket.class);
123    when(mockSocketWithoutHostname.getInetAddress())
124      .thenAnswer((Answer<?>) invocationOnMock -> mockInetAddressWithoutHostname);
125
126    mockSocketWithHostname = mock(Socket.class);
127    when(mockSocketWithHostname.getInetAddress())
128      .thenAnswer((Answer<?>) invocationOnMock -> mockInetAddressWithHostname);
129  }
130
131  @SuppressWarnings("JavaUtilDate")
132  private X509Certificate[] createSelfSignedCertificateChain(String ipAddress, String hostname)
133    throws Exception {
134    X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
135    nameBuilder.addRDN(BCStyle.CN, "NOT_LOCALHOST");
136    Date notBefore = new Date();
137    Calendar cal = Calendar.getInstance();
138    cal.setTime(notBefore);
139    cal.add(Calendar.YEAR, 1);
140    Date notAfter = cal.getTime();
141    BigInteger serialNumber = new BigInteger(128, new Random());
142
143    X509v3CertificateBuilder certificateBuilder =
144      new JcaX509v3CertificateBuilder(nameBuilder.build(), serialNumber, notBefore, notAfter,
145        nameBuilder.build(), keyPair.getPublic())
146          .addExtension(Extension.basicConstraints, true, new BasicConstraints(0))
147          .addExtension(Extension.keyUsage, true,
148            new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));
149
150    List<GeneralName> generalNames = new ArrayList<>();
151    if (ipAddress != null) {
152      generalNames.add(new GeneralName(GeneralName.iPAddress, ipAddress));
153    }
154    if (hostname != null) {
155      generalNames.add(new GeneralName(GeneralName.dNSName, hostname));
156    }
157
158    if (!generalNames.isEmpty()) {
159      certificateBuilder.addExtension(Extension.subjectAlternativeName, true,
160        new GeneralNames(generalNames.toArray(new GeneralName[] {})));
161    }
162
163    ContentSigner contentSigner =
164      new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(keyPair.getPrivate());
165
166    return new X509Certificate[] {
167      new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(contentSigner)) };
168  }
169
170  @Test
171  public void testServerHostnameVerificationWithHostnameVerificationDisabled() throws Exception {
172    HBaseTrustManager trustManager =
173      new HBaseTrustManager(mockX509ExtendedTrustManager, false, false);
174
175    X509Certificate[] certificateChain = createSelfSignedCertificateChain(IP_ADDRESS, HOSTNAME);
176    trustManager.checkServerTrusted(certificateChain, null, mockSocketWithHostname);
177
178    verify(mockInetAddressWithHostname, times(0)).getHostAddress();
179    verify(mockInetAddressWithHostname, times(0)).getHostName();
180
181    verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null,
182      mockSocketWithHostname);
183  }
184
185  @SuppressWarnings("checkstyle:linelength")
186  @Test
187  public void testServerTrustedWithHostnameVerificationDisabled() throws Exception {
188    HBaseTrustManager trustManager =
189      new HBaseTrustManager(mockX509ExtendedTrustManager, false, false);
190
191    X509Certificate[] certificateChain = createSelfSignedCertificateChain(IP_ADDRESS, HOSTNAME);
192    trustManager.checkServerTrusted(certificateChain, null, mockSocketWithHostname);
193
194    verify(mockInetAddressWithHostname, times(0)).getHostAddress();
195    verify(mockInetAddressWithHostname, times(0)).getHostName();
196
197    verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null,
198      mockSocketWithHostname);
199  }
200
201  @Test
202  public void testServerTrustedWithHostnameVerificationEnabled() throws Exception {
203    HBaseTrustManager trustManager =
204      new HBaseTrustManager(mockX509ExtendedTrustManager, true, true);
205
206    X509Certificate[] certificateChain = createSelfSignedCertificateChain(null, HOSTNAME);
207    trustManager.checkServerTrusted(certificateChain, null, mockSocketWithHostname);
208
209    verify(mockInetAddressWithHostname, times(1)).getHostAddress();
210    verify(mockInetAddressWithHostname, times(1)).getHostName();
211
212    verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null,
213      mockSocketWithHostname);
214  }
215
216  @Test
217  public void testServerTrustedWithHostnameVerificationEnabledUsingIpAddress() throws Exception {
218    HBaseTrustManager trustManager =
219      new HBaseTrustManager(mockX509ExtendedTrustManager, true, true);
220
221    X509Certificate[] certificateChain = createSelfSignedCertificateChain(IP_ADDRESS, null);
222    trustManager.checkServerTrusted(certificateChain, null, mockSocketWithHostname);
223
224    verify(mockInetAddressWithHostname, times(1)).getHostAddress();
225    verify(mockInetAddressWithHostname, times(0)).getHostName();
226
227    verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null,
228      mockSocketWithHostname);
229  }
230
231  @Test
232  public void testServerTrustedWithHostnameVerificationEnabledNoReverseLookup() throws Exception {
233    HBaseTrustManager trustManager =
234      new HBaseTrustManager(mockX509ExtendedTrustManager, true, false);
235
236    X509Certificate[] certificateChain = createSelfSignedCertificateChain(null, HOSTNAME);
237
238    // We only include hostname in the cert above, but the socket passed in is for an ip address.
239    // This mismatch would succeed if reverse lookup is enabled, but here fails since it's
240    // not enabled.
241    assertThrows(CertificateException.class,
242      () -> trustManager.checkServerTrusted(certificateChain, null, mockSocketWithoutHostname));
243
244    verify(mockInetAddressWithoutHostname, times(1)).getHostAddress();
245    verify(mockInetAddressWithoutHostname, times(0)).getHostName();
246
247    verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null,
248      mockSocketWithoutHostname);
249  }
250
251  @Test
252  public void testServerTrustedWithHostnameVerificationEnabledWithHostnameNoReverseLookup()
253    throws Exception {
254    HBaseTrustManager trustManager =
255      new HBaseTrustManager(mockX509ExtendedTrustManager, true, false);
256
257    X509Certificate[] certificateChain = createSelfSignedCertificateChain(null, HOSTNAME);
258
259    // since the socket inetAddress already has a hostname, we don't need reverse lookup.
260    // so this succeeds
261    trustManager.checkServerTrusted(certificateChain, null, mockSocketWithHostname);
262
263    verify(mockInetAddressWithHostname, times(1)).getHostAddress();
264    verify(mockInetAddressWithHostname, times(1)).getHostName();
265
266    verify(mockX509ExtendedTrustManager, times(1)).checkServerTrusted(certificateChain, null,
267      mockSocketWithHostname);
268  }
269
270  @Test
271  public void testClientTrustedWithHostnameVerificationDisabled() throws Exception {
272    HBaseTrustManager trustManager =
273      new HBaseTrustManager(mockX509ExtendedTrustManager, false, false);
274
275    X509Certificate[] certificateChain = createSelfSignedCertificateChain(null, HOSTNAME);
276    trustManager.checkClientTrusted(certificateChain, null, mockSocketWithHostname);
277
278    verify(mockInetAddressWithHostname, times(0)).getHostAddress();
279    verify(mockInetAddressWithHostname, times(0)).getHostName();
280
281    verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null,
282      mockSocketWithHostname);
283  }
284
285  @Test
286  public void testClientTrustedWithHostnameVerificationEnabled() throws Exception {
287    HBaseTrustManager trustManager =
288      new HBaseTrustManager(mockX509ExtendedTrustManager, true, true);
289
290    X509Certificate[] certificateChain = createSelfSignedCertificateChain(null, HOSTNAME);
291    trustManager.checkClientTrusted(certificateChain, null, mockSocketWithHostname);
292
293    verify(mockInetAddressWithHostname, times(1)).getHostAddress();
294    verify(mockInetAddressWithHostname, times(1)).getHostName();
295
296    verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null,
297      mockSocketWithHostname);
298  }
299
300  @Test
301  public void testClientTrustedWithHostnameVerificationEnabledUsingIpAddress() throws Exception {
302    HBaseTrustManager trustManager =
303      new HBaseTrustManager(mockX509ExtendedTrustManager, true, true);
304
305    X509Certificate[] certificateChain = createSelfSignedCertificateChain(IP_ADDRESS, null);
306    trustManager.checkClientTrusted(certificateChain, null, mockSocketWithHostname);
307
308    verify(mockInetAddressWithHostname, times(1)).getHostAddress();
309    verify(mockInetAddressWithHostname, times(0)).getHostName();
310
311    verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null,
312      mockSocketWithHostname);
313  }
314
315  @Test
316  public void testClientTrustedWithHostnameVerificationEnabledWithoutReverseLookup()
317    throws Exception {
318    HBaseTrustManager trustManager =
319      new HBaseTrustManager(mockX509ExtendedTrustManager, true, false);
320
321    X509Certificate[] certificateChain = createSelfSignedCertificateChain(null, HOSTNAME);
322
323    // We only include hostname in the cert above, but the socket passed in is for an ip address.
324    // This mismatch would succeed if reverse lookup is enabled, but here fails since it's
325    // not enabled.
326    assertThrows(CertificateException.class,
327      () -> trustManager.checkClientTrusted(certificateChain, null, mockSocketWithoutHostname));
328
329    verify(mockInetAddressWithoutHostname, times(1)).getHostAddress();
330    verify(mockInetAddressWithoutHostname, times(0)).getHostName();
331
332    verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null,
333      mockSocketWithoutHostname);
334  }
335
336  @Test
337  public void testClientTrustedWithHostnameVerificationEnabledWithHostnameNoReverseLookup()
338    throws Exception {
339    HBaseTrustManager trustManager =
340      new HBaseTrustManager(mockX509ExtendedTrustManager, true, false);
341
342    X509Certificate[] certificateChain = createSelfSignedCertificateChain(null, HOSTNAME);
343
344    // since the socket inetAddress already has a hostname, we don't need reverse lookup.
345    // so this succeeds
346    trustManager.checkClientTrusted(certificateChain, null, mockSocketWithHostname);
347
348    verify(mockInetAddressWithHostname, times(1)).getHostAddress();
349    verify(mockInetAddressWithHostname, times(1)).getHostName();
350
351    verify(mockX509ExtendedTrustManager, times(1)).checkClientTrusted(certificateChain, null,
352      mockSocketWithHostname);
353  }
354
355}