Commit c65aaf64 authored by Yawning Angel's avatar Yawning Angel
Browse files

transports/meeklite: Add a lightweight HPKP implementation

HPKP is effectively dead as far as a standard goes, but the idea has
merit in certain use cases, this being one of them.

As a TLS MITM essentially will strip whatever obfuscation that the
transport may provide, the digests of the SubjectPublicKeyInfo fields
of the Tor Browser Azure meek host are now hardcoded.

The behavior can be disabled by passing `disableHPKP=true` on the bridge
line, for cases where comaptibility is prefered over security.
parent bde8b7ff
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -2,6 +2,8 @@ Changes in version 0.0.9 - UNRELEASED:
 - Various meek_lite code cleanups and bug fixes.
 - Bug 29077: uTLS for ClientHello camouflage (meek_lite).
 - More fixes to HTTP Basic auth.
 - (meek_lite) Pin the certificate chain public keys for the default
   Tor Browser Azure bridge (meek_lite).

Changes in version 0.0.8 - 2019-01-20:
 - Bug 24793: Send the correct authorization HTTP header for basic auth.
+88 −0
Original line number Diff line number Diff line
/*
 * Copyright (c) 2019 Yawning Angel <yawning at schwanenlied dot me>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package meeklite

import (
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"

	"golang.org/x/net/idna"
)

var builtinPinDB *hpkpDatabase

type hpkpDatabase struct {
	pins map[string]map[string]bool
}

func (db *hpkpDatabase) HasPins(host string) (string, bool) {
	h, err := normalizeHost(host)
	return h, (db.pins[host] != nil && err == nil)
}

func (db *hpkpDatabase) Validate(host string, chains [][]*x509.Certificate) bool {
	var ok bool
	if host, ok = db.HasPins(host); !ok {
		return false
	}

	pins := db.pins[host]
	for _, chain := range chains {
		for _, cert := range chain {
			derivedPin := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
			derivedPinEncoded := base64.StdEncoding.EncodeToString(derivedPin[:])
			if !pins[derivedPinEncoded] {
				return false
			}
		}
	}

	return true
}

func (db *hpkpDatabase) Add(host string, pins []string) {
	h, err := normalizeHost(host)
	if err != nil {
		panic("failed to add hpkp pin, invalid host: " + err.Error())
	}

	pinMap := make(map[string]bool)
	for _, pin := range pins {
		pinMap[pin] = true
	}

	db.pins[h] = pinMap
}

func normalizeHost(host string) (string, error) {
	return idna.Lookup.ToASCII(host)
}

func init() {
	builtinPinDB = &hpkpDatabase{
		pins: make(map[string]map[string]bool),
	}

	// Generated on 2019-02-04.
	builtinPinDB.Add("ajax.aspnetcdn.com", []string{
		"PPjoAKk+kCVr9VNPXJkyHXEKnIyd5t5NqpPL3zCvJOE=",
		"wBdPad95AU7OgLRs0FU/E6ILO1MSCM84kJ9y0H+TT7s=",
		"Y9mvm0exBk1JoQ57f9Vm28jKo5lFm/woKcVxrYxu80o=",
	})
}
+14 −5
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import (
	gourl "net/url"
	"os"
	"runtime"
	"strings"
	"sync"
	"time"

@@ -53,6 +54,7 @@ const (
	urlArg         = "url"
	frontArg       = "front"
	utlsArg        = "utls"
	disableHPKPArg = "disableHPKP"

	maxChanBacklog = 16

@@ -77,6 +79,7 @@ type meekClientArgs struct {
	front string

	utls        *utls.ClientHelloID
	disableHPKP bool
}

func (ca *meekClientArgs) Network() string {
@@ -114,6 +117,12 @@ func newClientArgs(args *pt.Args) (ca *meekClientArgs, err error) {
		return nil, err
	}

	// Parse the (optional) HPKP disable argument.
	hpkpOpt, _ := args.Get(disableHPKPArg)
	if strings.ToLower(hpkpOpt) == "true" {
		ca.disableHPKP = true
	}

	return ca, nil
}

@@ -358,7 +367,7 @@ func newMeekConn(network, addr string, dialFn base.DialFunc, ca *meekClientArgs)
	case nil:
		rt = &http.Transport{Dial: dialFn}
	default:
		rt = newRoundTripper(dialFn, ca.utls)
		rt = newRoundTripper(dialFn, ca.utls, ca.disableHPKP)
	}

	conn := &meekConn{
+25 −3
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package meeklite

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
@@ -28,6 +29,7 @@ import (
	"strings"
	"sync"

	"gitlab.com/yawning/obfs4.git/common/log"
	"gitlab.com/yawning/obfs4.git/transports/base"
	utls "gitlab.com/yawning/utls.git"
	"golang.org/x/net/http2"
@@ -65,6 +67,7 @@ type roundTripper struct {
	transport     http.RoundTripper

	initConn    net.Conn
	disableHPKP bool
}

func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -128,7 +131,25 @@ func (rt *roundTripper) dialTLS(network, addr string) (net.Conn, error) {
		host = addr
	}

	conn := utls.UClient(rawConn, &utls.Config{ServerName: host}, *rt.clientHelloID)
	var verifyPeerCertificateFn func([][]byte, [][]*x509.Certificate) error
	if !rt.disableHPKP {
		if pinHost, ok := builtinPinDB.HasPins(host); ok {
			if rt.transport == nil {
				log.Debugf("meek_lite - HPKP enabled for host: %v", pinHost)
			}
			verifyPeerCertificateFn = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
				if !builtinPinDB.Validate(pinHost, verifiedChains) {
					log.Errorf("meek_lite - HPKP validation failure, potential MITM for host: %v", pinHost)
					return fmt.Errorf("meek_lite: HPKP validation failure for host: %v", pinHost)
				}
				return nil
			}
		}
	} else if rt.transport == nil {
		log.Warnf("meek_lite - HPKP disabled for host: %v", host)
	}

	conn := utls.UClient(rawConn, &utls.Config{ServerName: host, VerifyPeerCertificate: verifyPeerCertificateFn}, *rt.clientHelloID)
	if err = conn.Handshake(); err != nil {
		conn.Close()
		return nil, err
@@ -170,10 +191,11 @@ func getDialTLSAddr(u *url.URL) string {
	return net.JoinHostPort(u.Host, strconv.Itoa(pInt))
}

func newRoundTripper(dialFn base.DialFunc, clientHelloID *utls.ClientHelloID) http.RoundTripper {
func newRoundTripper(dialFn base.DialFunc, clientHelloID *utls.ClientHelloID, disableHPKP bool) http.RoundTripper {
	return &roundTripper{
		clientHelloID: clientHelloID,
		dialFn:        dialFn,
		disableHPKP:   disableHPKP,
	}
}