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