switch to keypairs
This commit is contained in:
parent
66e0639f48
commit
3ab579ad24
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
"d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
|
||||
"x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
|
||||
|
|
28
mockid.go
28
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)
|
||||
|
|
183
mockid/mockid.go
183
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 <protected-header>.<body>.<signature>", http.StatusBadRequest)
|
||||
http.Error(w, "Bad Format: token should be in the format of <protected-header>.<payload>.<signature>", 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,7 +505,22 @@ 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[:])
|
||||
sig := JOSESign(privkey, hash[:])
|
||||
sig64 := base64.RawURLEncoding.EncodeToString(sig)
|
||||
token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
|
||||
return protected, payload, token
|
||||
}
|
||||
|
||||
// TODO: move to keypairs
|
||||
|
||||
func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
|
||||
var sig []byte
|
||||
|
||||
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...)
|
||||
|
@ -490,38 +529,9 @@ func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, st
|
|||
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,
|
||||
sig = append(rb, sb...)
|
||||
}
|
||||
|
||||
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[:])
|
||||
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,
|
||||
))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue