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