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