From 539e5df5806bd22c8eaddeb0409e6b8b187eda4c Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 15 Mar 2023 18:09:51 +0300 Subject: [PATCH 1/1] Restricted CAs --- caches/caches.go | 3 ++ doc/index.texi | 4 +++ doc/restricted.texi | 13 +++++++++ fifos/ensure.do | 4 +-- fifos/restricted.go | 67 +++++++++++++++++++++++++++++++++++++++++++++ fifos/start.go | 3 ++ netrc.go | 4 ++- restricted.txt | 21 ++++++++++++++ tls/verify.go | 28 ++++++++++++++++++- warc/uris.go | 4 ++- 10 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 doc/restricted.texi create mode 100644 fifos/restricted.go create mode 100644 restricted.txt diff --git a/caches/caches.go b/caches/caches.go index 39dfd34..8144934 100644 --- a/caches/caches.go +++ b/caches/caches.go @@ -20,4 +20,7 @@ var ( Spies = make([]string, 0) SpiesM sync.RWMutex + + Restricted = make(map[string][]string) + RestrictedM sync.RWMutex ) diff --git a/doc/index.texi b/doc/index.texi index ed3bd99..f5ffde9 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -41,6 +41,9 @@ Even if native Go's checks are failed (for example domain still does not use @code{SubjectAltName} extension), you can still make a decision to forcefully trust the domain. +@item +CAs can have restrictions on what domains they are allowed to be served. + @item Optional @url{https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities, DANE-EE} check. @@ -104,6 +107,7 @@ Web fonts downloads are forbidden. @include spies.texi @include certs.texi @include tlsauth.texi +@include restricted.texi @include httpauth.texi @include warcs.texi @include gemini.texi diff --git a/doc/restricted.texi b/doc/restricted.texi new file mode 100644 index 0000000..241b71e --- /dev/null +++ b/doc/restricted.texi @@ -0,0 +1,13 @@ +@node Restricted +@unnumbered Restricted CAs + +You can restrict what hosts are allowed to be served by the specified +CA. For example you want to limit CA with SPKI's SHA256 hash of +@code{9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124} +to domains only in @code{stargrave.org} tree: + +@example +$ tee fifos/add-restricted < restricted.txt +9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124 stargrave.org +[...] +@end example diff --git a/fifos/ensure.do b/fifos/ensure.do index 1a961be..03d50d9 100644 --- a/fifos/ensure.do +++ b/fifos/ensure.do @@ -1,10 +1,10 @@ for f in cert dane err http-auth non-ok ok redir req tls tls-auth various warc ; do [ -p log-$f ] || mkfifo log-$f done -for f in accepted http-auth rejected spies tls-auth warcs ; do +for f in accepted http-auth rejected restricted spies tls-auth warcs ; do [ -p list-$f ] || mkfifo list-$f [ -p del-$f ] || mkfifo del-$f done -for f in spies tls-auth warcs ; do +for f in restricted spies tls-auth warcs ; do [ -p add-$f ] || mkfifo add-$f done diff --git a/fifos/restricted.go b/fifos/restricted.go new file mode 100644 index 0000000..76c5f0b --- /dev/null +++ b/fifos/restricted.go @@ -0,0 +1,67 @@ +/* +tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU + manager, WARC/geminispace browser +Copyright (C) 2021-2023 Sergey Matveev + +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, version 3 of the License. + +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 . +*/ + +package fifos + +import ( + "fmt" + "log" + "os" + "strings" + + "go.stargrave.org/tofuproxy/caches" +) + +func listRestricted(p string) { + for { + fd, err := os.OpenFile(p, os.O_WRONLY|os.O_APPEND, os.FileMode(0666)) + if err != nil { + log.Fatalln(err) + } + caches.RestrictedM.RLock() + for spki, hosts := range caches.Restricted { + for _, host := range hosts { + if _, err = fmt.Fprintf(fd, "%s\t%s\n", spki, host); err != nil { + break + } + } + } + caches.RestrictedM.RUnlock() + fd.Close() + } +} + +func addRestricted(p string) { + for { + neu := make(map[string][]string) + for _, line := range readLinesFromFIFO(p) { + cols := strings.Fields(line) + if len(cols) != 2 { + continue + } + spki, host := cols[0], cols[1] + log.Printf("%s: adding %s: %s\n", p, spki, host) + neu[spki] = append(neu[spki], host) + } + caches.RestrictedM.Lock() + for spki, hosts := range neu { + caches.Restricted[spki] = append(caches.Restricted[spki], hosts...) + } + caches.RestrictedM.Unlock() + } +} diff --git a/fifos/start.go b/fifos/start.go index 14acead..2dad65a 100644 --- a/fifos/start.go +++ b/fifos/start.go @@ -42,6 +42,7 @@ func Start(fifos string) { go listAccepted(filepath.Join(fifos, "list-accepted")) go listHTTPAuth(filepath.Join(fifos, "list-http-auth")) go listRejected(filepath.Join(fifos, "list-rejected")) + go listRestricted(filepath.Join(fifos, "list-restricted")) go listSpies(filepath.Join(fifos, "list-spies")) go listTLSAuth(filepath.Join(fifos, "list-tls-auth")) go listWARCs(filepath.Join(fifos, "list-warcs")) @@ -59,6 +60,8 @@ func Start(fifos string) { filepath.Join(fifos, "del-rejected"), ) + go addRestricted(filepath.Join(fifos, "add-restricted")) + go addSpy(filepath.Join(fifos, "add-spies")) go del( &caches.SpiesM, func(host string) { diff --git a/netrc.go b/netrc.go index d8877b4..bdc61bb 100644 --- a/netrc.go +++ b/netrc.go @@ -3,6 +3,8 @@ package tofuproxy import ( + "errors" + "io/fs" "log" "os" "path/filepath" @@ -20,7 +22,7 @@ func findInNetrc(host string) (string, string) { } data, err := os.ReadFile(netrcPath) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return "", "" } log.Fatalln(err) diff --git a/restricted.txt b/restricted.txt new file mode 100644 index 0000000..4cbb700 --- /dev/null +++ b/restricted.txt @@ -0,0 +1,21 @@ +02b8220c070728db771d9ac59e54521c4eddad21a783bb26cfdf19c1db0fae37 sber.ru +444b8b16828a1a8a1c8207e50d768bcad5d41f7ba0bac95ffbcee0524e3ad4fc tlscc.ru + +508992ef00482435975b688b0ea730fbe6df6f64453ca363c67a4fa81daa1fff cryptoanarchy.ru +508992ef00482435975b688b0ea730fbe6df6f64453ca363c67a4fa81daa1fff cryptoparty.ru +508992ef00482435975b688b0ea730fbe6df6f64453ca363c67a4fa81daa1fff cypherpunks.ru +508992ef00482435975b688b0ea730fbe6df6f64453ca363c67a4fa81daa1fff govpn.org +508992ef00482435975b688b0ea730fbe6df6f64453ca363c67a4fa81daa1fff nncpgo.org +508992ef00482435975b688b0ea730fbe6df6f64453ca363c67a4fa81daa1fff stargrave.org +9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124 cryptoanarchy.ru +9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124 cryptoparty.ru +9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124 cypherpunks.ru +9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124 govpn.org +9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124 nncpgo.org +9215d9eeddeb403b0ffebb228cfc13104da825117d3640a0dfbfc0c08a012124 stargrave.org +e8345915b3e01d8f9788349619cea61a9ef22ead08b5a6f7f75f17aec000dcde cryptoanarchy.ru +e8345915b3e01d8f9788349619cea61a9ef22ead08b5a6f7f75f17aec000dcde cryptoparty.ru +e8345915b3e01d8f9788349619cea61a9ef22ead08b5a6f7f75f17aec000dcde cypherpunks.ru +e8345915b3e01d8f9788349619cea61a9ef22ead08b5a6f7f75f17aec000dcde govpn.org +e8345915b3e01d8f9788349619cea61a9ef22ead08b5a6f7f75f17aec000dcde nncpgo.org +e8345915b3e01d8f9788349619cea61a9ef22ead08b5a6f7f75f17aec000dcde stargrave.org diff --git a/tls/verify.go b/tls/verify.go index 7bff1c1..b7a317d 100644 --- a/tls/verify.go +++ b/tls/verify.go @@ -24,7 +24,9 @@ import ( "crypto/x509" "encoding/hex" "encoding/pem" + "errors" "fmt" + "io/fs" "log" "os" "os/exec" @@ -206,6 +208,30 @@ func verifyCert( fifos.LogDANE <- fmt.Sprintf("%s\tNAK", host) } } + if len(verifiedChains) > 0 { + caHashes := make(map[string]struct{}) + for _, certs := range verifiedChains { + for _, cert := range certs { + caHashes[spkiHash(cert)] = struct{}{} + } + } + var restrictedHosts []string + caches.RestrictedM.RLock() + for h := range caHashes { + restrictedHosts = append(restrictedHosts, caches.Restricted[h]...) + } + caches.RestrictedM.RUnlock() + if len(restrictedHosts) > 0 { + for _, h := range restrictedHosts { + if host == h || strings.HasSuffix(host, "."+h) { + goto HostIsNotRestricted + } + } + fifos.LogCert <- fmt.Sprintf("Restricted\t%s", host) + return ErrRejected{host} + } + } +HostIsNotRestricted: fn := filepath.Join(Certs, host) certsOur, _, err := ucspi.CertPoolFromFile(fn) if err == nil || dialErr != nil || (daneExists && !daneMatched) { @@ -279,7 +305,7 @@ func verifyCert( return ErrRejected{host} } } else { - if !os.IsNotExist(err) { + if !errors.Is(err, fs.ErrNotExist) { return err } fifos.LogCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash) diff --git a/warc/uris.go b/warc/uris.go index 36ff15f..2c40dcf 100644 --- a/warc/uris.go +++ b/warc/uris.go @@ -20,8 +20,10 @@ package warc import ( "encoding/gob" + "errors" "fmt" "io" + "io/fs" "log" "os" "strconv" @@ -59,7 +61,7 @@ func Add(warcPath string) error { log.Println("loaded marshalled index:", warcPath+IndexExt) return nil } - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } r, err := NewReader(warcPath) -- 2.44.0