|
|
@ -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,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, |
|
|
|
)) |
|
|
|
} |