]> Sergey Matveev's repositories - tofuproxy.git/blob - tls/verify.go
Raised copyright years
[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-2022 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         "fmt"
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         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)
218                                 goto CertUpdate
219                         }
220                         return nil
221                 }
222                 var b bytes.Buffer
223                 b.WriteString(fmt.Sprintf("set host \"%s\"\n", host))
224                 if dialErr == nil {
225                         b.WriteString(fmt.Sprintf("set err \"\"\n"))
226                 } else {
227                         b.WriteString(fmt.Sprintf("set err \"%s\"\n", dialErr.Error()))
228                 }
229                 var daneStatus string
230                 if daneExists {
231                         if daneMatched {
232                                 daneStatus = "ok"
233                         } else {
234                                 daneStatus = "bad"
235                         }
236                 }
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))))
241                 }
242                 b.WriteString(fmt.Sprintf(
243                         "set certsTheir \"%s\"\n", strings.Join(hexCerts, " "),
244                 ))
245                 hexCerts = make([]string, 0, len(certsOur))
246                 for _, cert := range certsOur {
247                         hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(cert.Raw))))
248                 }
249                 b.WriteString(fmt.Sprintf(
250                         "set certsOur \"%s\"\n", strings.Join(hexCerts, " "),
251                 ))
252                 b.WriteString(VerifyDialog)
253                 cmd := exec.Command(CmdWish)
254                 // ioutil.WriteFile("/tmp/verify-dialog.tcl", b.Bytes(), 0666)
255                 cmd.Stdin = &b
256                 err = cmd.Run()
257                 exitError, ok := err.(*exec.ExitError)
258                 if !ok {
259                         fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
260                         return ErrRejected{host}
261                 }
262                 switch exitError.ExitCode() {
263                 case 10:
264                         fifos.LogCert <- fmt.Sprintf("Accept\t%s\t%s", host, certTheirHash)
265                         goto CertUpdate
266                 case 11:
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()
271                         return nil
272                 case 12:
273                         caches.RejectedM.Lock()
274                         caches.Rejected[host] = certTheirHash
275                         caches.RejectedM.Unlock()
276                         fallthrough
277                 default:
278                         fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
279                         return ErrRejected{host}
280                 }
281         } else {
282                 if !os.IsNotExist(err) {
283                         return err
284                 }
285                 fifos.LogCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
286         }
287 CertUpdate:
288         tmp, err := os.CreateTemp(Certs, "")
289         if err != nil {
290                 log.Fatalln(err)
291         }
292         for _, rawCert := range rawCerts {
293                 err = pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: rawCert})
294                 if err != nil {
295                         log.Fatalln(err)
296                 }
297         }
298         tmp.Close()
299         os.Rename(tmp.Name(), fn)
300         caches.AcceptedM.Lock()
301         caches.Accepted[host] = certTheirHash
302         caches.AcceptedM.Unlock()
303         return nil
304 }