]> Sergey Matveev's repositories - tofuproxy.git/blob - tls/verify.go
Unify copyright comment format
[tofuproxy.git] / tls / verify.go
1 // tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU
2 //              manager, WARC/geminispace browser
3 // Copyright (C) 2021-2024 Sergey Matveev <stargrave@stargrave.org>
4 //
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, version 3 of the License.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 package tofuproxy
18
19 import (
20         "bytes"
21         "crypto/sha256"
22         "crypto/x509"
23         "encoding/hex"
24         "encoding/pem"
25         "errors"
26         "fmt"
27         "io/fs"
28         "log"
29         "os"
30         "os/exec"
31         "path/filepath"
32         "strings"
33         "sync"
34
35         "go.cypherpunks.ru/ucspi"
36         "go.stargrave.org/tofuproxy/caches"
37         "go.stargrave.org/tofuproxy/fifos"
38 )
39
40 const VerifyDialog = `
41 # host err daneStatus certsTheir certsOur
42
43 if {[string length $err] > 0} {
44     set tErr [text .tErr]
45     $tErr insert end $err
46     $tErr configure -wrap word -height 5
47     $tErr configure -state disabled
48     grid .tErr
49 }
50
51 proc certsDecode {raws} {
52     set certs [list]
53     foreach raw [split $raws] {
54         set lines [list]
55         set lineN 0
56         foreach line [split [binary decode hex $raw] "\n"] {
57             lappend lines [format "%03d %s" $lineN $line]
58             incr lineN
59         }
60         lappend certs [join $lines "\n"]
61     }
62     return $certs
63 }
64
65 set certsTheir [certsDecode $certsTheir]
66 set certsOur [certsDecode $certsOur]
67
68 tk_setPalette grey
69 wm title . $host
70
71 proc paginator {i delta t l certs} {
72     incr i $delta
73     if {$i == [llength $certs]} {
74         set i 0
75     } elseif {$i < 0} {
76         set i [expr {[llength $certs] - 1}]
77     }
78     $t configure -state normal
79     $t delete 1.0 end
80     $t insert end [lindex $certs $i]
81     $t configure -state disabled
82     $l configure -text "[expr {$i + 1}] / [llength $certs]"
83     return $i
84 }
85
86 proc addCertsWindow {name} {
87     global certs$name t$name sb$name page$name l$name
88     set t [text .t$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
93     set t$name $t
94     set sb$name $t
95
96     frame .fControl$name
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]
100     }]
101     button .bPrev$name -text "Prev" -command [subst {
102         set page$name \[paginator \$page$name -1 \$t$name \$l$name \$certs$name]
103     }]
104     grid .fControl$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]]
107 }
108
109 addCertsWindow Their
110 if {[llength $certsOur] > 0} { addCertsWindow Our }
111 frame .fButtons
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"
117 }
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
124 grid .fButtons
125 grid .lDANE .bAccept .bOnce .bReject -in .fButtons
126 grid rowconfigure . 0 -weight 1
127 grid columnconfigure . 0 -weight 1
128
129 bind . <KeyPress> {switch -exact %K {
130     q {exit 0} ; # reject once
131     a doAccept
132     o doOnce
133     r doReject
134     n {.bNextTheir invoke}
135     p {.bPrevTheir invoke}
136     N {.bNextOur invoke}
137     P {.bPrevOur invoke}
138 }}
139 `
140
141 var (
142         CmdCerttool = "certtool"
143         CmdWish     = "wish8.6"
144
145         Certs   string
146         VerifyM sync.Mutex
147 )
148
149 func spkiHash(cert *x509.Certificate) string {
150         hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
151         return hex.EncodeToString(hsh[:])
152 }
153
154 type ErrRejected struct {
155         addr string
156 }
157
158 func (err ErrRejected) Error() string { return err.addr + " was rejected" }
159
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()
164         if err != nil {
165                 return err.Error()
166         }
167         return string(out)
168 }
169
170 func verifyCert(
171         host string,
172         dialErr error,
173         rawCerts [][]byte,
174         verifiedChains [][]*x509.Certificate,
175 ) error {
176         var certTheir *x509.Certificate
177         var err error
178         if len(verifiedChains) > 0 {
179                 certTheir = verifiedChains[0][0]
180         } else {
181                 certTheir, err = x509.ParseCertificate(rawCerts[0])
182                 if err != nil {
183                         return err
184                 }
185         }
186         certTheirHash := spkiHash(certTheir)
187         VerifyM.Lock()
188         defer VerifyM.Unlock()
189         caches.AcceptedM.RLock()
190         certOurHash := caches.Accepted[host]
191         caches.AcceptedM.RUnlock()
192         if certTheirHash == certOurHash {
193                 return nil
194         }
195         caches.RejectedM.RLock()
196         certOurHash = caches.Rejected[host]
197         caches.RejectedM.RUnlock()
198         if certTheirHash == certOurHash {
199                 return ErrRejected{host}
200         }
201         daneExists, daneMatched := DANE(host, certTheir)
202         if daneExists {
203                 if daneMatched {
204                         fifos.LogDANE <- fmt.Sprintf("%s\tACK", host)
205                 } else {
206                         fifos.LogDANE <- fmt.Sprintf("%s\tNAK", host)
207                 }
208         }
209         if len(verifiedChains) > 0 {
210                 caHashes := make(map[string]struct{})
211                 for _, certs := range verifiedChains {
212                         for _, cert := range certs {
213                                 caHashes[spkiHash(cert)] = struct{}{}
214                         }
215                 }
216                 var restrictedHosts []string
217                 caches.RestrictedM.RLock()
218                 for h := range caHashes {
219                         restrictedHosts = append(restrictedHosts, caches.Restricted[h]...)
220                 }
221                 caches.RestrictedM.RUnlock()
222                 if len(restrictedHosts) > 0 {
223                         for _, h := range restrictedHosts {
224                                 if host == h || strings.HasSuffix(host, "."+h) {
225                                         goto HostIsNotRestricted
226                                 }
227                         }
228                         fifos.LogCert <- fmt.Sprintf("Restricted\t%s", host)
229                         return ErrRejected{host}
230                 }
231         }
232 HostIsNotRestricted:
233         fn := filepath.Join(Certs, host)
234         certsOur, _, err := ucspi.CertPoolFromFile(fn)
235         if err == nil || dialErr != nil || (daneExists && !daneMatched) {
236                 if certsOur != nil && certTheirHash == spkiHash(certsOur[0]) {
237                         caches.AcceptedM.Lock()
238                         caches.Accepted[host] = certTheirHash
239                         caches.AcceptedM.Unlock()
240                         if !bytes.Equal(certsOur[0].Raw, rawCerts[0]) {
241                                 fifos.LogCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash)
242                                 goto CertUpdate
243                         }
244                         return nil
245                 }
246                 var b bytes.Buffer
247                 fmt.Fprintf(&b, "set host \"%s\"\n", host)
248                 if dialErr == nil {
249                         fmt.Fprintf(&b, "set err \"\"\n")
250                 } else {
251                         fmt.Fprintf(&b, "set err \"%s\"\n", dialErr.Error())
252                 }
253                 var daneStatus string
254                 if daneExists {
255                         if daneMatched {
256                                 daneStatus = "ok"
257                         } else {
258                                 daneStatus = "bad"
259                         }
260                 }
261                 fmt.Fprintf(&b, "set daneStatus \"%s\"\n", daneStatus)
262                 hexCerts := make([]string, 0, len(rawCerts))
263                 for _, rawCert := range rawCerts {
264                         hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(rawCert))))
265                 }
266                 fmt.Fprintf(&b, "set certsTheir \"%s\"\n", strings.Join(hexCerts, " "))
267                 hexCerts = make([]string, 0, len(certsOur))
268                 for _, cert := range certsOur {
269                         hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(cert.Raw))))
270                 }
271                 fmt.Fprintf(&b, "set certsOur \"%s\"\n", strings.Join(hexCerts, " "))
272                 b.WriteString(VerifyDialog)
273                 cmd := exec.Command(CmdWish)
274                 // os.WriteFile("/tmp/verify-dialog.tcl", b.Bytes(), 0666)
275                 cmd.Stdin = &b
276                 err = cmd.Run()
277                 exitError, ok := err.(*exec.ExitError)
278                 if !ok {
279                         fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
280                         return ErrRejected{host}
281                 }
282                 switch exitError.ExitCode() {
283                 case 10:
284                         fifos.LogCert <- fmt.Sprintf("Accept\t%s\t%s", host, certTheirHash)
285                         goto CertUpdate
286                 case 11:
287                         fifos.LogCert <- fmt.Sprintf("Once\t%s\t%s", host, certTheirHash)
288                         caches.AcceptedM.Lock()
289                         caches.Accepted[host] = certTheirHash
290                         caches.AcceptedM.Unlock()
291                         return nil
292                 case 12:
293                         caches.RejectedM.Lock()
294                         caches.Rejected[host] = certTheirHash
295                         caches.RejectedM.Unlock()
296                         fallthrough
297                 default:
298                         fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
299                         return ErrRejected{host}
300                 }
301         } else {
302                 if !errors.Is(err, fs.ErrNotExist) {
303                         return err
304                 }
305                 fifos.LogCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
306         }
307 CertUpdate:
308         tmp, err := os.CreateTemp(Certs, "")
309         if err != nil {
310                 log.Fatalln(err)
311         }
312         for _, rawCert := range rawCerts {
313                 err = pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: rawCert})
314                 if err != nil {
315                         log.Fatalln(err)
316                 }
317         }
318         tmp.Close()
319         os.Rename(tmp.Name(), fn)
320         caches.AcceptedM.Lock()
321         caches.Accepted[host] = certTheirHash
322         caches.AcceptedM.Unlock()
323         return nil
324 }