]> Sergey Matveev's repositories - tofuproxy.git/blob - verify.go
f6844cd997f7fd13a3198eb488ca6ff102ba6e01
[tofuproxy.git] / verify.go
1 /*
2 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, version 3 of the License.
7
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 */
16
17 package main
18
19 import (
20         "bytes"
21         "crypto/sha256"
22         "crypto/x509"
23         "encoding/hex"
24         "encoding/pem"
25         "fmt"
26         "log"
27         "os"
28         "os/exec"
29         "path/filepath"
30         "strings"
31         "sync"
32
33         "go.cypherpunks.ru/ucspi"
34 )
35
36 var (
37         CmdCerttool = "certtool"
38         CmdWish     = "wish8.7"
39
40         certs     *string
41         accepted  = make(map[string]string)
42         acceptedM sync.RWMutex
43         rejected  = make(map[string]string)
44         rejectedM sync.RWMutex
45 )
46
47 func spkiHash(cert *x509.Certificate) string {
48         hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
49         return hex.EncodeToString(hsh[:])
50 }
51
52 func acceptedAdd(addr, h string) {
53         acceptedM.Lock()
54         accepted[addr] = h
55         acceptedM.Unlock()
56 }
57
58 func rejectedAdd(addr, h string) {
59         rejectedM.Lock()
60         rejected[addr] = h
61         rejectedM.Unlock()
62 }
63
64 type ErrRejected struct {
65         addr string
66 }
67
68 func (err ErrRejected) Error() string { return err.addr + " was rejected" }
69
70 func certInfo(certRaw []byte) string {
71         cmd := exec.Command(CmdCerttool, "--certificate-info", "--inder")
72         cmd.Stdin = bytes.NewReader(certRaw)
73         out, err := cmd.Output()
74         if err != nil {
75                 return err.Error()
76         }
77         lines := make([]string, 0, 128)
78         for i, line := range strings.Split(string(out), "\n") {
79                 if strings.Contains(line, "ASCII:") {
80                         continue
81                 }
82                 lines = append(lines, fmt.Sprintf(
83                         "%03d %s", i, strings.ReplaceAll(line, `"`, `\"`),
84                 ))
85         }
86         return strings.Join(lines, "\n")
87 }
88
89 func verifyCert(
90         host string,
91         dialErr error,
92         rawCerts [][]byte,
93         verifiedChains [][]*x509.Certificate,
94 ) error {
95         var certTheir *x509.Certificate
96         var err error
97         if len(verifiedChains) > 0 {
98                 certTheir = verifiedChains[0][0]
99         } else {
100                 certTheir, err = x509.ParseCertificate(rawCerts[0])
101                 if err != nil {
102                         return err
103                 }
104         }
105         certTheirHash := spkiHash(certTheir)
106         acceptedM.RLock()
107         certOurHash := accepted[host]
108         acceptedM.RUnlock()
109         if certTheirHash == certOurHash {
110                 return nil
111         }
112         rejectedM.RLock()
113         certOurHash = rejected[host]
114         rejectedM.RUnlock()
115         if certTheirHash == certOurHash {
116                 return ErrRejected{host}
117         }
118         daneExists, daneMatched := dane(host, certTheir)
119         if daneExists {
120                 if daneMatched {
121                         sinkCert <- fmt.Sprintf("DANE\t%s\tmatched", host)
122                 } else {
123                         sinkErr <- fmt.Sprintf("DANE\t%s\tnot matched", host)
124                 }
125         }
126         fn := filepath.Join(*certs, host)
127         certsOur, _, err := ucspi.CertPoolFromFile(fn)
128         if err == nil || dialErr != nil || (daneExists && !daneMatched) {
129                 if certsOur != nil && certTheirHash == spkiHash(certsOur[0]) {
130                         acceptedAdd(host, certTheirHash)
131                         if bytes.Compare(certsOur[0].Raw, rawCerts[0]) != 0 {
132                                 sinkCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash)
133                                 goto CertUpdate
134                         }
135                         return nil
136                 }
137                 var b bytes.Buffer
138                 b.WriteString(fmt.Sprintf("wm title . \"%s\"\n", host))
139
140                 if dialErr != nil {
141                         b.WriteString(fmt.Sprintf(`set tErr [text .tErr]
142 $tErr insert end "%s"
143 $tErr configure -wrap word -height 5
144 `, dialErr.Error()))
145                         b.WriteString("grid .tErr -columnspan 3\n")
146                 }
147
148                 if daneExists {
149                         if daneMatched {
150                                 b.WriteString("label .lDANE -bg green -text \"DANE matched\"\n")
151                         } else {
152                                 b.WriteString("label .lDANE -bg red -text \"DANE not matched!\"\n")
153                         }
154                         b.WriteString("grid .lDANE\n")
155                 }
156
157                 var bCerts bytes.Buffer
158                 for i, rawCert := range rawCerts {
159                         bCerts.WriteString(fmt.Sprintf("Their %d:\n", i))
160                         bCerts.WriteString(certInfo(rawCert))
161                 }
162                 b.WriteString(fmt.Sprintf(`set tTheir [text .tTheir]
163 $tTheir insert end "%s"
164 set sbTheir [scrollbar .sbTheir -command [list $tTheir yview]]
165 $tTheir configure -wrap word -yscrollcommand [list $sbTheir set]
166 `, bCerts.String()))
167                 b.WriteString("grid $tTheir $sbTheir -sticky nsew -columnspan 3\n")
168
169                 if certsOur != nil {
170                         bCerts.Reset()
171                         for i, cert := range certsOur {
172                                 bCerts.WriteString(fmt.Sprintf("Our %d:\n", i))
173                                 bCerts.WriteString(certInfo(cert.Raw))
174                         }
175                         b.WriteString(fmt.Sprintf(`set tOur [text .tOur]
176 $tOur insert end "%s"
177 set sbOur [scrollbar .sbOur -command [list $tOur yview]]
178 $tOur configure -wrap word -yscrollcommand [list $sbOur set]
179 `, bCerts.String()))
180                         b.WriteString("grid $tOur $sbOur -sticky nsew -columnspan 3\n")
181                 }
182
183                 b.WriteString(`
184 proc doAccept {} { exit 10 }
185 proc doOnce {} { exit 11 }
186 proc doReject {} { exit 12 }
187 button .bAccept -text "Accept" -bg green -command doAccept
188 button .bOnce -text "Once" -command doOnce
189 button .bReject -text "Reject" -bg red -command doReject
190 grid .bAccept .bOnce .bReject
191 grid rowconfigure . 0 -weight 1
192 grid columnconfigure . 0 -weight 1
193 `)
194
195                 cmd := exec.Command(CmdWish)
196                 // ioutil.WriteFile("/tmp/w.tcl", b.Bytes(), 0666)
197                 cmd.Stdin = &b
198                 err = cmd.Run()
199                 exitError, ok := err.(*exec.ExitError)
200                 if !ok {
201                         sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash)
202                         return ErrRejected{host}
203                 }
204                 switch exitError.ExitCode() {
205                 case 10:
206                         sinkCert <- fmt.Sprintf("ADD\t%s\t%s", host, certTheirHash)
207                         goto CertUpdate
208                 case 11:
209                         sinkCert <- fmt.Sprintf("ONCE\t%s\t%s", host, certTheirHash)
210                         acceptedAdd(host, certTheirHash)
211                         return nil
212                 case 12:
213                         rejectedAdd(host, certTheirHash)
214                         fallthrough
215                 default:
216                         sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash)
217                         return ErrRejected{host}
218                 }
219         } else {
220                 if !os.IsNotExist(err) {
221                         return err
222                 }
223                 sinkCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
224         }
225 CertUpdate:
226         tmp, err := os.CreateTemp(*certs, "")
227         if err != nil {
228                 log.Fatalln(err)
229         }
230         for _, rawCert := range rawCerts {
231                 err = pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: rawCert})
232                 if err != nil {
233                         log.Fatalln(err)
234                 }
235         }
236         tmp.Close()
237         os.Rename(tmp.Name(), fn)
238         acceptedAdd(host, certTheirHash)
239         return nil
240 }