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