diff --git a/default.jwk.json b/default.jwk.json index ab40e44..16d79e1 100644 --- a/default.jwk.json +++ b/default.jwk.json @@ -1,4 +1,5 @@ { + "kty": "EC", "crv": "P-256", "d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs", "x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ", diff --git a/mockid.go b/mockid.go index b03a19d..bf905ef 100644 --- a/mockid.go +++ b/mockid.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "flag" "fmt" "io/ioutil" @@ -12,6 +11,7 @@ import ( "strconv" "git.coolaj86.com/coolaj86/go-mockid/mockid" + "git.rootprojects.org/root/keypairs" _ "github.com/joho/godotenv/autoload" ) @@ -44,24 +44,13 @@ func main() { return } - jwkm := map[string]string{} - err = json.Unmarshal(jwkb, &jwkm) + privkey, err := keypairs.ParseJWKPrivateKey(jwkb) if nil != err { // TODO delete the bad file? panic(fmt.Errorf("unmarshal jwk %v: %w", string(jwkb), err)) return } - jwk := &mockid.PrivateJWK{ - PublicJWK: mockid.PublicJWK{ - Crv: jwkm["crv"], - X: jwkm["x"], - Y: jwkm["y"], - }, - D: jwkm["d"], - } - priv := mockid.ParseKey(jwk) - if nil != urlFlag && "" != *urlFlag { host = *urlFlag } else { @@ -80,7 +69,7 @@ func main() { os.Exit(1) } - mockid.Route(jwksPrefix, priv, jwk) + mockid.Route(jwksPrefix, privkey) fs := http.FileServer(http.Dir("./public")) http.Handle("/", fs) @@ -97,11 +86,12 @@ func main() { done <- true }() - b, _ := json.Marshal(jwk) - fmt.Printf("Private Key:\n\t%s\n", string(b)) - b, _ = json.Marshal(jwk.PublicJWK) - fmt.Printf("Public Key:\n\t%s\n", string(b)) - protected, payload, token := mockid.GenToken(host, priv, url.Values{}) + // TODO privB := keypairs.MarshalJWKPrivateKey(privkey) + privB := mockid.MarshalJWKPrivateKey(privkey) + fmt.Printf("Private Key:\n\t%s\n", string(privB)) + pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public())) + fmt.Printf("Public Key:\n\t%s\n", string(pubB)) + protected, payload, token := mockid.GenToken(host, privkey, url.Values{}) fmt.Printf("Protected (Header):\n\t%s\n", protected) fmt.Printf("Payload (Claims):\n\t%s\n", payload) fmt.Printf("Access Token:\n\t%s\n", token) diff --git a/mockid/mockid.go b/mockid/mockid.go index 086efb9..26f745c 100644 --- a/mockid/mockid.go +++ b/mockid/mockid.go @@ -2,8 +2,8 @@ package mockid import ( "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/sha256" "crypto/sha512" "encoding/base64" @@ -21,13 +21,9 @@ import ( "git.rootprojects.org/root/keypairs" "git.rootprojects.org/root/keypairs/keyfetch" + //jwt "github.com/dgrijalva/jwt-go" ) -type PrivateJWK struct { - PublicJWK - D string `json:"d"` -} - type PublicJWK struct { Crv string `json:"crv"` KeyID string `json:"kid,omitempty"` @@ -36,15 +32,34 @@ type PublicJWK struct { Y string `json:"y"` } +type InspectableToken struct { + Public keypairs.PublicKey `json:"public"` + Protected map[string]interface{} `json:"protected"` + Payload map[string]interface{} `json:"payload"` + Signature string `json:"signature"` + Verified bool `json:"verified"` + Errors []string `json:"errors"` +} + +func (t *InspectableToken) MarshalJSON() ([]byte, error) { + pub := keypairs.MarshalJWKPublicKey(t.Public) + header, _ := json.Marshal(t.Protected) + payload, _ := json.Marshal(t.Payload) + errs, _ := json.Marshal(t.Errors) + return []byte(fmt.Sprintf( + `{"public":%s,"protected":%s,"payload":%s,"signature":%q,"verified":%t,"errors":%s}`, + pub, header, payload, t.Signature, t.Verified, errs, + )), nil +} + 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) +func Route(jwksPrefix string, privkey keypairs.PrivateKey) { + pubkey := keypairs.NewPublicKey(privkey.Public()) http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) { baseURL := getBaseURL(r) @@ -110,7 +125,7 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { 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()) + _, _, token := GenToken(getBaseURL(r), privkey, r.URL.Query()) fmt.Fprintf(w, token) }) @@ -135,7 +150,7 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { parts := strings.Split(token, ".") if 3 != len(parts) { - http.Error(w, "Bad Format: token should be in the format of ..", http.StatusBadRequest) + http.Error(w, "Bad Format: token should be in the format of ..", http.StatusBadRequest) return } protected64 := parts[0] @@ -149,7 +164,7 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { } 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) + http.Error(w, "Bad Format: token's payload should be URL-safe base64 encoded", http.StatusBadRequest) return } // TODO verify signature @@ -177,12 +192,12 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { 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) + http.Error(w, "Bad Format: token's payload 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") + errors = append(errors, "payload.iss must exist to complement header.kid") } pub, err := keyfetch.OIDCJWK(kid, iss) @@ -193,23 +208,16 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { 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"` - }{ + inspected := &InspectableToken{ Public: pub, Protected: protected, - Body: data, + Payload: data, Signature: signature64, Verified: false, Errors: errors, } - tokenB, err := json.Marshal(inspected) + tokenB, err := json.MarshalIndent(inspected, "", " ") if nil != err { fmt.Println("couldn't serialize inpsected token:") fmt.Println(err) @@ -239,13 +247,19 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { prefix = prefixes[0] } - _, _, token := GenToken(getBaseURL(r), priv, r.URL.Query()) + _, _, token := GenToken(getBaseURL(r), privkey, 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) + jwk := string(MarshalJWKPrivateKey(privkey)) + jwk = strings.Replace(jwk, `{"`, `{ "`, 1) + jwk = strings.Replace(jwk, `",`, `", `, -1) + jwk = jwk[0 : len(jwk)-1] + jwk = jwk + `, "ext": true , "key_ops": ["sign"] }` + // `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q }`, jwk.Crv, jwk.D, jwk.X, jwk.Y + fmt.Fprintf(w, jwk) }) http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { @@ -262,10 +276,14 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) { 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), - ) + exp := strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10) + jwk := string(keypairs.MarshalJWKPublicKey(pubkey)) + jwk = strings.Replace(jwk, `{"`, `{ "`, 1) + jwk = strings.Replace(jwk, `",`, `" ,`, -1) + jwk = jwk[0 : len(jwk)-1] + jwk = jwk + fmt.Sprintf(`, "ext": true , "key_ops": ["verify"], "exp": %s }`, exp) + // { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } + jwkstr := fmt.Sprintf(`{ "keys": [ %s ] }`, jwk) fmt.Println(jwkstr) fmt.Fprintf(w, jwkstr) return @@ -462,9 +480,15 @@ func postRSA(jwksPrefix string, tok map[string]interface{}, w http.ResponseWrite ))) } -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) +func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (string, string, string) { + thumbprint := keypairs.ThumbprintPublicKey(keypairs.NewPublicKey(privkey.Public())) + // TODO keypairs.Alg(key) + alg := "ES256" + switch privkey.(type) { + case *rsa.PrivateKey: + alg = "RS256" + } + protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint) protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected)) exp, err := parseExp(query.Get("exp")) @@ -481,47 +505,33 @@ func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, st 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...)) + sig := JOSESign(privkey, hash[:]) + sig64 := base64.RawURLEncoding.EncodeToString(sig) 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, - } +// TODO: move to keypairs - db, _ := base64.RawURLEncoding.DecodeString(jwk.D) - di := &big.Int{} - di.SetBytes(db) - priv := &ecdsa.PrivateKey{ - PublicKey: *pub, - D: di, - } - return priv -} +func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte { + var sig []byte -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[:]) + switch k := privkey.(type) { + case *rsa.PrivateKey: + panic("TODO: implement rsa sign") + case *ecdsa.PrivateKey: + r, s, _ := ecdsa.Sign(rand.Reader, k, 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...) + } + sig = append(rb, sb...) + } + return sig } func issueNonce(w http.ResponseWriter, r *http.Request) { @@ -567,3 +577,46 @@ func getBaseURL(r *http.Request) string { r.Host, ) } + +// MarshalJWKPrivateKey outputs the given private key as JWK +func MarshalJWKPrivateKey(privkey keypairs.PrivateKey) []byte { + // thumbprint keys are alphabetically sorted and only include the necessary public parts + switch k := privkey.(type) { + case *rsa.PrivateKey: + return MarshalRSAPrivateKey(k) + case *ecdsa.PrivateKey: + return MarshalECPrivateKey(k) + default: + // this is unreachable because we know the types that we pass in + log.Printf("keytype: %t, %+v\n", privkey, privkey) + panic(keypairs.ErrInvalidPublicKey) + } +} + +// MarshalECPrivateKey will output the given private key as JWK +func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte { + crv := k.Curve.Params().Name + d := base64.RawURLEncoding.EncodeToString(k.D.Bytes()) + x := base64.RawURLEncoding.EncodeToString(k.X.Bytes()) + y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes()) + return []byte(fmt.Sprintf( + `{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`, + crv, d, x, y, + )) +} + +// MarshalRSAPrivateKey will output the given private key as JWK +func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte { + e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes()) + n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes()) + d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes()) + p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes()) + q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes()) + dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes()) + dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes()) + qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes()) + return []byte(fmt.Sprintf( + `{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`, + d, dp, dq, e, n, p, q, qi, + )) +}