]> Sergey Matveev's repositories - tofuproxy.git/commitdiff
TLS client certificates
authorSergey Matveev <stargrave@stargrave.org>
Wed, 8 Sep 2021 14:22:03 +0000 (17:22 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Wed, 8 Sep 2021 14:22:03 +0000 (17:22 +0300)
.gitignore
all.do
cmd/tofuproxy/main.go
dane.go
doc/index.texi
doc/usage.texi
fifos/fifos.go
go.mod
go.sum
tls.go

index 1132f689bda9078a6a2ad7938c1fac2f9b3abe0c..1da43c8a8aab5969f4eefd11f3cb7a693b41d98f 100644 (file)
@@ -1,3 +1,4 @@
+/ccerts
 /cert.pem
 /certgen.cmd
 /certs
diff --git a/all.do b/all.do
index fbac54c9765c53362da451f1e31241f056742589..db8bcbf9fe6106b27cef871e13efb3ba8d57c6af 100644 (file)
--- a/all.do
+++ b/all.do
@@ -1,2 +1,2 @@
 redo-ifchange cert.pem tofuproxy.cmd fifos/ensure
-mkdir -p certs
+mkdir -p certs ccerts
index 5335ffb0f911358a647a23d562a97a554f190f74..8a0be00267f4cc17c191ecbea841c65aac0fd3bb 100644 (file)
@@ -33,6 +33,7 @@ func main() {
        prvPath := flag.String("key", "prv.pem", "Path to server PKCS#8 private key")
        bind := flag.String("bind", "[::1]:8080", "Bind address")
        certs := flag.String("certs", "./certs", "Directory with pinned certificates")
+       ccerts := flag.String("ccerts", "./ccerts", "Directory with client certificates")
        dnsSrv := flag.String("dns", "[::1]:53", "DNS server")
        fifosDir := flag.String("fifos", "fifos", "Directory with FIFOs")
        notai := flag.Bool("notai", false, "Do not prepend TAI64N to logs")
@@ -53,6 +54,7 @@ func main() {
        fifos.FIFOs = *fifosDir
        fifos.Init()
        tofuproxy.Certs = *certs
+       tofuproxy.CCerts = *ccerts
        tofuproxy.DNSSrv = *dnsSrv
        tofuproxy.CACert = caCert
        tofuproxy.CAPrv = caPrv
@@ -65,7 +67,7 @@ func main() {
                Handler:      &tofuproxy.Handler{},
                TLSNextProto: tofuproxy.TLSNextProtoS,
        }
-       log.Println("listening:", *bind, "certs:", *certs)
+       log.Println("listening:", *bind, "dns:", *dnsSrv, "certs:", *certs, "ccerts:", *ccerts)
        if err := srv.Serve(ln); err != nil {
                log.Fatalln(err)
        }
diff --git a/dane.go b/dane.go
index cd97220f6d7ff4ed9d7fdd477fca846e0de2b8e3..d3218dda2509b6e9f595513899383d10266402d4 100644 (file)
--- a/dane.go
+++ b/dane.go
@@ -72,6 +72,8 @@ func dane(addr string, cert *x509.Certificate) (bool, bool) {
                }
                var hsh []byte
                switch tlsa.MatchingType {
+               case 0:
+                       hsh = toMatch
                case 1:
                        our := sha256.Sum256(toMatch)
                        hsh = our[:]
index be8e76f0ec7a92aacd6837bb91ea9668d8f97b2c..10c8582d248cc85b118ceafdadca16a6d7fb4acb 100644 (file)
@@ -58,51 +58,74 @@ creating some kind of complex configuration framework.
 
 @itemize
 
-@item Effective responses proxying, without storing them in the memory first.
+@item
+@strong{Effective} responses proxying, without storing them in the memory first.
 
-@item TLS connection between client and @command{tofuproxy} has the
-    proper hostname set in ephemeral on-the-fly generated certificate.
+@item
+TLS connection between client and @command{tofuproxy} has the
+@strong{proper hostname} set in ephemeral on-the-fly generated
+certificate.
 
-@item @code{HEAD} method is forbidden, as Xombrero loves it too much and
-    it does not send User-Agent to differentiate it from others.
+@item
+@code{HEAD} method is forbidden.
 
-@item @code{www.reddit.com} is redirected to @code{old.reddit.com}.
+@item
+@code{www.reddit.com} is redirected to @code{old.reddit.com}.
 
-@item @url{https://habr.com/ru/all/, Хабр}'s resolution reduced images
-    are redirected to their full size variants.
+@item
+@url{https://habr.com/ru/all/, Хабр}'s resolution reduced images are
+redirected to their full size variants.
 
-@item Various spying domains (advertisement, tracking counters) are denied.
+@item
+Various @strong{spying} domains (advertisement, tracking counters) are denied.
 
-@item Web fonts downloads are forbidden.
+@item
+Web @strong{fonts} downloads are forbidden.
 
-@item Permanent HTTP redirects are replaced with HTML page with the link.
+@item
+@strong{Permanent} HTTP @strong{redirects} are replaced with HTML page
+with the link.
 
-@item Temporary HTTP redirects are replaced with HTML too, if it is
-    neither @url{https://newsboat.org/, Newsboat} nor image paths.
+@item
+@strong{Temporary} HTTP @strong{redirects} are replaced with HTML too,
+if it is neither @url{https://newsboat.org/, Newsboat} nor image paths.
 
-@item WebP images, if it is not Xombrero, is transcoded to PNG.
+@item
+@strong{WebP} images, if it is not Xombrero, is transcoded to PNG.
 
-@item JPEG XL and AVIF images are transparently transcoded to PNG too.
+@item
+@strong{JPEG XL} and @strong{AVIF} images are transparently transcoded
+to PNG too.
 
-@item Default Go's checks are applied to all certificates. If they pass,
-    then certificate chain is saved on the disk. Future connections are
-    compared against it, warning you about SPKI change and waiting for
-    your decision either to accept new chain (possibly once per
-    session), or reject it.
+@item
+Default Go's checks are applied to all certificates. If they pass, then
+certificate chain is saved on the disk (@strong{TOFU}). Future
+connections are compared against it, warning you about SPKI change
+(@strong{SPKI pinning}) and waiting for your decision either to accept
+new chain (possibly once per session), or reject it.
 
-@item Even when native Go's checks are failed, you can still make a
-    decision to forcefully trust the domain.
+@item
+Even when native Go's checks are failed, you can still make a decision
+to forcefully trust the domain.
 
-@item HTTP-based unauthorized responses are intercepted and
-    user/password input dialog is shown. It automatically loads initial
-    form values from @file{.netrc}.
+@item
+@strong{HTTP-based authorization} requests are intercepted and
+user/password input dialogue is shown. It automatically loads
+@strong{initial form} values from @file{.netrc}.
 
-@item Optionally DANE-EE check is also made for each domain you visit.
+@item
+TLS @strong{client certificates} supported: separate dialogue window for
+certificate choice.
 
-@item TLS session resumption and keep-alives are also supported.
+@item
+Optional @strong{DANE-EE} check is also made for each domain you visit.
 
-@item And Go itself tries also to act as a
-@url{https://http2.github.io/, HTTP/2} client too.
+@item
+TLS @strong{session resumption} and @strong{keep-alives} are also supported.
+
+@item
+And Go itself tries also to act as a @url{https://http2.github.io/,
+HTTP/2} client too.
 
 @end itemize
 
index 8cfbb0600dcfb3bbe85fa3b63ce0d4b7d195ffae..064d57eb11b262e899a9d6d12561e253991df63c 100644 (file)
@@ -37,7 +37,7 @@ Run @command{tofuproxy} itself. By default it will bind to
 
 @example
 $ ./tofuproxy.cmd
-main.go:316: listening: [::1]:8080 certs: ./certs
+main.go:70: listening: [::1]:8080 dns: [::1]:53 certs: ./certs ccerts: ./ccerts
 @end example
 
 @item Trust your newly generated CA:
@@ -63,4 +63,7 @@ will be shown Tk-dialog through the @command{wish} invocation. GnuTLS'es
 
 @image{dialog,,,Example dialog,.webp}
 
+@item If you want to use TLS client certificates, then place them to
+@file{-ccerts} directory.
+
 @end itemize
index 61f0f63011d98d896d33385410864471a83ef6cf..f07085f9d803d4523d1ffff00b697c2bf9bd072d 100644 (file)
@@ -28,7 +28,7 @@ import (
 
 var (
        NoTAI     bool
-       FIFOs string
+       FIFOs     string
        SinkCert  = make(chan string)
        SinkDANE  = make(chan string)
        SinkErr   = make(chan string)
@@ -37,7 +37,6 @@ var (
        SinkRedir = make(chan string)
        SinkReq   = make(chan string)
        SinkTLS   = make(chan string)
-
 )
 
 func sinker(c chan string, p string) {
diff --git a/go.mod b/go.mod
index b8365611a2b398d8fa59a41b565137bf7cf21f25..779bef0c9bb95c59956f87d437a2d71a3727b30f 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
        github.com/dustin/go-humanize v1.0.0
        github.com/miekg/dns v1.1.29
        go.cypherpunks.ru/tai64n/v2 v2.0.0
-       go.cypherpunks.ru/ucspi v0.0.0-20210904200151-57c1e9924fef
+       go.cypherpunks.ru/ucspi v0.0.0-20210908140534-cfdc20a8225f
 )
 
 require (
diff --git a/go.sum b/go.sum
index 3c12a21abd631b9c0da5837e229791c2c2f0cac4..a3c1ed0226e5200ebb333345d69febf8a8d0a6b7 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
 github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
 go.cypherpunks.ru/tai64n/v2 v2.0.0 h1:AlohA1/zRqInhIGK7CVnn7tC5/vt1TaOAEyBgeu5Ruo=
 go.cypherpunks.ru/tai64n/v2 v2.0.0/go.mod h1:9eeXyzoNO/8grf+bw+T1HXRCRjHVXjUsvnIyaUkjS5I=
-go.cypherpunks.ru/ucspi v0.0.0-20210904200151-57c1e9924fef h1:tGNNuzuVYOYkvWQuu4bYMapbFfCVPv0kPeBHrDxtQ2I=
-go.cypherpunks.ru/ucspi v0.0.0-20210904200151-57c1e9924fef/go.mod h1:x/y6IZN50ZDjQHhsF10B9p3s+YdJi9/kUUGmuuDAXYw=
+go.cypherpunks.ru/ucspi v0.0.0-20210908140534-cfdc20a8225f h1:vxGYZtzCyEXh2+7c0egZODfQQtguwdiCA8WBgfqPnX4=
+go.cypherpunks.ru/ucspi v0.0.0-20210908140534-cfdc20a8225f/go.mod h1:x/y6IZN50ZDjQHhsF10B9p3s+YdJi9/kUUGmuuDAXYw=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/tls.go b/tls.go
index 4c29a6ff6bdf541b806fb83f08d4e3b8283e7aa4..2db3ceb87c20a2a9703c8b3e9efb34f056437171 100644 (file)
--- a/tls.go
+++ b/tls.go
@@ -104,6 +104,7 @@ func (h *HTTPSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
 func dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
        host := strings.TrimSuffix(addr, ":443")
+       ccg := ClientCertificateGetter{host}
        cfg := tls.Config{
                VerifyPeerCertificate: func(
                        rawCerts [][]byte,
@@ -111,8 +112,9 @@ func dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
                ) error {
                        return verifyCert(host, nil, rawCerts, verifiedChains)
                },
-               ClientSessionCache: sessionCache,
-               NextProtos:         []string{"h2", "http/1.1"},
+               ClientSessionCache:   sessionCache,
+               NextProtos:           []string{"h2", "http/1.1"},
+               GetClientCertificate: ccg.get,
        }
        conn, dialErr := tls.Dial(network, addr, &cfg)
        if dialErr != nil {