Changeset 40c4a42


Ignore:
Timestamp:
Apr 25, 2015 11:06:44 PM (5 years ago)
Author:
zzz <zzz@…>
Branches:
master
Children:
b90816f
Parents:
26f8939
Message:

I2PSSLSocketFactory:

  • Add hostname verification using code from

Apache HttpClient? 4.4.1 (Apache 2.0 license)
and one small class from HttpCore? 4.4.1,
slightly modified to remove additional Apache dependencies
and unneeded code.

  • Includes support for public suffix list;

use basic list with standard TLDs,
and also support loading the big Mozilla list,
but don't bundle the 150KB Mozilla list for now.

  • For Android, use its default verifier, which

should actually work (unlike Oracle)

  • Java 7 not required, although servers requiring SNI will now

fail on Java 6, which does not support SNI
SSLEepGet:

  • Rework recent setSoTimeout code changes, as they broke SNI
  • Add option to save certs even if no errors
  • Add option to disable hostname verification
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • LICENSE.txt

    r26f8939 r40c4a42  
    8181   See licenses/LICENSE-LGPLv2.1.txt
    8282
     83   HostnameVerifier:
     84   From Apache HttpClient 4.4.1 and HttpCore 4.4.1
     85   See licenses/LICENSE-Apache2.0.txt
     86
    8387
    8488Router (router.jar):
     
    8892   See licenses/LICENSE-GPLv2.txt
    8993
    90    UPnP subsystem (CyberLink) 2.1:
     94   UPnP subsystem (CyberLink) 3.0:
    9195   Copyright (C) 2003-2010 Satoshi Konno
    9296   See licenses/LICENSE-UPnP.txt
  • build.xml

    r26f8939 r40c4a42  
    679679            doctitle="I2P Javadocs for Release ${release.number} Build ${i2p.build.number}${build.extra}"
    680680            windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}">
    681             <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.oldcrypto:org.bouncycastle.oldcrypto.*:gnu.crypto.*:gnu.getopt:gnu.gettext:com.nettgryppa.security:net.metanotion:net.metanotion.*" />
     681            <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.oldcrypto:org.bouncycastle.oldcrypto.*:gnu.crypto.*:gnu.getopt:gnu.gettext:com.nettgryppa.security:net.metanotion:net.metanotion.*:org.apache.http.conn.ssl:org.apache.http.conn.util:org.apache.http.util" />
    682682            <group title="Streaming Library" packages="net.i2p.client.streaming:net.i2p.client.streaming.impl" />
    683683            <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:net.i2p.data.router:org.cybergarage:org.cybergarage.*:org.freenetproject:org.xlattice.crypto.filters" />
     
    14071407        <copy file="installer/resources/geoipv6.dat.gz" todir="pkg-temp/geoip/" />
    14081408        <copy file="installer/resources/continents.txt" todir="pkg-temp/geoip/" />
     1409      <!--
     1410        <copy file="installer/resources/public-suffix-list.txt" todir="pkg-temp/geoip/" />
     1411       -->
    14091412    </target>
    14101413
     
    14251428        <copy file="installer/resources/countries.txt" todir="pkg-temp/geoip/" />
    14261429        <copy file="installer/resources/continents.txt" todir="pkg-temp/geoip/" />
     1430      <!--
     1431        <copy file="installer/resources/public-suffix-list.txt" todir="pkg-temp/geoip/" />
     1432       -->
    14271433    </target>
    14281434
  • core/java/src/net/i2p/util/I2PSSLSocketFactory.java

    r26f8939 r40c4a42  
    2222 */
    2323
     24/*
     25 * Contains code adapted from:
     26 * Apache httpcomponents PublicSuffixMatcherLoader.java
     27 *
     28 * ====================================================================
     29 * Licensed to the Apache Software Foundation (ASF) under one
     30 * or more contributor license agreements.  See the NOTICE file
     31 * distributed with this work for additional information
     32 * regarding copyright ownership.  The ASF licenses this file
     33 * to you under the Apache License, Version 2.0 (the
     34 * "License"); you may not use this file except in compliance
     35 * with the License.  You may obtain a copy of the License at
     36 *
     37 *   http://www.apache.org/licenses/LICENSE-2.0
     38 *
     39 * Unless required by applicable law or agreed to in writing,
     40 * software distributed under the License is distributed on an
     41 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     42 * KIND, either express or implied.  See the License for the
     43 * specific language governing permissions and limitations
     44 * under the License.
     45 * ====================================================================
     46 *
     47 * This software consists of voluntary contributions made by many
     48 * individuals on behalf of the Apache Software Foundation.  For more
     49 * information on the Apache Software Foundation, please see
     50 * <http://www.apache.org/>.
     51 *
     52 */
     53import java.io.BufferedReader;
    2454import java.io.File;
     55import java.io.FileInputStream;
    2556import java.io.IOException;
     57import java.io.InputStream;
     58import java.io.InputStreamReader;
     59import java.util.Arrays;
     60import java.util.Collections;
     61import java.util.Locale;
    2662import java.net.InetAddress;
    2763import java.net.Socket;
     
    3571import java.util.Set;
    3672
     73import javax.net.ssl.HostnameVerifier;
     74import javax.net.ssl.HttpsURLConnection;
    3775import javax.net.ssl.SSLContext;
     76import javax.net.ssl.SSLException;
     77import javax.net.ssl.SSLHandshakeException;
    3878import javax.net.ssl.SSLServerSocket;
     79import javax.net.ssl.SSLSession;
    3980import javax.net.ssl.SSLSocket;
    4081import javax.net.ssl.SSLSocketFactory;
     
    4485import net.i2p.crypto.KeyStoreUtil;
    4586
     87import org.apache.http.conn.ssl.DefaultHostnameVerifier;
     88import org.apache.http.conn.util.PublicSuffixList;
     89import org.apache.http.conn.util.PublicSuffixListParser;
     90import org.apache.http.conn.util.PublicSuffixMatcher;
     91
    4692/**
    4793 * Loads trusted ASCII certs from ~/.i2p/certificates/ and $I2P/certificates/.
     94 *
     95 * TODO extend SSLSocketFactory
    4896 *
    4997 * @author zzz
     
    5199 */
    52100public class I2PSSLSocketFactory {
     101
     102    private static final String PROP_DISABLE = "i2p.disableSSLHostnameVerification";
     103    private static final String PROP_GEOIP_DIR = "geoip.dir";
     104    private static final String GEOIP_DIR_DEFAULT = "geoip";
     105    private static final String GEOIP_FILE_DEFAULT = "geoip.txt";
     106    private static final String COUNTRY_FILE_DEFAULT = "countries.txt";
     107    private static final String PUBLIC_SUFFIX_LIST = "public-suffix-list.txt";
     108    private static PublicSuffixMatcher DEFAULT_MATCHER;
     109    private static boolean _matcherLoaded;
     110    // not in countries.txt, but only the public ones, not the private ones
     111    private static final String[] DEFAULT_TLDS = {
     112        "arpa", "asia", "biz", "cat", "com", "coop",
     113        "edu", "gov", "info", "int", "jobs", "mil",
     114        "mobi", "museum", "name", "net", "org", "post",
     115        "pro", "tel", "travel", "xxx"
     116    };
     117    // not in countries.txt or public-suffix-list.txt
     118    private static final String[] ADDITIONAL_TLDS = {
     119        "i2p", "mooo.com", "onion"
     120    };
    53121
    54122    /**
     
    149217    public static final List<String> INCLUDE_CIPHERS = Collections.emptyList();
    150218
     219    /** the "real" factory */
    151220    private final SSLSocketFactory _factory;
     221    private final I2PAppContext _context;
    152222
    153223    /**
     
    158228                               throws GeneralSecurityException {
    159229        _factory = initSSLContext(context, loadSystemCerts, relativeCertPath);
     230        _context = context;
    160231    }
    161232
    162233    /**
    163234     * Returns a socket to the host.
     235     *
     236     * A host argument that's an IP address (instead of a host name)
     237     * is not recommended, as this will probably fail
     238     * SSL certificate validation.
     239     *
     240     * Hostname validation is skipped for localhost addresses, but you still
     241     * must trust the certificate.
     242     *
    164243     */
    165244    public Socket createSocket(String host, int port) throws IOException {
    166245        SSLSocket rv = (SSLSocket) _factory.createSocket(host, port);
    167246        setProtocolsAndCiphers(rv);
     247        verifyHostname(_context, rv, host);
    168248        return rv;
    169249    }
     
    171251    /**
    172252     * Returns a socket to the host.
     253     *
     254     * An InetAddress argument created with an IP address (instead of a host name)
     255     * is not recommended, as this will perform a reverse DNS lookup to
     256     * get the host name for certificate validation, which will probably then fail.
     257     *
     258     * Hostname validation is skipped for localhost addresses, but you still
     259     * must trust the certificate.
     260     *
    173261     * @since 0.9.9
    174262     */
     
    176264        SSLSocket rv = (SSLSocket) _factory.createSocket(host, port);
    177265        setProtocolsAndCiphers(rv);
     266        String name = host.getHostName();
     267        verifyHostname(_context, rv, name);
    178268        return rv;
     269    }
     270
     271    /**
     272     *  Validate the hostname
     273     *
     274     *  ref: https://developer.android.com/training/articles/security-ssl.html
     275     *  ref: http://op-co.de/blog/posts/java_sslsocket_mitm/
     276     *  ref: http://kevinlocke.name/bits/2012/10/03/ssl-certificate-verification-in-dispatch-and-asynchttpclient/
     277     *
     278     *  @throws SSLException on hostname verification failure
     279     *  @since 0.9.20
     280     */
     281    public static void verifyHostname(I2PAppContext ctx, SSLSocket socket, String host) throws SSLException {
     282        Log log = ctx.logManager().getLog(I2PSSLSocketFactory.class);
     283        if (ctx.getBooleanProperty(PROP_DISABLE) ||
     284            host.equals("localhost") ||
     285            host.equals("127.0.0.1") ||
     286            host.equals("::1") ||
     287            host.equals("0:0:0:0:0:0:0::1")) {
     288            if (log.shouldWarn())
     289                log.warn("Skipping hostname validation for " + host);
     290            return;
     291        }
     292        HostnameVerifier hv;
     293        if (SystemVersion.isAndroid()) {
     294            // https://developer.android.com/training/articles/security-ssl.html
     295            hv = HttpsURLConnection.getDefaultHostnameVerifier();
     296        } else {
     297            // haha the above may work for Android but it doesn't in Oracle
     298            //
     299            // quote http://kevinlocke.name/bits/2012/10/03/ssl-certificate-verification-in-dispatch-and-asynchttpclient/ :
     300            // Unlike SSLContext, using the Java default (HttpsURLConnection.getDefaultHostnameVerifier)
     301            // is not a viable option because the default HostnameVerifier expects to only be called
     302            // in the case that there is a mismatch (and therefore always returns false) while some
     303            // of the AsyncHttpClient providers (e.g. Netty, the default) call it on all connections.
     304            // in the case that there is a mismatch (and therefore always returns false) while some
     305            // To make matters worse, the check is not trivial (consider SAN and wildcard matching)
     306            // and is implemented in sun.security.util.HostnameChecker (a Sun internal proprietary API).
     307            // This leaves the developer in the position of either depending on an internal API or
     308            // finding/copying/creating another implementation of this functionality.
     309            //
     310            hv = new DefaultHostnameVerifier(getDefaultMatcher(ctx));
     311        }
     312        SSLSession sess = socket.getSession();
     313        // Verify that the certicate hostname is for mail.google.com
     314        // This is due to lack of SNI support in the current SSLSocket.
     315        if (!hv.verify(host, sess)) {
     316            throw new SSLHandshakeException("SSL hostname verify failed, Expected " + host +
     317                                            // throws SSLPeerUnverifiedException
     318                                            //", found " + sess.getPeerPrincipal() +
     319                                            // returns null
     320                                            //", found " + sess.getPeerHost() +
     321                                            // enable logging for DefaultHostnameVerifier to find out the CN and SANs
     322                                            " - set " + PROP_DISABLE +
     323                                            "=true to disable verification (dangerous!)");
     324        }
     325        // At this point SSLSocket performed certificate verificaiton and
     326        // we have performed hostname verification, so it is safe to proceed.
     327    }
     328
     329    /**
     330     *  From Apache PublicSuffixMatcherLoader.getDefault()
     331     *
     332     *  https://publicsuffix.org/list/effective_tld_names.dat
     333     *  What does this get us?
     334     *  Deciding whether to issue or accept an SSL wildcard certificate for *.public.suffix.
     335     *
     336     *  @return null on failure
     337     *  @since 0.9.20
     338     */
     339    private static PublicSuffixMatcher getDefaultMatcher(I2PAppContext ctx) {
     340        synchronized (I2PSSLSocketFactory.class) {
     341            if (!_matcherLoaded) {
     342                String geoDir = ctx.getProperty(PROP_GEOIP_DIR, GEOIP_DIR_DEFAULT);
     343                File geoFile = new File(geoDir);
     344                if (!geoFile.isAbsolute())
     345                    geoFile = new File(ctx.getBaseDir(), geoDir);
     346                geoFile = new File(geoFile, PUBLIC_SUFFIX_LIST);
     347                Log log = ctx.logManager().getLog(I2PSSLSocketFactory.class);
     348                if (geoFile.exists()) {
     349                    try {
     350                        // we can't use PublicSuffixMatcherLoader.load() here because we
     351                        // want to add some of our own and a PublicSuffixMatcher's
     352                        // underlying PublicSuffixList is immutable and inaccessible
     353                        long begin = System.currentTimeMillis();
     354                        InputStream in = null;
     355                        PublicSuffixList list = new PublicSuffixList(Arrays.asList(ADDITIONAL_TLDS),
     356                                                                     Collections.<String>emptyList());
     357                        try {
     358                            in = new FileInputStream(geoFile);
     359                            PublicSuffixList list2 = new PublicSuffixListParser().parse(
     360                                 new InputStreamReader(in, "UTF-8"));
     361                            list = merge(list, list2);
     362                        } finally {
     363                            try { if (in != null) in.close(); } catch (IOException ioe) {}
     364                        }
     365                        DEFAULT_MATCHER = new PublicSuffixMatcher(list.getRules(), list.getExceptions());
     366                        if (log.shouldWarn())
     367                            log.warn("Loaded " + geoFile + " in " + (System.currentTimeMillis() - begin) +
     368                                     " ms and created list with " + list.getRules().size() + " entries and " +
     369                                     list.getExceptions().size() + " exceptions");
     370                    } catch (IOException ex) {
     371                         log.error("Failure loading public suffix list from " + geoFile, ex);
     372                         // DEFAULT_MATCHER remains null
     373                    }
     374                } else {
     375                    List<String> list = new ArrayList<String>(320);
     376                    addCountries(ctx, list);
     377                    list.addAll(Arrays.asList(DEFAULT_TLDS));
     378                    list.addAll(Arrays.asList(ADDITIONAL_TLDS));
     379                    DEFAULT_MATCHER = new PublicSuffixMatcher(list, null);
     380                    if (log.shouldWarn())
     381                        log.warn("No public suffix list found at " + geoFile +
     382                                 " - created default with " + list.size() + " entries");
     383                }
     384            }
     385            _matcherLoaded = true;
     386        }
     387        return DEFAULT_MATCHER;
     388    }
     389
     390   /**
     391    *  Merge two PublicSuffixLists
     392    *  Have to do this because they are unmodifiable
     393    *
     394    *  @since 0.9.20
     395    */
     396    private static PublicSuffixList merge(PublicSuffixList a, PublicSuffixList b) {
     397        List<String> ar = a.getRules();
     398        List<String> ae = a.getExceptions();
     399        List<String> br = b.getRules();
     400        List<String> be = b.getExceptions();
     401        List<String> cr = new ArrayList<String>(ar.size() + br.size());
     402        List<String> ce = new ArrayList<String>(ae.size() + be.size());
     403        cr.addAll(ar);
     404        cr.addAll(br);
     405        ce.addAll(ae);
     406        ce.addAll(be);
     407        return new PublicSuffixList(cr, ce);
     408    }
     409
     410   /**
     411    *  Read in the country file and add all TLDs to the list.
     412    *  It would almost be easier just to add all possible 26*26 two-letter codes.
     413    *
     414    *  @param tlds out parameter
     415    *  @since 0.9.20 adapted from GeoIP.loadCountryFile()
     416    */
     417    private static void addCountries(I2PAppContext ctx, List<String> tlds) {
     418        Log log = ctx.logManager().getLog(I2PSSLSocketFactory.class);
     419        String geoDir = ctx.getProperty(PROP_GEOIP_DIR, GEOIP_DIR_DEFAULT);
     420        File geoFile = new File(geoDir);
     421        if (!geoFile.isAbsolute())
     422            geoFile = new File(ctx.getBaseDir(), geoDir);
     423        geoFile = new File(geoFile, COUNTRY_FILE_DEFAULT);
     424        if (!geoFile.exists()) {
     425            if (log.shouldWarn())
     426                log.warn("Country file not found: " + geoFile.getAbsolutePath());
     427            return;
     428        }
     429        BufferedReader br = null;
     430        try {
     431            br = new BufferedReader(new InputStreamReader(
     432                    new FileInputStream(geoFile), "UTF-8"));
     433            String line = null;
     434            int i = 0;
     435            while ( (line = br.readLine()) != null) {
     436                try {
     437                    if (line.charAt(0) == '#')
     438                        continue;
     439                    String[] s = line.split(",");
     440                    String lc = s[0].toLowerCase(Locale.US);
     441                    tlds.add(lc);
     442                    i++;
     443                } catch (IndexOutOfBoundsException ioobe) {}
     444            }
     445            if (log.shouldInfo())
     446                log.info("Loaded " + i + " TLDs from " + geoFile.getAbsolutePath());
     447        } catch (IOException ioe) {
     448            log.error("Error reading the Country File", ioe);
     449        } finally {
     450            if (br != null) try { br.close(); } catch (IOException ioe) {}
     451        }
    179452    }
    180453
  • core/java/src/net/i2p/util/SSLEepGet.java

    r26f8939 r40c4a42  
    5656import javax.net.ssl.SSLContext;
    5757import javax.net.ssl.SSLEngine;
    58 import javax.net.ssl.SSLHandshakeException;
     58import javax.net.ssl.SSLException;
    5959import javax.net.ssl.SSLParameters;
    6060import javax.net.ssl.SSLSocket;
     
    8484public class SSLEepGet extends EepGet {
    8585    /** if true, save cert chain on cert error */
    86     private boolean _saveCerts;
     86    private int _saveCerts;
     87    /** if true, don't do hostname verification */
     88    private boolean _bypassVerification;
    8789    /** true if called from main(), used for logging */
    8890    private boolean _commandLine;
     
    155157     */
    156158    public static void main(String args[]) {
    157         boolean saveCerts = false;
     159        int saveCerts = 0;
     160        boolean noVerify = false;
    158161        boolean error = false;
    159         Getopt g = new Getopt("ssleepget", args, "s");
     162        Getopt g = new Getopt("ssleepget", args, "sz");
    160163        try {
    161164            int c;
     
    163166              switch (c) {
    164167                case 's':
    165                     saveCerts = true;
     168                    saveCerts++;
     169                    break;
     170
     171                case 'z':
     172                    noVerify = true;
    166173                    break;
    167174
     
    195202
    196203        SSLEepGet get = new SSLEepGet(I2PAppContext.getGlobalContext(), out, url);
    197         if (saveCerts)
    198             get._saveCerts = true;
     204        if (saveCerts > 0)
     205            get._saveCerts = saveCerts;
     206        if (noVerify)
     207            get._bypassVerification = true;
    199208        get._commandLine = true;
    200209        get.addStatusListener(get.new CLIStatusListener(1024, 40));
     
    204213   
    205214    private static void usage() {
    206         System.err.println("Usage: SSLEepGet https://url\n" +
    207                            "To save unknown certs, use: SSLEepGet -s https://url");
     215        System.err.println("Usage: SSLEepGet [-sz] https://url\n" +
     216                           "  -s save unknown certs\n" +
     217                           "  -s -s save all certs\n" +
     218                           "  -z bypass hostname verification");
    208219    }
    209220
     
    353364            X509Certificate cert = chain[k];
    354365            String name = host + '-' + (k + 1) + ".crt";
    355             System.out.println("NOTE: Saving untrusted X509 certificate as " + name);
     366            System.out.println("NOTE: Saving X509 certificate as " + name);
    356367            System.out.println("      Issuer:     " + cert.getIssuerX500Principal());
    357368            System.out.println("      Valid From: " + cert.getNotBefore());
     
    365376        }
    366377        System.out.println("NOTE: To trust them, copy the certificate file(s) to the certificates directory and rerun without the -s option");
    367         System.out.println("NOTE: EepGet failed, certificate error follows:");
    368378    }
    369379
     
    555565                if (port == -1)
    556566                    port = 443;
     567                // Warning, createSocket() followed by connect(InetSocketAddress)
     568                // disables SNI, at least on Java 7.
     569                // So we must do createSocket(host, port) and then setSoTimeout;
     570                // we can't crate a disconnected socket and then call setSoTimeout, sadly.
    557571                if (_sslContext != null)
    558                     _proxy = _sslContext.getSocketFactory().createSocket();
     572                    _proxy = _sslContext.getSocketFactory().createSocket(host, port);
    559573                else
    560                     _proxy = SSLSocketFactory.getDefault().createSocket();
     574                    _proxy = SSLSocketFactory.getDefault().createSocket(host, port);
    561575                if (_fetchHeaderTimeout > 0) {
    562576                    _proxy.setSoTimeout(_fetchHeaderTimeout);
    563                     _proxy.connect(new InetSocketAddress(host, port), _fetchHeaderTimeout);
    564                 } else {
    565                     _proxy.connect(new InetSocketAddress(host, port));
    566577                }
    567578                SSLSocket socket = (SSLSocket) _proxy;
    568579                I2PSSLSocketFactory.setProtocolsAndCiphers(socket);
     580                if (!_bypassVerification) {
     581                    try {
     582                        I2PSSLSocketFactory.verifyHostname(_context, socket, host);
     583                    } catch (SSLException ssle) {
     584                        if (_saveCerts > 0 && _stm != null)
     585                            saveCerts(host, _stm);
     586                        throw ssle;
     587                    }
     588                }
    569589            } else {
    570590                throw new MalformedURLException("Only https supported: " + _actualURL);
     
    582602            _proxyOut.write(DataHelper.getUTF8(req));
    583603            _proxyOut.flush();
    584         } catch (SSLHandshakeException sslhe) {
     604            if (_saveCerts > 1 && _stm != null)
     605                saveCerts(host, _stm);
     606        } catch (SSLException sslhe) {
    585607            // this maybe would be better done in the catch in super.fetch(), but
    586608            // then we'd have to copy it all over here.
    587609            _log.error("SSL negotiation error with " + host + ':' + port +
    588610                       " - self-signed certificate or untrusted certificate authority?", sslhe);
    589             if (_saveCerts && _stm != null)
     611            if (_saveCerts > 0 && _stm != null)
    590612                saveCerts(host, _stm);
    591613            else if (_commandLine) {
Note: See TracChangeset for help on using the changeset viewer.