]> Sergey Matveev's repositories - tofuproxy.git/blobdiff - verify.go
Ability to remove hosts from the states, refactoring
[tofuproxy.git] / verify.go
index 922fc63a65ffa77f467b3653ff5767eeeeed7ad5..de71f9145f3e332fd75c748b410a06404b22877c 100644 (file)
--- a/verify.go
+++ b/verify.go
@@ -1,4 +1,5 @@
 /*
+tofuproxy -- HTTP proxy with TLS certificates management
 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -14,11 +15,13 @@ 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 main
+package tofuproxy
 
 import (
        "bytes"
+       "crypto/sha256"
        "crypto/x509"
+       "encoding/hex"
        "encoding/pem"
        "fmt"
        "log"
@@ -26,10 +29,51 @@ import (
        "os/exec"
        "path/filepath"
        "strings"
+       "sync"
 
        "go.cypherpunks.ru/ucspi"
+       "go.stargrave.org/tofuproxy/caches"
+       "go.stargrave.org/tofuproxy/fifos"
 )
 
+var (
+       CmdCerttool = "certtool"
+       CmdWish     = "wish8.6"
+
+       Certs   string
+       VerifyM sync.Mutex
+)
+
+func spkiHash(cert *x509.Certificate) string {
+       hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
+       return hex.EncodeToString(hsh[:])
+}
+
+type ErrRejected struct {
+       addr string
+}
+
+func (err ErrRejected) Error() string { return err.addr + " was rejected" }
+
+func certInfo(certRaw []byte) string {
+       cmd := exec.Command(CmdCerttool, "--certificate-info", "--inder")
+       cmd.Stdin = bytes.NewReader(certRaw)
+       out, err := cmd.Output()
+       if err != nil {
+               return err.Error()
+       }
+       lines := make([]string, 0, 128)
+       for i, line := range strings.Split(string(out), "\n") {
+               if strings.Contains(line, "ASCII:") {
+                       continue
+               }
+               lines = append(lines, fmt.Sprintf(
+                       "%03d %s", i, strings.ReplaceAll(line, `"`, `\"`),
+               ))
+       }
+       return strings.Join(lines, "\n")
+}
+
 func verifyCert(
        host string,
        dialErr error,
@@ -47,90 +91,135 @@ func verifyCert(
                }
        }
        certTheirHash := spkiHash(certTheir)
-       acceptedM.RLock()
-       certOurHash := accepted[host]
-       acceptedM.RUnlock()
+       VerifyM.Lock()
+       defer VerifyM.Unlock()
+       caches.AcceptedM.RLock()
+       certOurHash := caches.Accepted[host]
+       caches.AcceptedM.RUnlock()
        if certTheirHash == certOurHash {
                return nil
        }
-       rejectedM.RLock()
-       certOurHash = rejected[host]
-       rejectedM.RUnlock()
+       caches.RejectedM.RLock()
+       certOurHash = caches.Rejected[host]
+       caches.RejectedM.RUnlock()
        if certTheirHash == certOurHash {
                return ErrRejected{host}
        }
        daneExists, daneMatched := dane(host, certTheir)
        if daneExists {
                if daneMatched {
-                       sinkCert <- fmt.Sprintf("DANE\t%s\tmatched", host)
+                       fifos.LogDANE <- fmt.Sprintf("%s\tACK", host)
                } else {
-                       sinkErr <- fmt.Sprintf("DANE\t%s\tnot matched", host)
+                       fifos.LogDANE <- fmt.Sprintf("%s\tNAK", host)
                }
        }
-       fn := filepath.Join(*certs, host)
+       fn := filepath.Join(Certs, host)
        certsOur, _, err := ucspi.CertPoolFromFile(fn)
        if err == nil || dialErr != nil || (daneExists && !daneMatched) {
                if certsOur != nil && certTheirHash == spkiHash(certsOur[0]) {
-                       acceptedAdd(host, certTheirHash)
+                       caches.AcceptedM.Lock()
+                       caches.Accepted[host] = certTheirHash
+                       caches.AcceptedM.Unlock()
                        if bytes.Compare(certsOur[0].Raw, rawCerts[0]) != 0 {
-                               sinkCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash)
+                               fifos.LogCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash)
                                goto CertUpdate
                        }
                        return nil
                }
-               cmd := exec.Command(
-                       "xmessage",
-                       "-buttons",
-                       "Accept:10,Once:11,Reject:12",
-                       "-file", "-",
-               )
                var b bytes.Buffer
-               b.WriteString("Host: " + host)
+               b.WriteString(fmt.Sprintf("wm title . \"%s\"\n", host))
+
                if dialErr != nil {
-                       b.WriteString("\nError: " + dialErr.Error())
+                       b.WriteString(fmt.Sprintf(`set tErr [text .tErr]
+$tErr insert end "%s"
+$tErr configure -wrap word -height 5
+`, dialErr.Error()))
+                       b.WriteString("grid .tErr -columnspan 3\n")
                }
+
                if daneExists {
                        if daneMatched {
-                               b.WriteString("\nDANE matched")
+                               b.WriteString("label .lDANE -bg green -text \"DANE matched\"\n")
                        } else {
-                               b.WriteString("\nDANE no match!")
+                               b.WriteString("label .lDANE -bg red -text \"DANE NOT matched\"\n")
                        }
+                       b.WriteString("grid .lDANE\n")
                }
+
+               var bCerts bytes.Buffer
                for i, rawCert := range rawCerts {
-                       b.WriteString(fmt.Sprintf("\nTheir %d:\n", i))
-                       b.WriteString(certInfo(rawCert))
+                       bCerts.WriteString(fmt.Sprintf("Their %d:\n", i))
+                       bCerts.WriteString(certInfo(rawCert))
                }
+               b.WriteString(fmt.Sprintf(`set tTheir [text .tTheir]
+$tTheir insert end "%s"
+set sbTheir [scrollbar .sbTheir -command [list $tTheir yview]]
+$tTheir configure -wrap word -yscrollcommand [list $sbTheir set]
+`, bCerts.String()))
+               b.WriteString("grid $tTheir $sbTheir -sticky nsew -columnspan 3\n")
+
                if certsOur != nil {
-                       b.WriteString(strings.Repeat("-", 80))
+                       bCerts.Reset()
                        for i, cert := range certsOur {
-                               b.WriteString(fmt.Sprintf("\nOur %d:\n", i))
-                               b.WriteString(certInfo(cert.Raw))
+                               bCerts.WriteString(fmt.Sprintf("Our %d:\n", i))
+                               bCerts.WriteString(certInfo(cert.Raw))
                        }
+                       b.WriteString(fmt.Sprintf(`set tOur [text .tOur]
+$tOur insert end "%s"
+set sbOur [scrollbar .sbOur -command [list $tOur yview]]
+$tOur configure -wrap word -yscrollcommand [list $sbOur set]
+`, bCerts.String()))
+                       b.WriteString("grid $tOur $sbOur -sticky nsew -columnspan 3\n")
                }
+
+               b.WriteString(`
+proc doAccept {} { exit 10 }
+proc doOnce {} { exit 11 }
+proc doReject {} { exit 12 }
+button .bAccept -text "Accept" -bg green -command doAccept
+button .bOnce -text "Once" -bg green -command doOnce
+button .bReject -text "Reject" -bg red -command doReject
+grid .bAccept .bOnce .bReject
+grid rowconfigure . 0 -weight 1
+grid columnconfigure . 0 -weight 1
+`)
+
+               cmd := exec.Command(CmdWish)
+               // ioutil.WriteFile("/tmp/w.tcl", b.Bytes(), 0666)
                cmd.Stdin = &b
-               switch cmd.Run().(*exec.ExitError).ExitCode() {
+               err = cmd.Run()
+               exitError, ok := err.(*exec.ExitError)
+               if !ok {
+                       fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
+                       return ErrRejected{host}
+               }
+               switch exitError.ExitCode() {
                case 10:
-                       sinkCert <- fmt.Sprintf("ADD\t%s\t%s", host, certTheirHash)
+                       fifos.LogCert <- fmt.Sprintf("Accept\t%s\t%s", host, certTheirHash)
                        goto CertUpdate
                case 11:
-                       sinkCert <- fmt.Sprintf("ONCE\t%s\t%s", host, certTheirHash)
-                       acceptedAdd(host, certTheirHash)
+                       fifos.LogCert <- fmt.Sprintf("Once\t%s\t%s", host, certTheirHash)
+                       caches.AcceptedM.Lock()
+                       caches.Accepted[host] = certTheirHash
+                       caches.AcceptedM.Unlock()
                        return nil
                case 12:
-                       rejectedAdd(host, certTheirHash)
+                       caches.RejectedM.Lock()
+                       caches.Rejected[host] = certTheirHash
+                       caches.RejectedM.Unlock()
                        fallthrough
                default:
-                       sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash)
+                       fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
                        return ErrRejected{host}
                }
        } else {
                if !os.IsNotExist(err) {
                        return err
                }
-               sinkCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
+               fifos.LogCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
        }
 CertUpdate:
-       tmp, err := os.CreateTemp(*certs, "")
+       tmp, err := os.CreateTemp(Certs, "")
        if err != nil {
                log.Fatalln(err)
        }
@@ -142,6 +231,8 @@ CertUpdate:
        }
        tmp.Close()
        os.Rename(tmp.Name(), fn)
-       acceptedAdd(host, certTheirHash)
+       caches.AcceptedM.Lock()
+       caches.Accepted[host] = certTheirHash
+       caches.AcceptedM.Unlock()
        return nil
 }