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