From 364de7114ae8b767af24d41c6310821557b58d04 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 19 Aug 2019 03:34:19 +0000 Subject: [PATCH] add ability to post public keys --- mockid.go | 170 +++++++++++++++++++++++++++++++++++++++++++--- public/index.html | 5 ++ 2 files changed, 166 insertions(+), 9 deletions(-) diff --git a/mockid.go b/mockid.go index a79cc71..6e2fbba 100644 --- a/mockid.go +++ b/mockid.go @@ -5,16 +5,20 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/sha256" + "crypto/sha512" "encoding/base64" "encoding/json" "flag" "fmt" + "io/ioutil" "log" "math/big" "net/http" "net/url" "os" + "path/filepath" "strconv" + "strings" "time" ) @@ -23,11 +27,15 @@ type PrivateJWK struct { D string `json:"d"` } type PublicJWK struct { - Crv string `json:"crv"` - X string `json:"x"` - Y string `json:"y"` + Crv string `json:"crv"` + KeyID string `json:"kid,omitempty"` + Kty string `json:"kty,omitempty"` + X string `json:"x"` + Y string `json:"y"` } +var jwksPrefix string + func main() { done := make(chan bool) var port int @@ -53,6 +61,7 @@ func main() { portFlag := flag.Int("port", 0, "Port on which the HTTP server should run") urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com") + prefixFlag := flag.String("jwkspath", "", "The path to the JWKs storage directory") flag.Parse() if nil != portFlag && *portFlag > 0 { @@ -72,6 +81,121 @@ func main() { host = "http://localhost:" + strconv.Itoa(port) } + if nil != prefixFlag && "" != *prefixFlag { + jwksPrefix = *prefixFlag + } else { + jwksPrefix = "public-jwks" + } + err := os.MkdirAll(jwksPrefix, 0755) + if nil != err { + fmt.Fprintf(os.Stderr, "couldn't write %q: %s", jwksPrefix, err) + os.Exit(1) + } + + http.HandleFunc("/api/jwks", func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path) + if "POST" != r.Method { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + tok := make(map[string]interface{}) + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&tok) + if nil != err { + http.Error(w, "Bad Request: invalid json", http.StatusBadRequest) + return + } + defer r.Body.Close() + + // TODO better, JSON error messages + if _, ok := tok["d"]; ok { + http.Error(w, "Bad Request: private key", http.StatusBadRequest) + return + } + + kty, _ := tok["kty"].(string) + if "EC" != kty { + http.Error(w, "Bad Request: only EC keys are supported", http.StatusBadRequest) + return + } + + crv, ok := tok["crv"].(string) + if 5 != len(crv) || "P-" != crv[:2] { + http.Error(w, "Bad Request: bad curve", http.StatusBadRequest) + return + } + x, ok := tok["x"].(string) + if !ok { + http.Error(w, "Bad Request: missing 'x'", http.StatusBadRequest) + return + } + y, ok := tok["y"].(string) + if !ok { + http.Error(w, "Bad Request: missing 'y'", http.StatusBadRequest) + return + } + + // TODO RSA + thumbprintable := []byte( + fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y), + ) + + var thumb []byte + switch crv[2:] { + case "256": + hash := sha256.Sum256(thumbprintable) + thumb = hash[:] + case "384": + hash := sha512.Sum384(thumbprintable) + thumb = hash[:] + case "521": + hash := sha512.Sum512(thumbprintable) + thumb = hash[:] + default: + http.Error(w, "Bad Request: bad curve", http.StatusBadRequest) + return + } + + kid := base64.RawURLEncoding.EncodeToString(thumb) + if kid2, _ := tok["kid"].(string); "" != kid2 && kid != kid2 { + http.Error(w, "Bad Request: kid should be "+kid, http.StatusBadRequest) + return + } + + // TODO allow posting at the top-level? + // TODO support a group of keys by PPID + // (right now it's only by KID) + if !strings.HasPrefix(r.Host, strings.ToLower(kid)+".") { + http.Error(w, "Bad Request: prefix should be "+kid, http.StatusBadRequest) + return + } + + pub := []byte(fmt.Sprintf( + `{"crv":%q,"kid":%q,"kty":"EC","x":%q,"y":%q}`, crv, kid, x, y, + )) + err = ioutil.WriteFile( + filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"), + pub, + 0644, + ) + if nil != err { + fmt.Println("can't write file") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + var scheme string + if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") { + scheme = "https://" + } else { + scheme = "http://" + } + w.Write([]byte(fmt.Sprintf( + `{ "iss":%q, "jwks_url":%q }`, scheme+r.Host+"/", scheme+r.Host+"/.well-known/jwks.json", + ))) + }) + http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s\n", r.Method, r.URL.Path) var scheme string @@ -83,6 +207,7 @@ func main() { _, _, token := genToken(scheme+r.Host, priv, r.URL.Query()) fmt.Fprintf(w, token) }) + http.HandleFunc("/authorization_header", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s\n", r.Method, r.URL.Path) var scheme string @@ -111,10 +236,12 @@ func main() { _, _, token := genToken(scheme+r.Host, priv, r.URL.Query()) fmt.Fprintf(w, "%s: %s%s", header, prefix, token) }) + http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) fmt.Fprintf(w, `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q , "ext": true , "key_ops": ["sign"] }`, jwk.Crv, jwk.D, jwk.X, jwk.Y) }) + http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { var scheme string if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") { @@ -125,16 +252,41 @@ func main() { log.Printf("%s %s\n", r.Method, r.URL.Path) fmt.Fprintf(w, `{ "issuer": "%s", "jwks_uri": "%s/.well-known/jwks.json" }`, scheme+r.Host, scheme+r.Host) }) - http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s", r.Method, r.URL.Path) - jwkstr := fmt.Sprintf( - `{ "keys": [ { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } ] }`, - jwk.Crv, jwk.X, jwk.Y, thumbprint, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), - ) + http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s", r.Method, r.Host, r.URL.Path) + parts := strings.Split(r.Host, ".") + kid := parts[0] + + b, err := ioutil.ReadFile(filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json")) + if nil != err { + //http.Error(w, "Not Found", http.StatusNotFound) + jwkstr := fmt.Sprintf( + `{ "keys": [ { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } ] }`, + jwk.Crv, jwk.X, jwk.Y, thumbprint, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), + ) + fmt.Println(jwkstr) + fmt.Fprintf(w, jwkstr) + return + } + + tok := &PublicJWK{} + err = json.Unmarshal(b, tok) + if nil != err { + // TODO delete the bad file? + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + jwkstr := fmt.Sprintf( + `{ "keys": [ { "kty": "EC", "crv": %q, "x": %q, "y": %q, "kid": %q,`+ + ` "ext": true, "key_ops": ["verify"], "exp": %s } ] }`, + tok.Crv, tok.X, tok.Y, tok.KeyID, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), + ) fmt.Println(jwkstr) fmt.Fprintf(w, jwkstr) }) + fs := http.FileServer(http.Dir("public")) http.Handle("/", fs) /* diff --git a/public/index.html b/public/index.html index 3a4b122..95a0344 100644 --- a/public/index.html +++ b/public/index.html @@ -17,6 +17,11 @@ Compatible with * https://xxx.mock.pocketid.app/.well-known/jwks.json * https://mock.pocketid.app/key.jwk.json + * POST https://xxx.mock.pocketid.app/api/jwks + { "kty":"EC", "crv":"P-256", "x":"xxx", "y":"yyy" } + + * GET https://xxx.mock.pocketid.app/.well-known/jwks.json +

Get a token

Get a verifiable access token