2 tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU
3 manager, WARC/geminispace browser
4 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, version 3 of the License.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
35 "go.cypherpunks.ru/ucspi"
36 "go.stargrave.org/tofuproxy/caches"
37 "go.stargrave.org/tofuproxy/fifos"
40 const VerifyDialog = `
41 # host err daneStatus certsTheir certsOur
43 if {[string length $err] > 0} {
46 $tErr configure -wrap word -height 5
47 $tErr configure -state disabled
51 proc certsDecode {raws} {
53 foreach raw [split $raws] {
56 foreach line [split [binary decode hex $raw] "\n"] {
57 lappend lines [format "%03d %s" $lineN $line]
60 lappend certs [join $lines "\n"]
65 set certsTheir [certsDecode $certsTheir]
66 set certsOur [certsDecode $certsOur]
71 proc paginator {i delta t l certs} {
73 if {$i == [llength $certs]} {
76 set i [expr {[llength $certs] - 1}]
78 $t configure -state normal
80 $t insert end [lindex $certs $i]
81 $t configure -state disabled
82 $l configure -text "[expr {$i + 1}] / [llength $certs]"
86 proc addCertsWindow {name} {
87 global certs$name t$name sb$name page$name l$name
89 set sb [scrollbar .sb$name -command [list $t yview]]
90 $t configure -wrap word -yscrollcommand [list $sb set]
91 $t configure -state disabled
92 grid $t $sb -sticky nsew
97 set l$name [label .lPage$name]
98 button .bNext$name -text "Next" -command [subst {
99 set page$name \[paginator \$page$name +1 \$t$name \$l$name \$certs$name]
101 button .bPrev$name -text "Prev" -command [subst {
102 set page$name \[paginator \$page$name -1 \$t$name \$l$name \$certs$name]
105 grid .lPage$name .bNext$name .bPrev$name -in .fControl$name
106 set page$name [paginator -1 +1 $t [set l$name] [set certs$name]]
110 if {[llength $certsOur] > 0} { addCertsWindow Our }
112 set lDANE [label .lDANE]
113 if {$daneStatus ne ""} {
114 array set daneColour {ok green bad red}
115 $lDANE configure -bg $daneColour($daneStatus)
116 $lDANE configure -text "DANE-EE: $daneStatus"
118 proc doAccept {} { exit 10 }
119 proc doOnce {} { exit 11 }
120 proc doReject {} { exit 12 }
121 button .bAccept -text "Accept" -bg green -command doAccept
122 button .bOnce -text "Once" -bg green -command doOnce
123 button .bReject -text "Reject" -bg red -command doReject
125 grid .lDANE .bAccept .bOnce .bReject -in .fButtons
126 grid rowconfigure . 0 -weight 1
127 grid columnconfigure . 0 -weight 1
129 bind . <KeyPress> {switch -exact %K {
130 q {exit 0} ; # reject once
134 n {.bNextTheir invoke}
135 p {.bPrevTheir invoke}
142 CmdCerttool = "certtool"
149 func spkiHash(cert *x509.Certificate) string {
150 hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
151 return hex.EncodeToString(hsh[:])
154 type ErrRejected struct {
158 func (err ErrRejected) Error() string { return err.addr + " was rejected" }
160 func certInfo(certRaw []byte) string {
161 cmd := exec.Command(CmdCerttool, "--certificate-info", "--inder")
162 cmd.Stdin = bytes.NewReader(certRaw)
163 out, err := cmd.Output()
174 verifiedChains [][]*x509.Certificate,
176 var certTheir *x509.Certificate
178 if len(verifiedChains) > 0 {
179 certTheir = verifiedChains[0][0]
181 certTheir, err = x509.ParseCertificate(rawCerts[0])
186 certTheirHash := spkiHash(certTheir)
188 defer VerifyM.Unlock()
189 caches.AcceptedM.RLock()
190 certOurHash := caches.Accepted[host]
191 caches.AcceptedM.RUnlock()
192 if certTheirHash == certOurHash {
195 caches.RejectedM.RLock()
196 certOurHash = caches.Rejected[host]
197 caches.RejectedM.RUnlock()
198 if certTheirHash == certOurHash {
199 return ErrRejected{host}
201 daneExists, daneMatched := dane(host, certTheir)
204 fifos.LogDANE <- fmt.Sprintf("%s\tACK", host)
206 fifos.LogDANE <- fmt.Sprintf("%s\tNAK", host)
209 fn := filepath.Join(Certs, host)
210 certsOur, _, err := ucspi.CertPoolFromFile(fn)
211 if err == nil || dialErr != nil || (daneExists && !daneMatched) {
212 if certsOur != nil && certTheirHash == spkiHash(certsOur[0]) {
213 caches.AcceptedM.Lock()
214 caches.Accepted[host] = certTheirHash
215 caches.AcceptedM.Unlock()
216 if bytes.Compare(certsOur[0].Raw, rawCerts[0]) != 0 {
217 fifos.LogCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash)
223 b.WriteString(fmt.Sprintf("set host \"%s\"\n", host))
225 b.WriteString(fmt.Sprintf("set err \"\"\n"))
227 b.WriteString(fmt.Sprintf("set err \"%s\"\n", dialErr.Error()))
229 var daneStatus string
237 b.WriteString(fmt.Sprintf("set daneStatus \"%s\"\n", daneStatus))
238 hexCerts := make([]string, 0, len(rawCerts))
239 for _, rawCert := range rawCerts {
240 hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(rawCert))))
242 b.WriteString(fmt.Sprintf(
243 "set certsTheir \"%s\"\n", strings.Join(hexCerts, " "),
245 hexCerts = make([]string, 0, len(certsOur))
246 for _, cert := range certsOur {
247 hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(cert.Raw))))
249 b.WriteString(fmt.Sprintf(
250 "set certsOur \"%s\"\n", strings.Join(hexCerts, " "),
252 b.WriteString(VerifyDialog)
253 cmd := exec.Command(CmdWish)
254 // ioutil.WriteFile("/tmp/verify-dialog.tcl", b.Bytes(), 0666)
257 exitError, ok := err.(*exec.ExitError)
259 fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
260 return ErrRejected{host}
262 switch exitError.ExitCode() {
264 fifos.LogCert <- fmt.Sprintf("Accept\t%s\t%s", host, certTheirHash)
267 fifos.LogCert <- fmt.Sprintf("Once\t%s\t%s", host, certTheirHash)
268 caches.AcceptedM.Lock()
269 caches.Accepted[host] = certTheirHash
270 caches.AcceptedM.Unlock()
273 caches.RejectedM.Lock()
274 caches.Rejected[host] = certTheirHash
275 caches.RejectedM.Unlock()
278 fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
279 return ErrRejected{host}
282 if !os.IsNotExist(err) {
285 fifos.LogCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
288 tmp, err := os.CreateTemp(Certs, "")
292 for _, rawCert := range rawCerts {
293 err = pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: rawCert})
299 os.Rename(tmp.Name(), fn)
300 caches.AcceptedM.Lock()
301 caches.Accepted[host] = certTheirHash
302 caches.AcceptedM.Unlock()