From: Sergey Matveev <stargrave@stargrave.org>
Date: Wed, 15 Mar 2023 15:09:51 +0000 (+0300)
Subject: Restricted CAs
X-Git-Tag: v0.1.0~10
X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=539e5df5806bd22c8eaddeb0409e6b8b187eda4c;p=tofuproxy.git

Restricted CAs
---

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 <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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)