2 tofuproxy -- HTTP proxy with TLS certificates management
3 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
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.
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.
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/>.
34 "go.cypherpunks.ru/ucspi"
35 "go.stargrave.org/tofuproxy/caches"
36 "go.stargrave.org/tofuproxy/fifos"
40 CmdCerttool = "certtool"
47 func spkiHash(cert *x509.Certificate) string {
48 hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
49 return hex.EncodeToString(hsh[:])
52 type ErrRejected struct {
56 func (err ErrRejected) Error() string { return err.addr + " was rejected" }
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()
65 lines := make([]string, 0, 128)
66 for i, line := range strings.Split(string(out), "\n") {
67 if strings.Contains(line, "ASCII:") {
70 lines = append(lines, fmt.Sprintf(
71 "%03d %s", i, strings.ReplaceAll(line, `"`, `\"`),
74 return strings.Join(lines, "\n")
81 verifiedChains [][]*x509.Certificate,
83 var certTheir *x509.Certificate
85 if len(verifiedChains) > 0 {
86 certTheir = verifiedChains[0][0]
88 certTheir, err = x509.ParseCertificate(rawCerts[0])
93 certTheirHash := spkiHash(certTheir)
95 defer VerifyM.Unlock()
96 caches.AcceptedM.RLock()
97 certOurHash := caches.Accepted[host]
98 caches.AcceptedM.RUnlock()
99 if certTheirHash == certOurHash {
102 caches.RejectedM.RLock()
103 certOurHash = caches.Rejected[host]
104 caches.RejectedM.RUnlock()
105 if certTheirHash == certOurHash {
106 return ErrRejected{host}
108 daneExists, daneMatched := dane(host, certTheir)
111 fifos.LogDANE <- fmt.Sprintf("%s\tACK", host)
113 fifos.LogDANE <- fmt.Sprintf("%s\tNAK", host)
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)
130 b.WriteString("tk_setPalette grey\n")
131 b.WriteString(fmt.Sprintf("wm title . \"%s\"\n", host))
134 b.WriteString(fmt.Sprintf(`set tErr [text .tErr]
135 $tErr insert end "%s"
136 $tErr configure -wrap word -height 5
138 b.WriteString("grid .tErr -columnspan 3\n")
143 b.WriteString("label .lDANE -bg green -text \"DANE matched\"\n")
145 b.WriteString("label .lDANE -bg red -text \"DANE NOT matched\"\n")
147 b.WriteString("grid .lDANE\n")
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))
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]
160 b.WriteString("grid $tTheir $sbTheir -sticky nsew -columnspan 3\n")
164 for i, cert := range certsOur {
165 bCerts.WriteString(fmt.Sprintf("Our %d:\n", i))
166 bCerts.WriteString(certInfo(cert.Raw))
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]
173 b.WriteString("grid $tOur $sbOur -sticky nsew -columnspan 3\n")
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
188 cmd := exec.Command(CmdWish)
189 // ioutil.WriteFile("/tmp/w.tcl", b.Bytes(), 0666)
192 exitError, ok := err.(*exec.ExitError)
194 fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
195 return ErrRejected{host}
197 switch exitError.ExitCode() {
199 fifos.LogCert <- fmt.Sprintf("Accept\t%s\t%s", host, certTheirHash)
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()
208 caches.RejectedM.Lock()
209 caches.Rejected[host] = certTheirHash
210 caches.RejectedM.Unlock()
213 fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
214 return ErrRejected{host}
217 if !os.IsNotExist(err) {
220 fifos.LogCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
223 tmp, err := os.CreateTemp(Certs, "")
227 for _, rawCert := range rawCerts {
228 err = pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: rawCert})
234 os.Rename(tmp.Name(), fn)
235 caches.AcceptedM.Lock()
236 caches.Accepted[host] = certTheirHash
237 caches.AcceptedM.Unlock()