package mockid import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "log" "math/big" "net/http" "net/url" "path/filepath" "strconv" "strings" "time" "git.rootprojects.org/root/keypairs" "git.rootprojects.org/root/keypairs/keyfetch" ) type PrivateJWK struct { PublicJWK D string `json:"d"` } type PublicJWK struct { Crv string `json:"crv"` KeyID string `json:"kid,omitempty"` Kty string `json:"kty,omitempty"` X string `json:"x"` Y string `json:"y"` } var nonces map[string]int64 func init() { nonces = make(map[string]int64) } func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { pub := &priv.PublicKey thumbprint := thumbprintKey(pub) http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) { baseURL := getBaseURL(r) /* res.statusCode = 200; res.setHeader("Cache-Control", "max-age=0, no-cache, no-store"); // TODO //res.setHeader("Date", "Sun, 10 Mar 2019 08:04:45 GMT"); // is this the expiration of the nonce itself? methinks maybe so //res.setHeader("Expires", "Sun, 10 Mar 2019 08:04:45 GMT"); // TODO use one of the registered domains //var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index" */ //var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined); //var indexUrl = "http://localhost:" + port + "/index"; indexUrl := baseURL + "/index" w.Header().Set("Link", "<"+indexUrl+">;rel=\"index\"") w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store") w.Header().Set("Pragma", "no-cache") //res.setHeader("Strict-Transport-Security", "max-age=604800"); w.Header().Set("X-Frame-Options", "DENY") issueNonce(w, r) }) http.HandleFunc("/api/new-account", requireNonce(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not Implemented", http.StatusNotImplemented) })) 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) switch kty { case "EC": postEC(jwksPrefix, tok, w, r) case "RSA": postRSA(jwksPrefix, tok, w, r) default: http.Error(w, "Bad Request: only EC and RSA keys are supported", http.StatusBadRequest) return } }) http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s\n", r.Method, r.URL.Path) _, _, token := GenToken(getBaseURL(r), priv, r.URL.Query()) fmt.Fprintf(w, token) }) http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") log.Printf("%s %s %s\n", r.Method, r.URL.Path, token) if "" == token { token = r.URL.Query().Get("access_token") if "" == token { http.Error(w, "Bad Format: missing Authorization header and 'access_token' query", http.StatusBadRequest) return } } else { parts := strings.Split(token, " ") if 2 != len(parts) { http.Error(w, "Bad Format: expected Authorization header to be in the format of 'Bearer '", http.StatusBadRequest) return } token = parts[1] } parts := strings.Split(token, ".") if 3 != len(parts) { http.Error(w, "Bad Format: token should be in the format of ..", http.StatusBadRequest) return } protected64 := parts[0] data64 := parts[1] signature64 := parts[2] protectedB, err := base64.RawURLEncoding.DecodeString(protected64) if nil != err { http.Error(w, "Bad Format: token's header should be URL-safe base64 encoded", http.StatusBadRequest) return } dataB, err := base64.RawURLEncoding.DecodeString(data64) if nil != err { http.Error(w, "Bad Format: token's body should be URL-safe base64 encoded", http.StatusBadRequest) return } // TODO verify signature _, err = base64.RawURLEncoding.DecodeString(signature64) if nil != err { http.Error(w, "Bad Format: token's signature should be URL-safe base64 encoded", http.StatusBadRequest) return } errors := []string{} protected := map[string]interface{}{} err = json.Unmarshal(protectedB, &protected) if nil != err { http.Error(w, "Bad Format: token's header should be URL-safe base64-encoded JSON", http.StatusBadRequest) return } kid, kidOK := protected["kid"].(string) // TODO parse jwkM _, jwkOK := protected["jwk"] if !kidOK && !jwkOK { errors = append(errors, "must have either header.kid or header.jwk") } data := map[string]interface{}{} err = json.Unmarshal(dataB, &data) if nil != err { http.Error(w, "Bad Format: token's body should be URL-safe base64-encoded JSON", http.StatusBadRequest) return } iss, issOK := data["iss"].(string) if !jwkOK && !issOK { errors = append(errors, "body.iss must exist to complement header.kid") } pub, err := keyfetch.OIDCJWK(kid, iss) if nil != err { fmt.Println("couldn't fetch pub key:") fmt.Println(err) } fmt.Println("fetched pub key:") fmt.Println(pub) inspected := struct { Public keypairs.PublicKey `json:"public"` Protected map[string]interface{} `json:"protected"` Body map[string]interface{} `json:"body"` Signature string `json:"signature"` Verified bool `json:"verified"` Errors []string `json:"errors"` }{ Public: pub, Protected: protected, Body: data, Signature: signature64, Verified: false, Errors: errors, } tokenB, err := json.Marshal(inspected) if nil != err { fmt.Println("couldn't serialize inpsected token:") fmt.Println(err) } fmt.Println("serialized inpsected token") fmt.Println(inspected) fmt.Println(string(tokenB)) fmt.Fprintf(w, string(tokenB)) }) http.HandleFunc("/authorization_header", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s\n", r.Method, r.URL.Path) var header string headers, _ := r.URL.Query()["header"] if 0 == len(headers) { header = "Authorization" } else { header = headers[0] } var prefix string prefixes, _ := r.URL.Query()["prefix"] if 0 == len(prefixes) { prefix = "Bearer " } else { prefix = prefixes[0] } _, _, token := GenToken(getBaseURL(r), 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) { baseURL := getBaseURL(r) log.Printf("%s %s\n", r.Method, r.URL.Path) fmt.Fprintf(w, `{ "issuer": "%s", "jwks_uri": "%s/.well-known/jwks.json" }`, baseURL, baseURL) }) 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) }) } func parseExp(exp string) (int, error) { if "" == exp { exp = "15m" } mult := 1 switch exp[len(exp)-1] { case 'w': mult *= 7 fallthrough case 'd': mult *= 24 fallthrough case 'h': mult *= 60 fallthrough case 'm': mult *= 60 fallthrough case 's': // no fallthrough default: // could be 'k' or 'z', but we assume its empty exp += "s" } num, err := strconv.Atoi(exp[:len(exp)-1]) if nil != err { return 0, err } return num * mult, nil } func postEC(jwksPrefix string, tok map[string]interface{}, w http.ResponseWriter, r *http.Request) { 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 } thumbprintable := []byte( fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y), ) alg := crv[2:] var thumb []byte switch alg { case "256": hash := sha256.Sum256(thumbprintable) thumb = hash[:] case "384": hash := sha512.Sum384(thumbprintable) thumb = hash[:] case "521": fallthrough case "512": hash := sha512.Sum512(thumbprintable) thumb = hash[:] default: http.Error(w, "Bad Request: bad key length or 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 } pub := []byte(fmt.Sprintf( `{"crv":%q,"kid":%q,"kty":"EC","x":%q,"y":%q}`, crv, kid, x, y, )) // 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 } if err := ioutil.WriteFile( filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"), pub, 0644, ); nil != err { fmt.Println("can't write file") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } baseURL := getBaseURL(r) w.Write([]byte(fmt.Sprintf( `{ "iss":%q, "jwks_url":%q }`, baseURL+"/", baseURL+"/.well-known/jwks.json", ))) } func postRSA(jwksPrefix string, tok map[string]interface{}, w http.ResponseWriter, r *http.Request) { e, ok := tok["e"].(string) if !ok { http.Error(w, "Bad Request: missing 'e'", http.StatusBadRequest) return } n, ok := tok["n"].(string) if !ok { http.Error(w, "Bad Request: missing 'n'", http.StatusBadRequest) return } thumbprintable := []byte( fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n), ) var thumb []byte // TODO handle bit lengths well switch 3 * (len(n) / 4.0) { case 256: hash := sha256.Sum256(thumbprintable) thumb = hash[:] case 384: hash := sha512.Sum384(thumbprintable) thumb = hash[:] case 512: hash := sha512.Sum512(thumbprintable) thumb = hash[:] default: http.Error(w, "Bad Request: only standard RSA key lengths (2048, 3072, 4096) are supported", 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 } pub := []byte(fmt.Sprintf( `{"e":%q,"kid":%q,"kty":"EC","n":%q}`, e, kid, n, )) // 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 } if err := ioutil.WriteFile( filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"), pub, 0644, ); nil != err { fmt.Println("can't write file") http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } baseURL := getBaseURL(r) w.Write([]byte(fmt.Sprintf( `{ "iss":%q, "jwks_url":%q }`, baseURL+"/", baseURL+"/.well-known/jwks.json", ))) } func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, string, string) { thumbprint := thumbprintKey(&priv.PublicKey) protected := fmt.Sprintf(`{"typ":"JWT","alg":"ES256","kid":"%s"}`, thumbprint) protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected)) exp, err := parseExp(query.Get("exp")) if nil != err { // cryptic error code // TODO propagate error exp = 422 } payload := fmt.Sprintf( `{"iss":"%s/","sub":"dummy","exp":%s}`, host, strconv.FormatInt(time.Now().Add(time.Duration(exp)*time.Second).Unix(), 10), ) payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload)) hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64))) r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:]) rb := r.Bytes() for len(rb) < 32 { rb = append([]byte{0}, rb...) } sb := s.Bytes() for len(rb) < 32 { sb = append([]byte{0}, sb...) } sig64 := base64.RawURLEncoding.EncodeToString(append(rb, sb...)) token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64) return protected, payload, token } func ParseKey(jwk *PrivateJWK) *ecdsa.PrivateKey { xb, _ := base64.RawURLEncoding.DecodeString(jwk.X) xi := &big.Int{} xi.SetBytes(xb) yb, _ := base64.RawURLEncoding.DecodeString(jwk.Y) yi := &big.Int{} yi.SetBytes(yb) pub := &ecdsa.PublicKey{ Curve: elliptic.P256(), X: xi, Y: yi, } db, _ := base64.RawURLEncoding.DecodeString(jwk.D) di := &big.Int{} di.SetBytes(db) priv := &ecdsa.PrivateKey{ PublicKey: *pub, D: di, } return priv } func thumbprintKey(pub *ecdsa.PublicKey) string { minpub := []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, "P-256", pub.X, pub.Y)) sha := sha256.Sum256(minpub) return base64.RawURLEncoding.EncodeToString(sha[:]) } func issueNonce(w http.ResponseWriter, r *http.Request) { b := make([]byte, 16) _, _ = rand.Read(b) nonce := base64.RawURLEncoding.EncodeToString(b) nonces[nonce] = time.Now().Unix() w.Header().Set("Replay-Nonce", nonce) } func requireNonce(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { nonce := r.Header.Get("Replay-Nonce") // TODO expire nonces every so often t := nonces[nonce] if 0 == t { http.Error( w, `{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`, http.StatusBadRequest, ) return } delete(nonces, nonce) issueNonce(w, r) next(w, r) } } func getBaseURL(r *http.Request) string { var scheme string if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") { scheme = "https:" } else { scheme = "http:" } return fmt.Sprintf( "%s//%s", scheme, r.Host, ) }