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