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