/*
+tofuproxy -- flexible HTTP/WARC 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
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"
- "io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
+ "sync"
"go.cypherpunks.ru/ucspi"
+ "go.stargrave.org/tofuproxy/caches"
+ "go.stargrave.org/tofuproxy/fifos"
)
+const VerifyDialog = `
+# host err daneStatus certsTheir certsOur
+
+if {[string length $err] > 0} {
+ set tErr [text .tErr]
+ $tErr insert end $err
+ $tErr configure -wrap word -height 5
+ $tErr configure -state disabled
+ grid .tErr
+}
+
+proc certsDecode {raws} {
+ set certs [list]
+ foreach raw [split $raws] {
+ set lines [list]
+ set lineN 0
+ foreach line [split [binary decode hex $raw] "\n"] {
+ lappend lines [format "%03d %s" $lineN $line]
+ incr lineN
+ }
+ lappend certs [join $lines "\n"]
+ }
+ return $certs
+}
+
+set certsTheir [certsDecode $certsTheir]
+set certsOur [certsDecode $certsOur]
+
+tk_setPalette grey
+wm title . $host
+
+proc paginator {i delta t l certs} {
+ incr i $delta
+ if {$i == [llength $certs]} {
+ set i 0
+ } elseif {$i < 0} {
+ set i [expr {[llength $certs] - 1}]
+ }
+ $t configure -state normal
+ $t delete 1.0 end
+ $t insert end [lindex $certs $i]
+ $t configure -state disabled
+ $l configure -text "[expr {$i + 1}] / [llength $certs]"
+ return $i
+}
+
+proc addCertsWindow {name} {
+ global certs$name t$name sb$name page$name l$name
+ set t [text .t$name]
+ set sb [scrollbar .sb$name -command [list $t yview]]
+ $t configure -wrap word -yscrollcommand [list $sb set]
+ $t configure -state disabled
+ grid $t $sb -sticky nsew
+ set t$name $t
+ set sb$name $t
+
+ frame .fControl$name
+ set l$name [label .lPage$name]
+ button .bNext$name -text "Next" -command [subst {
+ set page$name \[paginator \$page$name +1 \$t$name \$l$name \$certs$name]
+ }]
+ button .bPrev$name -text "Prev" -command [subst {
+ set page$name \[paginator \$page$name -1 \$t$name \$l$name \$certs$name]
+ }]
+ grid .fControl$name
+ grid .lPage$name .bNext$name .bPrev$name -in .fControl$name
+ set page$name [paginator -1 +1 $t [set l$name] [set certs$name]]
+}
+
+addCertsWindow Their
+if {[llength $certsOur] > 0} { addCertsWindow Our }
+frame .fButtons
+set lDANE [label .lDANE]
+if {$daneStatus ne ""} {
+ array set daneColour {ok green bad red}
+ $lDANE configure -bg $daneColour($daneStatus)
+ $lDANE configure -text "DANE-EE: $daneStatus"
+}
+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 .fButtons
+grid .lDANE .bAccept .bOnce .bReject -in .fButtons
+grid rowconfigure . 0 -weight 1
+grid columnconfigure . 0 -weight 1
+
+bind . <KeyPress> {switch -exact %K {
+ q {exit 0} ; # reject once
+ a doAccept
+ o doOnce
+ r doReject
+ n {.bNextTheir invoke}
+ p {.bPrevTheir invoke}
+ N {.bNextOur invoke}
+ P {.bPrevOur invoke}
+}}
+`
+
+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("certtool", "--certificate-info", "--inder")
+ 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")
+ return string(out)
}
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
}
var b bytes.Buffer
- b.WriteString(fmt.Sprintf("wm title . \"%s\"\n", host))
-
- if dialErr != nil {
- 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")
+ b.WriteString(fmt.Sprintf("set host \"%s\"\n", host))
+ if dialErr == nil {
+ b.WriteString(fmt.Sprintf("set err \"\"\n"))
+ } else {
+ b.WriteString(fmt.Sprintf("set err \"%s\"\n", dialErr.Error()))
}
-
+ var daneStatus string
if daneExists {
if daneMatched {
- b.WriteString("label .lDANE -bg green -text \"DANE matched\"\n")
+ daneStatus = "ok"
} else {
- b.WriteString("label .lDANE -bg red -text \"DANE not matched!\"\n")
+ daneStatus = "bad"
}
- b.WriteString("grid .lDANE\n")
}
-
- var bCerts bytes.Buffer
- for i, rawCert := range rawCerts {
- bCerts.WriteString(fmt.Sprintf("Their %d:\n", i))
- bCerts.WriteString(certInfo(rawCert))
+ b.WriteString(fmt.Sprintf("set daneStatus \"%s\"\n", daneStatus))
+ hexCerts := make([]string, 0, len(rawCerts))
+ for _, rawCert := range rawCerts {
+ hexCerts = append(hexCerts, hex.EncodeToString([]byte(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 {
- bCerts.Reset()
- for i, cert := range certsOur {
- 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(fmt.Sprintf(
+ "set certsTheir \"%s\"\n", strings.Join(hexCerts, " "),
+ ))
+ hexCerts = make([]string, 0, len(certsOur))
+ for _, cert := range certsOur {
+ hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(cert.Raw))))
}
-
- 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" -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("wish8.7")
- ioutil.WriteFile("/tmp/w.tcl", b.Bytes(), 0666)
+ b.WriteString(fmt.Sprintf(
+ "set certsOur \"%s\"\n", strings.Join(hexCerts, " "),
+ ))
+ b.WriteString(VerifyDialog)
+ cmd := exec.Command(CmdWish)
+ // ioutil.WriteFile("/tmp/verify-dialog.tcl", b.Bytes(), 0666)
cmd.Stdin = &b
err = cmd.Run()
exitError, ok := err.(*exec.ExitError)
if !ok {
- sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash)
+ 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)
}
}
tmp.Close()
os.Rename(tmp.Name(), fn)
- acceptedAdd(host, certTheirHash)
+ caches.AcceptedM.Lock()
+ caches.Accepted[host] = certTheirHash
+ caches.AcceptedM.Unlock()
return nil
}