]> Sergey Matveev's repositories - tofuproxy.git/blob - verify.go
7844d14f97ee8bc1542614b930971e24348064dc
[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         VerifyM   sync.Mutex
46 )
47
48 func spkiHash(cert *x509.Certificate) string {
49         hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
50         return hex.EncodeToString(hsh[:])
51 }
52
53 func acceptedAdd(addr, h string) {
54         acceptedM.Lock()
55         accepted[addr] = h
56         acceptedM.Unlock()
57 }
58
59 func rejectedAdd(addr, h string) {
60         rejectedM.Lock()
61         rejected[addr] = h
62         rejectedM.Unlock()
63 }
64
65 type ErrRejected struct {
66         addr string
67 }
68
69 func (err ErrRejected) Error() string { return err.addr + " was rejected" }
70
71 func certInfo(certRaw []byte) string {
72         cmd := exec.Command(CmdCerttool, "--certificate-info", "--inder")
73         cmd.Stdin = bytes.NewReader(certRaw)
74         out, err := cmd.Output()
75         if err != nil {
76                 return err.Error()
77         }
78         lines := make([]string, 0, 128)
79         for i, line := range strings.Split(string(out), "\n") {
80                 if strings.Contains(line, "ASCII:") {
81                         continue
82                 }
83                 lines = append(lines, fmt.Sprintf(
84                         "%03d %s", i, strings.ReplaceAll(line, `"`, `\"`),
85                 ))
86         }
87         return strings.Join(lines, "\n")
88 }
89
90 func verifyCert(
91         host string,
92         dialErr error,
93         rawCerts [][]byte,
94         verifiedChains [][]*x509.Certificate,
95 ) error {
96         var certTheir *x509.Certificate
97         var err error
98         if len(verifiedChains) > 0 {
99                 certTheir = verifiedChains[0][0]
100         } else {
101                 certTheir, err = x509.ParseCertificate(rawCerts[0])
102                 if err != nil {
103                         return err
104                 }
105         }
106         certTheirHash := spkiHash(certTheir)
107         VerifyM.Lock()
108         defer VerifyM.Unlock()
109         acceptedM.RLock()
110         certOurHash := accepted[host]
111         acceptedM.RUnlock()
112         if certTheirHash == certOurHash {
113                 return nil
114         }
115         rejectedM.RLock()
116         certOurHash = rejected[host]
117         rejectedM.RUnlock()
118         if certTheirHash == certOurHash {
119                 return ErrRejected{host}
120         }
121         daneExists, daneMatched := dane(host, certTheir)
122         if daneExists {
123                 if daneMatched {
124                         sinkCert <- fmt.Sprintf("DANE\t%s\tmatched", host)
125                 } else {
126                         sinkErr <- fmt.Sprintf("DANE\t%s\tnot matched", host)
127                 }
128         }
129         fn := filepath.Join(*certs, host)
130         certsOur, _, err := ucspi.CertPoolFromFile(fn)
131         if err == nil || dialErr != nil || (daneExists && !daneMatched) {
132                 if certsOur != nil && certTheirHash == spkiHash(certsOur[0]) {
133                         acceptedAdd(host, certTheirHash)
134                         if bytes.Compare(certsOur[0].Raw, rawCerts[0]) != 0 {
135                                 sinkCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash)
136                                 goto CertUpdate
137                         }
138                         return nil
139                 }
140                 var b bytes.Buffer
141                 b.WriteString(fmt.Sprintf("wm title . \"%s\"\n", host))
142
143                 if dialErr != nil {
144                         b.WriteString(fmt.Sprintf(`set tErr [text .tErr]
145 $tErr insert end "%s"
146 $tErr configure -wrap word -height 5
147 `, dialErr.Error()))
148                         b.WriteString("grid .tErr -columnspan 3\n")
149                 }
150
151                 if daneExists {
152                         if daneMatched {
153                                 b.WriteString("label .lDANE -bg green -text \"DANE matched\"\n")
154                         } else {
155                                 b.WriteString("label .lDANE -bg red -text \"DANE not matched!\"\n")
156                         }
157                         b.WriteString("grid .lDANE\n")
158                 }
159
160                 var bCerts bytes.Buffer
161                 for i, rawCert := range rawCerts {
162                         bCerts.WriteString(fmt.Sprintf("Their %d:\n", i))
163                         bCerts.WriteString(certInfo(rawCert))
164                 }
165                 b.WriteString(fmt.Sprintf(`set tTheir [text .tTheir]
166 $tTheir insert end "%s"
167 set sbTheir [scrollbar .sbTheir -command [list $tTheir yview]]
168 $tTheir configure -wrap word -yscrollcommand [list $sbTheir set]
169 `, bCerts.String()))
170                 b.WriteString("grid $tTheir $sbTheir -sticky nsew -columnspan 3\n")
171
172                 if certsOur != nil {
173                         bCerts.Reset()
174                         for i, cert := range certsOur {
175                                 bCerts.WriteString(fmt.Sprintf("Our %d:\n", i))
176                                 bCerts.WriteString(certInfo(cert.Raw))
177                         }
178                         b.WriteString(fmt.Sprintf(`set tOur [text .tOur]
179 $tOur insert end "%s"
180 set sbOur [scrollbar .sbOur -command [list $tOur yview]]
181 $tOur configure -wrap word -yscrollcommand [list $sbOur set]
182 `, bCerts.String()))
183                         b.WriteString("grid $tOur $sbOur -sticky nsew -columnspan 3\n")
184                 }
185
186                 b.WriteString(`
187 proc doAccept {} { exit 10 }
188 proc doOnce {} { exit 11 }
189 proc doReject {} { exit 12 }
190 button .bAccept -text "Accept" -bg green -command doAccept
191 button .bOnce -text "Once" -command doOnce
192 button .bReject -text "Reject" -bg red -command doReject
193 grid .bAccept .bOnce .bReject
194 grid rowconfigure . 0 -weight 1
195 grid columnconfigure . 0 -weight 1
196 `)
197
198                 cmd := exec.Command(CmdWish)
199                 // ioutil.WriteFile("/tmp/w.tcl", b.Bytes(), 0666)
200                 cmd.Stdin = &b
201                 err = cmd.Run()
202                 exitError, ok := err.(*exec.ExitError)
203                 if !ok {
204                         sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash)
205                         return ErrRejected{host}
206                 }
207                 switch exitError.ExitCode() {
208                 case 10:
209                         sinkCert <- fmt.Sprintf("ADD\t%s\t%s", host, certTheirHash)
210                         goto CertUpdate
211                 case 11:
212                         sinkCert <- fmt.Sprintf("ONCE\t%s\t%s", host, certTheirHash)
213                         acceptedAdd(host, certTheirHash)
214                         return nil
215                 case 12:
216                         rejectedAdd(host, certTheirHash)
217                         fallthrough
218                 default:
219                         sinkCert <- fmt.Sprintf("DENY\t%s\t%s", host, certTheirHash)
220                         return ErrRejected{host}
221                 }
222         } else {
223                 if !os.IsNotExist(err) {
224                         return err
225                 }
226                 sinkCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
227         }
228 CertUpdate:
229         tmp, err := os.CreateTemp(*certs, "")
230         if err != nil {
231                 log.Fatalln(err)
232         }
233         for _, rawCert := range rawCerts {
234                 err = pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: rawCert})
235                 if err != nil {
236                         log.Fatalln(err)
237                 }
238         }
239         tmp.Close()
240         os.Rename(tmp.Name(), fn)
241         acceptedAdd(host, certTheirHash)
242         return nil
243 }