-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy patheccentric.go
310 lines (260 loc) · 10.7 KB
/
eccentric.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// Eccentric Authentication Suite
//
// Tools and utilities to create an Eccentric Authentication application.
// Makes it easy to do correctly.
//
// Copyright 2013, Guido Witmond <guido@witmond.nl>
// Licensed under AGPL v3 or later. See LICENSE.
package eccentric
import (
"net/http"
"html/template"
"io/ioutil"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"log"
"regexp"
"github.com/gwitmond/unbound" // also does DANE parsing
)
// Authentication contains the configuration parameters for the application.
// RegisterURL: URL of the page at the FPCA where the user agent signs up for a certificate
// example: "https://register-dating.wtmnd.nl:10444/register-pubkey"
// Debug: Boolean to determine debugging
type Authentication struct {
RegisterURL string
Templates *template.Template
Debug bool
}
func (ecca *Authentication) debug (format string, params... interface{}) {
if ecca.Debug == true {
log.Printf(format, params...)
}
}
// HTTP handlers
// templateHandler returns a handler that serves HTTP requests by
// applying the named template without parameters to the template
func (ecca *Authentication) TemplateHandler (templateName string) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ecca.debug("Ecca: rendering template: %q\n", templateName)
check(ecca.Templates.ExecuteTemplate(w, templateName, nil))
})
}
// loggedInHandler returns a handler that calls the given handler when the client uses a certificate to authenticate.
// Otherwise it sends a Ecca-login page
func (ecca *Authentication) LoggedInHandler (handler http.HandlerFunc, templateParams ...interface{}) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Require to be logged in.
ecca.debug("Ecca: Checking if user is logged in")
if len(req.TLS.PeerCertificates) == 0 {
ecca.debug("Ecca: User does not have a (correct) certificate, sending login page\n")
ecca.SendToLoginPage(w, templateParams...)
return
}
// User is logged in. Run the application handler.
ecca.debug("Ecca: User has certificate. CN is: %v\n", req.TLS.PeerCertificates[0].Subject.CommonName)
handler.ServeHTTP(w, req)
})
}
// SendToLoginPage redirects the browser to the sites' FPCA.
// It sets the WWW-Authenticate header so the user agent knows where to sign up.
// It sets response headers so no output may have been written so far.
func (ecca *Authentication) SendToLoginPage (w http.ResponseWriter, template_params ...interface{}) {
w.Header().Set("Content-Type", "text/html")
// Tell the user-agent where to obtain a certificate.
w.Header().Set("WWW-Authenticate", "Ecca type=\"public-key\" register=\"" + ecca.RegisterURL + "\"")
w.WriteHeader(401)
// Render a template if we have one
if (len(template_params) >= 1) {
template := (template_params[0]).(string)
check (ecca.Templates.ExecuteTemplate(w, template, template_params[1:]))
} else {
w.Write([]byte("You need to register.\n"))
}
}
// ReadCert, read (server) certificate file or panic
func ReadCert(certFile string) (*x509.CertPool) {
pool := x509.NewCertPool()
certPEMBlock, err := ioutil.ReadFile(certFile)
if err != nil {
panic("Cannot read certificate file " + certFile)
}
ok := pool.AppendCertsFromPEM(certPEMBlock)
if !ok {
panic("Cannot parse certificate file " + certFile)
}
return pool
}
type AppHandler func(http.ResponseWriter, *http.Request) error
// Catch panics and show them.
func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
rec := recover()
if rec != nil {
fmt.Printf("Panic detected: %#v\n", rec)
http.Error(w, fmt.Sprintf("Panic: %#v", rec), 500)
}
}()
err := fn(w, r)
if err != nil {
fmt.Printf("Error detected: %v:\n", err.Error())
http.Error(w, err.Error(), 500)
}
}
// ValidateEccentricCertificate verifies that the given certificate parses to a real x509 certificate
// and that it is signed by the FPCA
// DANE/TLSA record it specifies in the CN.
// TODO: Deprecate this function as it handles only direct signing by the FPCA, no SubCAs
// Use ValidateEccentricCertificateChain instead
func ValidateEccentricCertificate(cl_cert *x509.Certificate) (caCert *x509.Certificate, err error) {
log.Printf("Validate Eccentric Certificate got client certificate: Issuer: %#v\nand Subject: %#v", cl_cert.Issuer, cl_cert.Subject)
// Check the cn if it has the @@ in it.
cn := cl_cert.Subject.CommonName // the chosen userid@@realm
log.Printf("Subject CommonName is %v\n", cn)
_, _, err = ParseCN(cn)
if err != nil { return nil, err }
// Now fetch the issuer. That must be the FPCA.
issuer := cl_cert.Issuer.CommonName
if issuer == "" {
return nil, errors.New("Certificate does not look like an Eccentric Authenticated client certificate. It has an empty Issuer.CommonName. We expect the fqdn of its FPCA.")
}
unb := unbound.New()
caCert, err = unb.GetCACert(issuer)
check(err)
log.Printf("Got certificate: Issuer: %#v\nand Subject: %#v", caCert.Issuer, caCert.Subject)
err = cl_cert.CheckSignatureFrom(caCert)
check (err) // TODO: give out neat error at validation failure, not a panic.
return caCert, nil
}
// ValidateEccentricCertificate verifies that the given certificate parses to a real x509 certificate
// It is signed by the FPCA
// DANE/TLSA record it specifies in the CN.
func ValidateEccentricCertificateChain(cl_cert *x509.Certificate, root *x509.Certificate) (chain []x509.Certificate, err error) {
log.Printf("Validate Eccentric Certificate Chain got client certificate: Issuer: %#v\nand Subject: %#v", cl_cert.Issuer, cl_cert.Subject)
// Check the cn if it has the @@ in it.
cn := cl_cert.Subject.CommonName // the chosen userid@@realm
log.Printf("Subject CommonName is %v\n", cn)
_, _, err = ParseCN(cn)
if err != nil { return nil, err }
// Now fetch the chain
chain, err = FetchCertificateChain(cl_cert, root)
return // chain
}
// Fetch the certificate chain from the given certifcate upto the root.
// Return the chain that validates the cl_cert.
// This version looks up certificate in DNS based upon their CommonName.
// ie. FPCA.domain.tld, ROOTCA.domain.tld.
// We stop searching when certX.Issuer.CN == Root.Subject.CN
// We return at least 1 certificate, the Root.
// TODO: Get certificates based upon Serials
func FetchCertificateChain(cl_cert *x509.Certificate, root *x509.Certificate) ([]x509.Certificate, error) {
chain, err := fetchCertificateChain(cl_cert, root)
// reverse to make the Root certificate last, as OpenSSL likes it.
for i, j := 0, len(chain)-1; i<j; i,j = i+1, i-i {
chain[i], chain[j] = chain[j], chain[i]
}
return chain, err
}
func fetchCertificateChain(cl_cert *x509.Certificate, root *x509.Certificate) (chain []x509.Certificate, err error) {
// check if cl_cert is signed by Root
if (cl_cert.Issuer.CommonName == root.Subject.CommonName) {
// client cert is signed by Root, there are no (more) intermediaries. We're done.
chain = append(chain, *root)
return chain, nil
}
issuer := cl_cert.Issuer.CommonName
if issuer == "" {
// chain is empty at this point.
return chain, errors.New("Certificate does not look like an Eccentric Authenticated Intermediate certificate. It has an empty Issuer.CommonName. We expect the fqdn of its FPCA.")
}
unb := unbound.New()
caCert, err := unb.GetCACert(issuer)
check(err)
log.Printf("Got certificate: Issuer: %#v\nand Subject: %#v", caCert.Issuer, caCert.Subject)
// check if the signature matches
err = cl_cert.CheckSignatureFrom(caCert)
check(err) // TODO: give out neat error at validation failure, not a panic.
// recurse to get higher up the tree.
chain, err = FetchCertificateChain(caCert, root)
if (err != nil) { return } // empty, err
chain = append(chain, *caCert)
return // chain, nil
}
// FetchRootCA fetches the RootCA certificate for the given hostname.
func FetchRootCA(hostname string) (*x509.Certificate, error) {
rootname := "RootCA." + hostname // per definition
return FetchCN(rootname)
}
// FetchFPCA fetches the FPCA certificate for the given hostname.
func FetchFPCA(hostname string) (*x509.Certificate, error) {
fpcaname := "FPCA." + hostname // per definition
return FetchCN(fpcaname)
}
// FetchCN fetches a DANE record for CN in DNSSEC
func FetchCN(cn string) (*x509.Certificate, error) {
unb := unbound.New()
cert, err := unb.GetCACert(cn)
check(err)
log.Printf("Got certificate: Issuer: %#v\nand Subject: %#v", cert.Issuer, cert.Subject)
return cert, err
}
// Parse a single (client) certificate,
// Return a x509.Certificate structure
// To Be Deprecated. Use ParseCertString or ParseCertByteA instead
func ParseCert(cert string) (*x509.Certificate, error) {
return ParseCertString(cert)
}
// Parse a single (client) certificate,
// Return a x509.Certificate structure
func ParseCertString(cert string) (*x509.Certificate, error) {
return ParseCertByteA([]byte(cert))
}
// Parse a single (client) certificate,
// Return a x509.Certificate structure
func ParseCertByteA(cert []byte) (*x509.Certificate, error) {
// decode pem...,
pemBlock, _ := pem.Decode(cert)
if pemBlock == nil {
return nil, errors.New("Did not receive a PEM encoded block of data")
}
// check PEM
if pemBlock == nil {
return nil, errors.New("Did not receive a PEM encoded certificate")
}
// check type...,
if pemBlock.Type != "CERTIFICATE" {
return nil, errors.New("Did not receive a PEM encoded certificate")
}
// parse der to validate the data...,
return x509.ParseCertificate(pemBlock.Bytes)
}
// match <cn>@@<fqdn> with ascii domain names
var cnRE = regexp.MustCompile(`^([^@]+)@@([a-zA-Z0-9._]+)$`)
// parseCN parses the string and returns the username and realm parts if it mathes the
// cnRE - regular expression. Otherwise, it returns two empty strings.
func ParseCN(cn string) (username, realm string, err error) {
match := cnRE.FindStringSubmatch(cn)
// fmt.Printf("match %v gives: %#v\n", cn, match)
if len(match) == 3 {
return match[1], match[2], nil
}
return "", "", errors.New("Certificate does not look like an Eccentric Authenticated client certificate. It has no <cn>@@sitename in the Subject.CommonName.")
}
func PEMEncode(cert *x509.Certificate) []byte {
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
}
func PEMDecode(pemBytes []byte) x509.Certificate {
block, _ := pem.Decode(pemBytes)
certs, err := x509.ParseCertificates(block.Bytes)
check(err)
if len(certs) != 1 {
check(errors.New(fmt.Sprintf("Cannot parse CA certificate from database. Received: %#v which parsed to %#v\n", pemBytes, certs)))
}
return *certs[0]
}
func check(err error) {
if err != nil {
panic(err)
}
}