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