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.assertThrows;
021
022import java.io.ByteArrayInputStream;
023import java.io.InputStream;
024import java.lang.invoke.MethodHandles;
025import java.security.KeyPair;
026import java.security.Security;
027import java.security.cert.CertificateFactory;
028import java.security.cert.X509Certificate;
029import javax.net.ssl.SSLException;
030import org.apache.hadoop.hbase.testclassification.MiscTests;
031import org.apache.hadoop.hbase.testclassification.SmallTests;
032import org.bouncycastle.asn1.x500.X500Name;
033import org.bouncycastle.asn1.x500.X500NameBuilder;
034import org.bouncycastle.asn1.x500.style.BCStyle;
035import org.bouncycastle.asn1.x509.GeneralName;
036import org.bouncycastle.asn1.x509.GeneralNames;
037import org.bouncycastle.jce.provider.BouncyCastleProvider;
038import org.junit.jupiter.api.BeforeAll;
039import org.junit.jupiter.api.BeforeEach;
040import org.junit.jupiter.api.Tag;
041import org.junit.jupiter.api.Test;
042
043import org.apache.hbase.thirdparty.com.google.common.net.InetAddresses;
044
045/**
046 * Test cases taken and adapted from Apache ZooKeeper Project
047 * @see <a href=
048 *      "https://github.com/apache/zookeeper/blob/5820d10d9dc58c8e12d2e25386fdf92acb360359/zookeeper-server/src/test/java/org/apache/zookeeper/common/ZKHostnameVerifierTest.java">Base
049 *      revision</a>
050 */
051@Tag(MiscTests.TAG)
052@Tag(SmallTests.TAG)
053public class TestHBaseHostnameVerifier {
054  private static CertificateCreator certificateCreator;
055  private HBaseHostnameVerifier impl;
056
057  @BeforeAll
058  public static void setupClass() throws Exception {
059    Security.addProvider(new BouncyCastleProvider());
060    X500NameBuilder caNameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
061    caNameBuilder.addRDN(BCStyle.CN,
062      MethodHandles.lookup().lookupClass().getCanonicalName() + " Root CA");
063    KeyPair keyPair = X509TestHelpers.generateKeyPair(X509KeyType.EC);
064    X509Certificate caCert = X509TestHelpers.newSelfSignedCACert(caNameBuilder.build(), keyPair);
065    certificateCreator = new CertificateCreator(keyPair, caCert);
066  }
067
068  @BeforeEach
069  public void setup() {
070    impl = new HBaseHostnameVerifier();
071  }
072
073  private static class CertificateCreator {
074    private final KeyPair caCertPair;
075    private final X509Certificate caCert;
076
077    public CertificateCreator(KeyPair caCertPair, X509Certificate caCert) {
078      this.caCertPair = caCertPair;
079      this.caCert = caCert;
080    }
081
082    public byte[] newCert(String cn, String... subjectAltName) throws Exception {
083      return X509TestHelpers.newCert(caCert, caCertPair, cn == null ? null : new X500Name(cn),
084        caCertPair.getPublic(), parseSubjectAltNames(subjectAltName)).getEncoded();
085    }
086
087    private GeneralNames parseSubjectAltNames(String... subjectAltName) {
088      if (subjectAltName == null || subjectAltName.length == 0) {
089        return null;
090      }
091      GeneralName[] names = new GeneralName[subjectAltName.length];
092      for (int i = 0; i < subjectAltName.length; i++) {
093        String current = subjectAltName[i];
094        int type;
095        if (InetAddresses.isInetAddress(current)) {
096          type = GeneralName.iPAddress;
097        } else if (current.startsWith("email:")) {
098          type = GeneralName.rfc822Name;
099        } else {
100          type = GeneralName.dNSName;
101        }
102        names[i] = new GeneralName(type, subjectAltName[i]);
103      }
104      return new GeneralNames(names);
105    }
106
107  }
108
109  @Test
110  public void testVerify() throws Exception {
111    final CertificateFactory cf = CertificateFactory.getInstance("X.509");
112    InputStream in;
113    X509Certificate x509;
114
115    in = new ByteArrayInputStream(certificateCreator.newCert("CN=foo.com"));
116    x509 = (X509Certificate) cf.generateCertificate(in);
117
118    impl.verify("foo.com", x509);
119    exceptionPlease(impl, "a.foo.com", x509);
120    exceptionPlease(impl, "bar.com", x509);
121
122    in = new ByteArrayInputStream(certificateCreator.newCert("CN=\u82b1\u5b50.co.jp"));
123    x509 = (X509Certificate) cf.generateCertificate(in);
124    impl.verify("\u82b1\u5b50.co.jp", x509);
125    exceptionPlease(impl, "a.\u82b1\u5b50.co.jp", x509);
126
127    in = new ByteArrayInputStream(certificateCreator.newCert("CN=foo.com", "bar.com"));
128    x509 = (X509Certificate) cf.generateCertificate(in);
129    exceptionPlease(impl, "foo.com", x509);
130    exceptionPlease(impl, "a.foo.com", x509);
131    impl.verify("bar.com", x509);
132    exceptionPlease(impl, "a.bar.com", x509);
133
134    in = new ByteArrayInputStream(
135      certificateCreator.newCert("CN=foo.com", "bar.com", "\u82b1\u5b50.co.jp"));
136    x509 = (X509Certificate) cf.generateCertificate(in);
137    exceptionPlease(impl, "foo.com", x509);
138    exceptionPlease(impl, "a.foo.com", x509);
139    impl.verify("bar.com", x509);
140    exceptionPlease(impl, "a.bar.com", x509);
141
142    /*
143     * Java isn't extracting international subjectAlts properly. (Or OpenSSL isn't storing them
144     * properly).
145     */
146    // DEFAULT.verify("\u82b1\u5b50.co.jp", x509 );
147    // impl.verify("\u82b1\u5b50.co.jp", x509 );
148    exceptionPlease(impl, "a.\u82b1\u5b50.co.jp", x509);
149
150    in = new ByteArrayInputStream(certificateCreator.newCert("CN=", "foo.com"));
151    x509 = (X509Certificate) cf.generateCertificate(in);
152    impl.verify("foo.com", x509);
153    exceptionPlease(impl, "a.foo.com", x509);
154
155    in = new ByteArrayInputStream(
156      certificateCreator.newCert("CN=foo.com, CN=bar.com, CN=\u82b1\u5b50.co.jp"));
157    x509 = (X509Certificate) cf.generateCertificate(in);
158    exceptionPlease(impl, "foo.com", x509);
159    exceptionPlease(impl, "a.foo.com", x509);
160    exceptionPlease(impl, "bar.com", x509);
161    exceptionPlease(impl, "a.bar.com", x509);
162    impl.verify("\u82b1\u5b50.co.jp", x509);
163    exceptionPlease(impl, "a.\u82b1\u5b50.co.jp", x509);
164
165    in = new ByteArrayInputStream(certificateCreator.newCert("CN=*.foo.com"));
166    x509 = (X509Certificate) cf.generateCertificate(in);
167    exceptionPlease(impl, "foo.com", x509);
168    impl.verify("www.foo.com", x509);
169    impl.verify("\u82b1\u5b50.foo.com", x509);
170    exceptionPlease(impl, "a.b.foo.com", x509);
171
172    in = new ByteArrayInputStream(certificateCreator.newCert("CN=*.co.jp"));
173    x509 = (X509Certificate) cf.generateCertificate(in);
174    // Silly test because no-one would ever be able to lookup an IP address
175    // using "*.co.jp".
176    impl.verify("*.co.jp", x509);
177    impl.verify("foo.co.jp", x509);
178    impl.verify("\u82b1\u5b50.co.jp", x509);
179
180    in = new ByteArrayInputStream(
181      certificateCreator.newCert("CN=*.foo.com", "*.bar.com", "*.\u82b1\u5b50.co.jp"));
182    x509 = (X509Certificate) cf.generateCertificate(in);
183    // try the foo.com variations
184    exceptionPlease(impl, "foo.com", x509);
185    exceptionPlease(impl, "www.foo.com", x509);
186    exceptionPlease(impl, "\u82b1\u5b50.foo.com", x509);
187    exceptionPlease(impl, "a.b.foo.com", x509);
188    // try the bar.com variations
189    exceptionPlease(impl, "bar.com", x509);
190    impl.verify("www.bar.com", x509);
191    impl.verify("\u82b1\u5b50.bar.com", x509);
192    exceptionPlease(impl, "a.b.bar.com", x509);
193
194    in = new ByteArrayInputStream(certificateCreator.newCert("CN=repository.infonotary.com"));
195    x509 = (X509Certificate) cf.generateCertificate(in);
196    impl.verify("repository.infonotary.com", x509);
197
198    in = new ByteArrayInputStream(certificateCreator.newCert("CN=*.google.com"));
199    x509 = (X509Certificate) cf.generateCertificate(in);
200    impl.verify("*.google.com", x509);
201
202    in = new ByteArrayInputStream(certificateCreator.newCert("CN=*.google.com"));
203    x509 = (X509Certificate) cf.generateCertificate(in);
204    impl.verify("*.Google.com", x509);
205
206    in = new ByteArrayInputStream(certificateCreator.newCert("CN=dummy-value.com", "1.1.1.1"));
207    x509 = (X509Certificate) cf.generateCertificate(in);
208    impl.verify("1.1.1.1", x509);
209
210    exceptionPlease(impl, "1.1.1.2", x509);
211    exceptionPlease(impl, "2001:0db8:85a3:0000:0000:8a2e:0370:1111", x509);
212    exceptionPlease(impl, "dummy-value.com", x509);
213
214    in = new ByteArrayInputStream(
215      certificateCreator.newCert("CN=dummy-value.com", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
216    x509 = (X509Certificate) cf.generateCertificate(in);
217    impl.verify("2001:0db8:85a3:0000:0000:8a2e:0370:7334", x509);
218
219    exceptionPlease(impl, "1.1.1.2", x509);
220    exceptionPlease(impl, "2001:0db8:85a3:0000:0000:8a2e:0370:1111", x509);
221    exceptionPlease(impl, "dummy-value.com", x509);
222
223    in = new ByteArrayInputStream(
224      certificateCreator.newCert("CN=dummy-value.com", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
225    x509 = (X509Certificate) cf.generateCertificate(in);
226    impl.verify("2001:0db8:85a3:0000:0000:8a2e:0370:7334", x509);
227    impl.verify("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", x509);
228
229    exceptionPlease(impl, "1.1.1.2", x509);
230    exceptionPlease(impl, "2001:0db8:85a3:0000:0000:8a2e:0370:1111", x509);
231    exceptionPlease(impl, "dummy-value.com", x509);
232
233    in = new ByteArrayInputStream(
234      certificateCreator.newCert("CN=www.company.com", "email:email@example.com"));
235    x509 = (X509Certificate) cf.generateCertificate(in);
236    impl.verify("www.company.com", x509);
237  }
238
239  private void exceptionPlease(final HBaseHostnameVerifier hv, final String host,
240    final X509Certificate x509) {
241    assertThrows(SSLException.class, () -> hv.verify(host, x509),
242      "HostnameVerifier shouldn't allow [" + host + "]");
243  }
244}