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"
39 const VerifyDialog = `
40 # host err daneStatus certsTheir certsOur
42 if {[string length $err] > 0} {
45 $tErr configure -wrap word -height 5
46 $tErr configure -state disabled
50 proc certsDecode {raws} {
52 foreach raw [split $raws] {
55 foreach line [split [binary decode hex $raw] "\n"] {
56 lappend lines [format "%03d %s" $lineN $line]
59 lappend certs [join $lines "\n"]
64 set certsTheir [certsDecode $certsTheir]
65 set certsOur [certsDecode $certsOur]
70 proc paginator {i delta t l certs} {
72 if {$i == [llength $certs]} {
75 set i [expr {[llength $certs] - 1}]
77 $t configure -state normal
79 $t insert end [lindex $certs $i]
80 $t configure -state disabled
81 $l configure -text "[expr {$i + 1}] / [llength $certs]"
85 proc addCertsWindow {name} {
86 global certs$name t$name sb$name page$name l$name
88 set sb [scrollbar .sb$name -command [list $t yview]]
89 $t configure -wrap word -yscrollcommand [list $sb set]
90 $t configure -state disabled
91 grid $t $sb -sticky nsew
96 set l$name [label .lPage$name]
97 button .bNext$name -text "Next" -command [subst {
98 set page$name \[paginator \$page$name +1 \$t$name \$l$name \$certs$name]
100 button .bPrev$name -text "Prev" -command [subst {
101 set page$name \[paginator \$page$name -1 \$t$name \$l$name \$certs$name]
104 grid .lPage$name .bNext$name .bPrev$name -in .fControl$name
105 set page$name [paginator -1 +1 $t [set l$name] [set certs$name]]
109 if {[llength $certsOur] > 0} { addCertsWindow Our }
111 set lDANE [label .lDANE]
112 if {$daneStatus ne ""} {
113 array set daneColour {ok green bad red}
114 $lDANE configure -bg $daneColour($daneStatus)
115 $lDANE configure -text "DANE-EE: $daneStatus"
117 proc doAccept {} { exit 10 }
118 proc doOnce {} { exit 11 }
119 proc doReject {} { exit 12 }
120 button .bAccept -text "Accept" -bg green -command doAccept
121 button .bOnce -text "Once" -bg green -command doOnce
122 button .bReject -text "Reject" -bg red -command doReject
124 grid .lDANE .bAccept .bOnce .bReject -in .fButtons
125 grid rowconfigure . 0 -weight 1
126 grid columnconfigure . 0 -weight 1
128 bind . <KeyPress> {switch -exact %K {
129 q {exit 0} ; # reject once
133 n {.bNextTheir invoke}
134 p {.bPrevTheir invoke}
141 CmdCerttool = "certtool"
148 func spkiHash(cert *x509.Certificate) string {
149 hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
150 return hex.EncodeToString(hsh[:])
153 type ErrRejected struct {
157 func (err ErrRejected) Error() string { return err.addr + " was rejected" }
159 func certInfo(certRaw []byte) string {
160 cmd := exec.Command(CmdCerttool, "--certificate-info", "--inder")
161 cmd.Stdin = bytes.NewReader(certRaw)
162 out, err := cmd.Output()
173 verifiedChains [][]*x509.Certificate,
175 var certTheir *x509.Certificate
177 if len(verifiedChains) > 0 {
178 certTheir = verifiedChains[0][0]
180 certTheir, err = x509.ParseCertificate(rawCerts[0])
185 certTheirHash := spkiHash(certTheir)
187 defer VerifyM.Unlock()
188 caches.AcceptedM.RLock()
189 certOurHash := caches.Accepted[host]
190 caches.AcceptedM.RUnlock()
191 if certTheirHash == certOurHash {
194 caches.RejectedM.RLock()
195 certOurHash = caches.Rejected[host]
196 caches.RejectedM.RUnlock()
197 if certTheirHash == certOurHash {
198 return ErrRejected{host}
200 daneExists, daneMatched := dane(host, certTheir)
203 fifos.LogDANE <- fmt.Sprintf("%s\tACK", host)
205 fifos.LogDANE <- fmt.Sprintf("%s\tNAK", host)
208 fn := filepath.Join(Certs, host)
209 certsOur, _, err := ucspi.CertPoolFromFile(fn)
210 if err == nil || dialErr != nil || (daneExists && !daneMatched) {
211 if certsOur != nil && certTheirHash == spkiHash(certsOur[0]) {
212 caches.AcceptedM.Lock()
213 caches.Accepted[host] = certTheirHash
214 caches.AcceptedM.Unlock()
215 if bytes.Compare(certsOur[0].Raw, rawCerts[0]) != 0 {
216 fifos.LogCert <- fmt.Sprintf("Refresh\t%s\t%s", host, certTheirHash)
222 b.WriteString(fmt.Sprintf("set host \"%s\"\n", host))
224 b.WriteString(fmt.Sprintf("set err \"\"\n"))
226 b.WriteString(fmt.Sprintf("set err \"%s\"\n", dialErr.Error()))
228 var daneStatus string
236 b.WriteString(fmt.Sprintf("set daneStatus \"%s\"\n", daneStatus))
237 hexCerts := make([]string, 0, len(rawCerts))
238 for _, rawCert := range rawCerts {
239 hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(rawCert))))
241 b.WriteString(fmt.Sprintf(
242 "set certsTheir \"%s\"\n", strings.Join(hexCerts, " "),
244 hexCerts = make([]string, 0, len(certsOur))
245 for _, cert := range certsOur {
246 hexCerts = append(hexCerts, hex.EncodeToString([]byte(certInfo(cert.Raw))))
248 b.WriteString(fmt.Sprintf(
249 "set certsOur \"%s\"\n", strings.Join(hexCerts, " "),
251 b.WriteString(VerifyDialog)
252 cmd := exec.Command(CmdWish)
253 // ioutil.WriteFile("/tmp/verify-dialog.tcl", b.Bytes(), 0666)
256 exitError, ok := err.(*exec.ExitError)
258 fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
259 return ErrRejected{host}
261 switch exitError.ExitCode() {
263 fifos.LogCert <- fmt.Sprintf("Accept\t%s\t%s", host, certTheirHash)
266 fifos.LogCert <- fmt.Sprintf("Once\t%s\t%s", host, certTheirHash)
267 caches.AcceptedM.Lock()
268 caches.Accepted[host] = certTheirHash
269 caches.AcceptedM.Unlock()
272 caches.RejectedM.Lock()
273 caches.Rejected[host] = certTheirHash
274 caches.RejectedM.Unlock()
277 fifos.LogCert <- fmt.Sprintf("Reject\t%s\t%s", host, certTheirHash)
278 return ErrRejected{host}
281 if !os.IsNotExist(err) {
284 fifos.LogCert <- fmt.Sprintf("TOFU\t%s\t%s", host, certTheirHash)
287 tmp, err := os.CreateTemp(Certs, "")
291 for _, rawCert := range rawCerts {
292 err = pem.Encode(tmp, &pem.Block{Type: "CERTIFICATE", Bytes: rawCert})
298 os.Rename(tmp.Name(), fn)
299 caches.AcceptedM.Lock()
300 caches.Accepted[host] = certTheirHash
301 caches.AcceptedM.Unlock()