summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-09-05 03:52:50 +0000
committerMike Crute <mike@crute.us>2017-09-05 03:52:50 +0000
commitb7867d9cf5b0dd175b8167a552b830ebfe47d0ed (patch)
tree4b52c7461c7d9c48d68bec78cac6d06ae5940d28
parent34d0f2d7e323acdc48cf91b0dc8514b6753de5d5 (diff)
downloadoidc_proxy-b7867d9cf5b0dd175b8167a552b830ebfe47d0ed.tar.bz2
oidc_proxy-b7867d9cf5b0dd175b8167a552b830ebfe47d0ed.tar.xz
oidc_proxy-b7867d9cf5b0dd175b8167a552b830ebfe47d0ed.zip
Finish JWS and Cert validation
-rw-r--r--cautious_http_client.go13
-rw-r--r--jws_validator.go104
-rw-r--r--key_validator.go144
-rw-r--r--main.go118
-rwxr-xr-xoidc_proxybin0 -> 6495319 bytes
-rw-r--r--util.go43
6 files changed, 335 insertions, 87 deletions
diff --git a/cautious_http_client.go b/cautious_http_client.go
index 66179f2..2f33ae0 100644
--- a/cautious_http_client.go
+++ b/cautious_http_client.go
@@ -2,6 +2,7 @@ package main
2 2
3import ( 3import (
4 "encoding/json" 4 "encoding/json"
5 "fmt"
5 "net" 6 "net"
6 "net/http" 7 "net/http"
7 "net/url" 8 "net/url"
@@ -28,9 +29,9 @@ func NewCautiousHTTPClient() CautiousHTTPClient {
28 }).DialContext, 29 }).DialContext,
29 MaxIdleConns: 100, 30 MaxIdleConns: 100,
30 IdleConnTimeout: 90 * time.Second, 31 IdleConnTimeout: 90 * time.Second,
31 TLSHandshakeTimeout: 3 * time.Second, 32 TLSHandshakeTimeout: 1 * time.Second,
32 ExpectContinueTimeout: 1 * time.Second, 33 ExpectContinueTimeout: 1 * time.Second,
33 ResponseHeaderTimeout: 5 * time.Second, 34 ResponseHeaderTimeout: 10 * time.Second,
34 MaxResponseHeaderBytes: 500000, // .5 MB 35 MaxResponseHeaderBytes: 500000, // .5 MB
35 } 36 }
36 37
@@ -49,11 +50,9 @@ func (c *cautiousHttpClient) Get(gurl string) (*http.Response, error) {
49 } 50 }
50 51
51 // TODO 52 // TODO
52 /* 53 if u.Scheme != "https" && false {
53 if u.Scheme != "https" { 54 return nil, fmt.Errorf("URL for GET must be secure")
54 return nil, fmt.Errorf("URL for GET must be secure") 55 }
55 }
56 */
57 56
58 r, err := c.client.Get(u.String()) 57 r, err := c.client.Get(u.String())
59 if err != nil { 58 if err != nil {
diff --git a/jws_validator.go b/jws_validator.go
new file mode 100644
index 0000000..e77c026
--- /dev/null
+++ b/jws_validator.go
@@ -0,0 +1,104 @@
1package main
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "gopkg.in/square/go-jose.v2"
8 "gopkg.in/square/go-jose.v2/jwt"
9 "time"
10)
11
12type Claims struct {
13 Nonce string `json:"nonce,omitempty"`
14 jwt.Claims
15}
16
17type JWSValidator interface {
18 Validate(string, string) (*Claims, error)
19}
20
21type jwsValidator struct {
22 algorithms *stringSet
23 jwks map[string]jose.JSONWebKey
24 issuer string
25 clientID string
26 clockSkew time.Duration
27 maxLifetime time.Duration
28}
29
30// TODO
31// validate amr claim contains requested acr values (selective_mfa will be just mfa)
32// validate acr claim is the same as requested acr_values
33func NewJWSValidator(jwks map[string]jose.JSONWebKey, issuer string, client_id string, skew time.Duration, max_life time.Duration) JWSValidator {
34 return &jwsValidator{
35 algorithms: NewStringSet("PS256", "PS385", "PS512"),
36 jwks: jwks,
37 issuer: issuer,
38 clientID: client_id,
39 clockSkew: skew,
40 maxLifetime: max_life,
41 }
42}
43
44func (v *jwsValidator) Validate(j string, nonce string) (*Claims, error) {
45 parsed_jwt, err := jwt.ParseSigned(j)
46 if err != nil {
47 return nil, err
48 }
49
50 if len(parsed_jwt.Headers) != 1 {
51 return nil, fmt.Errorf("Invalid signature count")
52 }
53
54 head := parsed_jwt.Headers[0]
55
56 if !v.algorithms.Contains(head.Algorithm) {
57 return nil, fmt.Errorf("Invalid signature algorithm")
58 }
59
60 if typ, ok := head.ExtraHeaders[jose.HeaderType]; !ok || typ != "JWS" {
61 return nil, fmt.Errorf("Invalid token type")
62 }
63
64 key, ok := v.jwks[head.KeyID]
65 if !ok {
66 return nil, fmt.Errorf("No key found for key id")
67 }
68
69 claims := &Claims{}
70 if err = parsed_jwt.Claims(key, claims); err != nil {
71 return nil, err
72 }
73
74 exp := jwt.Expected{
75 Issuer: v.issuer,
76 Audience: jwt.Audience{v.clientID},
77 Time: time.Now(),
78 }
79
80 if err := claims.ValidateWithLeeway(exp, v.clockSkew); err != nil {
81 return nil, err
82 }
83
84 if claims.IssuedAt.Time().Add(v.maxLifetime).Before(time.Now()) {
85 return nil, fmt.Errorf("Token exceeded max lifetime")
86 }
87
88 if err = v.validateNonce(nonce, claims.Nonce); err != nil {
89 return nil, err
90 }
91
92 return claims, nil
93}
94
95func (v *jwsValidator) validateNonce(nonce string, token_nonce string) error {
96 s256 := sha256.New()
97 s256.Write([]byte(nonce))
98 hashed_nonce := hex.EncodeToString(s256.Sum(nil))
99 if token_nonce != hashed_nonce {
100 return fmt.Errorf("Invalid nonce")
101 }
102
103 return nil
104}
diff --git a/key_validator.go b/key_validator.go
new file mode 100644
index 0000000..fe6eb7b
--- /dev/null
+++ b/key_validator.go
@@ -0,0 +1,144 @@
1package main
2
3import (
4 "crypto/rsa"
5 "crypto/x509"
6 "encoding/pem"
7 "fmt"
8 "gopkg.in/square/go-jose.v2"
9 "io/ioutil"
10)
11
12type KeyValidator interface {
13 Validate(jose.JSONWebKey) error
14 LoadRootPEM(string) error
15}
16
17type keyValidator struct {
18 pkiSubject string
19 algorithms *stringSet
20 roots *x509.CertPool
21}
22
23func NewKeyValidator(subject string) KeyValidator {
24 return &keyValidator{
25 pkiSubject: subject,
26 algorithms: NewStringSet("PS256", "PS385", "PS512"),
27 roots: x509.NewCertPool(),
28 }
29}
30
31func (v *keyValidator) LoadRootPEM(filename string) error {
32 pem_data, err := ioutil.ReadFile(filename)
33 if err != nil {
34 return err
35 }
36
37 pem_block, _ := pem.Decode(pem_data)
38 if pem_block == nil {
39 return fmt.Errorf("PEM decode failed")
40 }
41
42 cert, err := x509.ParseCertificate(pem_block.Bytes)
43 if err != nil {
44 return err
45 }
46
47 v.roots.AddCert(cert)
48
49 return nil
50}
51
52func (v *keyValidator) Validate(key jose.JSONWebKey) error {
53 pk, ok := key.Key.(*rsa.PublicKey)
54 if !ok {
55 return fmt.Errorf("Key type is not RSA")
56 }
57
58 if !v.algorithms.Contains(key.Algorithm) {
59 return fmt.Errorf("Key algorithm is not supported")
60 }
61
62 cert := key.Certificates[0]
63 cpk, ok := cert.PublicKey.(*rsa.PublicKey)
64 if !ok {
65 return fmt.Errorf("Public key is not RSA")
66 }
67
68 if cpk.N.BitLen() < 2048 {
69 return fmt.Errorf("Key length less than 2048 bits")
70 }
71
72 if cert.KeyUsage&x509.KeyUsageDigitalSignature != 1 {
73 return fmt.Errorf("Certificate not valid for digital signatures")
74 }
75
76 err := v.validateCertificateChain(key.Certificates)
77 if err != nil {
78 return err
79 }
80
81 err = v.validateCertificateCRL(cert)
82 if err != nil {
83 return err
84 }
85
86 err = v.validatePublicKeyInCertificate(pk, cpk)
87 if err != nil {
88 return err
89 }
90
91 return nil
92}
93
94// TODO
95// Fetch CRL from distrubtion point in cert
96// Validate CRL signed by trusted CA
97// Validate cert not in CRL
98func (v *keyValidator) validateCertificateCRL(cert *x509.Certificate) error {
99 return nil
100}
101
102func (v *keyValidator) validateCertificateChain(chain []*x509.Certificate) error {
103 vo := x509.VerifyOptions{
104 Roots: v.roots,
105 KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
106 }
107
108 if len(chain) > 1 {
109 ip := x509.NewCertPool()
110 for _, i := range chain[1:] {
111 ip.AddCert(i)
112 }
113
114 vo.Intermediates = ip
115 }
116
117 chains, err := chain[0].Verify(vo)
118 if err != nil {
119 return err
120 }
121
122 if len(chains) <= 0 {
123 return fmt.Errorf("No valid certificate chains found")
124 }
125
126 if chain[0].Subject.CommonName != v.pkiSubject {
127 return fmt.Errorf("Invalid certificate subject name")
128 }
129
130 return nil
131}
132
133// validate first item of x5c matches n and e
134func (v *keyValidator) validatePublicKeyInCertificate(pk *rsa.PublicKey, cpk *rsa.PublicKey) error {
135 if cpk.E != pk.E {
136 return fmt.Errorf("E in key and E in cert do not match")
137 }
138
139 if pk.N.Cmp(cpk.N) != 0 {
140 return fmt.Errorf("N in key and N in cert do not match")
141 }
142
143 return nil
144}
diff --git a/main.go b/main.go
index 44501c0..965e72c 100644
--- a/main.go
+++ b/main.go
@@ -11,6 +11,7 @@ import (
11 "net/http/httputil" 11 "net/http/httputil"
12 "net/url" 12 "net/url"
13 "strings" 13 "strings"
14 "time"
14) 15)
15 16
16const ( 17const (
@@ -19,6 +20,8 @@ const (
19 RFP_COOKIE_NAME string = "sso_rfp" 20 RFP_COOKIE_NAME string = "sso_rfp"
20) 21)
21 22
23// TODO: Enable https checks in HTTP client
24
22// acr_values can be mfa or selective_mfa (mfa only for external users) 25// acr_values can be mfa or selective_mfa (mfa only for external users)
23// mfa amr values: 26// mfa amr values:
24// pas - password 27// pas - password
@@ -34,6 +37,9 @@ type ProxyConfig struct {
34 UpstreamURL string 37 UpstreamURL string
35 ListenOn string 38 ListenOn string
36 TrustedCACert string 39 TrustedCACert string
40 PKISubject string // TODO: Should be same as IDP w/out scheme and port
41 ClockSkew time.Duration
42 MaxLiftetime time.Duration
37 IsOptional bool 43 IsOptional bool
38 RequestMFA bool 44 RequestMFA bool
39 AllowedMFAMethods []string // An OR set 45 AllowedMFAMethods []string // An OR set
@@ -45,7 +51,7 @@ type IdPConfig struct {
45 AuthorizationEndpoint string `json:"authorization_endpoint"` 51 AuthorizationEndpoint string `json:"authorization_endpoint"`
46 Issuer string `json:"issuer"` 52 Issuer string `json:"issuer"`
47 JwksUri string `json:"jwks_uri"` 53 JwksUri string `json:"jwks_uri"`
48 SupportedGrantTypes []string `json:"grant_types_supported"` 54 GrantTypes []string `json:"grant_types_supported"`
49 IdTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"` 55 IdTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"`
50 ResponseModes []string `json:"response_modes_supported"` 56 ResponseModes []string `json:"response_modes_supported"`
51 ResponseTypes []string `json:"response_types_supported"` 57 ResponseTypes []string `json:"response_types_supported"`
@@ -53,6 +59,7 @@ type IdPConfig struct {
53 SubjectTypes []string `json:"subject_types_supported"` 59 SubjectTypes []string `json:"subject_types_supported"`
54} 60}
55 61
62// TODO: Optimization to fetch only if expired (per http headers)
56func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) { 63func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) {
57 u, err := url.Parse(idp_url) 64 u, err := url.Parse(idp_url)
58 if err != nil { 65 if err != nil {
@@ -70,7 +77,7 @@ func FetchIdPConfig(h CautiousHTTPClient, idp_url string) (*IdPConfig, error) {
70} 77}
71 78
72// TODO: Optimization to fetch only if expired (per http headers) 79// TODO: Optimization to fetch only if expired (per http headers)
73func FetchJWKS(h CautiousHTTPClient, jwks_url string) (map[string]jose.JSONWebKey, error) { 80func FetchJWKS(h CautiousHTTPClient, jwks_url string, val KeyValidator) (map[string]jose.JSONWebKey, error) {
74 var jwks jose.JSONWebKeySet 81 var jwks jose.JSONWebKeySet
75 err := h.GetJSON(jwks_url, &jwks) 82 err := h.GetJSON(jwks_url, &jwks)
76 if err != nil { 83 if err != nil {
@@ -80,20 +87,15 @@ func FetchJWKS(h CautiousHTTPClient, jwks_url string) (map[string]jose.JSONWebKe
80 keys := make(map[string]jose.JSONWebKey, len(jwks.Keys)) 87 keys := make(map[string]jose.JSONWebKey, len(jwks.Keys))
81 88
82 for _, k := range jwks.Keys { 89 for _, k := range jwks.Keys {
83 keys[k.KeyID] = k 90 err = val.Validate(k)
91 if err == nil {
92 keys[k.KeyID] = k
93 }
84 } 94 }
85 95
86 return keys, nil 96 return keys, nil
87} 97}
88 98
89func URLMustParse(u string) *url.URL {
90 o, err := url.Parse(u)
91 if err != nil {
92 panic(err)
93 }
94 return o
95}
96
97func GenerateNonce() (string, error) { 99func GenerateNonce() (string, error) {
98 nonce := make([]byte, NONCE_SIZE) 100 nonce := make([]byte, NONCE_SIZE)
99 n, err := rand.Read(nonce) 101 n, err := rand.Read(nonce)
@@ -103,10 +105,6 @@ func GenerateNonce() (string, error) {
103 return hex.EncodeToString(nonce), nil 105 return hex.EncodeToString(nonce), nil
104} 106}
105 107
106func CompareUpper(lhs, rhs string) bool {
107 return strings.ToUpper(lhs) == strings.ToUpper(rhs)
108}
109
110// TODO 108// TODO
111// Cookie rules 109// Cookie rules
112// Secure 110// Secure
@@ -117,28 +115,6 @@ func SetCookie() {
117} 115}
118 116
119// TODO 117// TODO
120// Fetch (connect timeout 1s, read timeout 30s, read size 1M)
121func DownloadCertificate() {
122}
123
124// TODO
125// Fetch (connect timeout 1s, read timeout 30s, read size 1M)
126func DownloadCRL() {
127}
128
129// TODO
130// Cert validation
131// Validate cert not in CRL
132// Validate cert chains to trusted CA cert (ship with proxy)
133// Validate CRL signed by trusted CA
134// Cert "Subject CN " must match exactly PKI setting (ex: foo-pki.foo.com)
135// Current time bust be within "Validity Not Before" and "Validity Not After" in cert +- 5 minutes
136// Cert Key length >= 2048
137// Certificate usage must include "digitalSignature"
138func ValidateCertificate() {
139}
140
141// TODO
142func MakeClientID(r *http.Request) string { 118func MakeClientID(r *http.Request) string {
143 if strings.Contains(r.Host, ":") { 119 if strings.Contains(r.Host, ":") {
144 return r.Host 120 return r.Host
@@ -167,42 +143,6 @@ func SetTokenCookieAndRedirect(w http.ResponseWriter, r *http.Request, token str
167} 143}
168 144
169// TODO 145// TODO
170// Occasionally refresh IDP config (per HTTP caching headers)
171//
172// Fetch ${IDP_HOST}/.well-known/openid-configuration
173// - validate certificate chains to a trusted root
174// - validate scopes_supported contains "openid"
175// - validate response_types_supported contains "id_token"
176// - validate grant_types_supported contains "implicit"
177// - validate id_token_signing_alg_values_supported contains a supported signing type (see below)
178// - Cache authorization_endpoint for redirecting users
179//
180// Fetch jwks_uri endpoint
181// - Build key map indexed by kid for all keys that are suppored by our rules
182// - kty == RSA
183// - alg header must be one of [PS256, PS385, PS512]
184// - pem decode x5c and validate the certificate chain as below
185// - validate first item of x5c matches n and e
186func RefreshIDPConfig() {
187}
188
189// TODO
190// If x5u exists in header
191// Fetch cert from x5u URL
192// Get CRL from cert, fetch (connect timeout 1s, read timeout 30s, read size 1M)
193//
194// exp claim has passed +- 5 minutes
195// iat claim is greater than 24 hours +- 5 minutes
196// aud claim is exact match for client_id
197// iss claim is exact match for idp (ex: foo.example.com)
198// if other aud claims validate that they are known
199// nonce in JWT must be SHA256 of rfp cookie value
200// Validate cert
201// alg jwt header must be one of [PS256, PS385, PS512]
202// typ jwt header must be JWS
203// validate jwt signature
204// validate amr claim contains requested acr values (selective_mfa will be just mfa)
205// validate acr claim is the same as requested acr_values
206func ValidateJWT(jwt, rfp string) bool { 146func ValidateJWT(jwt, rfp string) bool {
207 return true 147 return true
208} 148}
@@ -318,33 +258,51 @@ func LoginController(w http.ResponseWriter, r *http.Request) {
318// user will be redirected back to the main page for the site (/) 258// user will be redirected back to the main page for the site (/)
319func parseConfig() *ProxyConfig { 259func parseConfig() *ProxyConfig {
320 return &ProxyConfig{ 260 return &ProxyConfig{
321 IDProviderURL: "", 261 IDProviderURL: "http://mcrute-virt:9993",
322 ClientID: "", 262 ClientID: "test.crute.me:443",
323 UpstreamURL: "http://localhost:9991/", 263 UpstreamURL: "http://localhost:9991/",
324 ListenOn: ":9992", 264 ListenOn: ":9992",
325 TrustedCACert: "", 265 TrustedCACert: "/home/mcrute/oidc_project/test_ca/ca_cert.pem",
326 IsOptional: false, 266 IsOptional: false,
267 PKISubject: "Crute OpenID Signing 1",
268 MaxLiftetime: 24 * time.Hour,
269 ClockSkew: 5 * time.Minute,
327 } 270 }
328} 271}
329 272
330func main() { 273func main() {
274 cfg := parseConfig()
331 h := NewCautiousHTTPClient() 275 h := NewCautiousHTTPClient()
332 276
333 idpc, err := FetchIdPConfig(h, "http://mcrute-virt:9993") 277 v := NewKeyValidator(cfg.PKISubject)
278 v.LoadRootPEM(cfg.TrustedCACert)
279
280 idpc, err := FetchIdPConfig(h, cfg.IDProviderURL)
334 if err != nil { 281 if err != nil {
335 fmt.Printf("%s\n", err) 282 fmt.Printf("%s\n", err)
336 return 283 return
337 } 284 }
338 285
339 jwks, err := FetchJWKS(h, idpc.JwksUri) 286 jwks, err := FetchJWKS(h, idpc.JwksUri, v)
340 if err != nil { 287 if err != nil {
341 fmt.Printf("%s\n", err) 288 fmt.Printf("%s\n", err)
342 return 289 return
343 } 290 }
344 fmt.Printf("%+v\n", jwks) 291
292 jv := NewJWSValidator(jwks, idpc.Issuer, cfg.ClientID, cfg.ClockSkew, cfg.MaxLiftetime)
293
294 nonce := "ofspmfjuvoswhhde"
295 raw_jwt := "eyJ0eXAiOiJKV1MiLCJhbGciOiJQUzI1NiIsImtpZCI6IjEifQ.eyJub25jZSI6IjM0MjlhMjAyYzU4ZDkyYjQwNjNjOWM4MWM2MjQyNGRlNzBkMmIzZDQ4MmVlNDFhOTdjYmNhZjEwZDk5MWFiOTMiLCJpc3MiOiJpZHAuY3J1dGUubWU6NDQzIiwiaWF0IjoxNTA0NTc2Mzc0LCJuYmYiOjE1MDQ1NzYzNzQsImV4cCI6MTUwNDY2Mjc3NCwic3ViIjoibWNydXRlIiwiYXVkIjoidGVzdC5jcnV0ZS5tZTo0NDMifQ.iizlNfY1Vg7d-XRmgyYuhpNkNrOGaT9OOgO0HdjBozOWMvKzBTtATbIfoWOrNH6DiFY1as8uy3I1Pxnkrb8Ti8_cLDQeLxOv9klAbnebeuPI_wtZ0iwSUnSWaYzN6I6sqcEjHX3fibFvAQhO5dNDzSwONjw4AvcdpZKh579FO1sAvIw-1DmMyPSUun7rbC0Kf1Jtdlr3q7tOp3wdI_erkstxCNPwyuv7X1J7uetsu0BeJS25C2DxeB03BPEIUoo_C1xvcqikfSLLpoFcyToYiS-R9o-WpRjGid_yug65J5ALn2aM3vhe9rRbydKVm_omGL8-Etj06zbqM0Y6OrJUgA"
296 claims, err := jv.Validate(raw_jwt, nonce)
297 if err != nil {
298 fmt.Printf("Error validating: %s\n", err)
299 return
300 }
301
302 fmt.Printf("Valid JWT for: %+v\n", claims.Subject)
303
345 return 304 return
346 305
347 cfg := parseConfig()
348 cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL)) 306 cfg.reverseProxy = httputil.NewSingleHostReverseProxy(URLMustParse(cfg.UpstreamURL))
349 307
350 if cfg.IsOptional { 308 if cfg.IsOptional {
diff --git a/oidc_proxy b/oidc_proxy
new file mode 100755
index 0000000..e5df267
--- /dev/null
+++ b/oidc_proxy
Binary files differ
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..10709e2
--- /dev/null
+++ b/util.go
@@ -0,0 +1,43 @@
1package main
2
3import (
4 "net/url"
5 "strings"
6)
7
8type stringSet struct {
9 values map[string]bool
10}
11
12func NewStringSet(values ...string) *stringSet {
13 s := &stringSet{
14 values: make(map[string]bool, len(values)),
15 }
16
17 for _, v := range values {
18 s.Add(v)
19 }
20
21 return s
22}
23
24func (s *stringSet) Add(v string) {
25 s.values[v] = true
26}
27
28func (s *stringSet) Contains(k string) bool {
29 _, ok := s.values[k]
30 return ok
31}
32
33func URLMustParse(u string) *url.URL {
34 o, err := url.Parse(u)
35 if err != nil {
36 panic(err)
37 }
38 return o
39}
40
41func CompareUpper(lhs, rhs string) bool {
42 return strings.ToUpper(lhs) == strings.ToUpper(rhs)
43}