AJ ONeal
4 years ago
7 changed files with 334 additions and 23 deletions
@ -0,0 +1,57 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"net/http" |
||||
|
|
||||
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs" |
||||
|
) |
||||
|
|
||||
|
// SignJWS will create an uncompressed JWT with the given payload
|
||||
|
func SignJWS(w http.ResponseWriter, r *http.Request) { |
||||
|
sign(w, r, false) |
||||
|
} |
||||
|
|
||||
|
// SignJWT will create an compressed JWS (JWT) with the given payload
|
||||
|
func SignJWT(w http.ResponseWriter, r *http.Request) { |
||||
|
sign(w, r, true) |
||||
|
} |
||||
|
|
||||
|
func sign(w http.ResponseWriter, r *http.Request, jwt bool) { |
||||
|
if "POST" != r.Method { |
||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
opts, err := getOpts(r) |
||||
|
if nil != err { |
||||
|
http.Error(w, err.Error(), http.StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
privkey, err := getPrivKey(opts) |
||||
|
if nil != err { |
||||
|
http.Error(w, err.Error(), http.StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
header := opts.Header |
||||
|
if 0 != opts.Seed { |
||||
|
header["_seed"] = opts.Seed |
||||
|
} |
||||
|
|
||||
|
jws, err := xkeypairs.SignClaims(privkey, header, opts.Claims) |
||||
|
if nil != err { |
||||
|
http.Error(w, err.Error(), http.StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
var b []byte |
||||
|
if jwt { |
||||
|
s := xkeypairs.JWSToJWT(jws) |
||||
|
w.Write(append([]byte(s), '\n')) |
||||
|
return |
||||
|
} |
||||
|
b, _ = json.Marshal(jws) |
||||
|
w.Write(append(b, '\n')) |
||||
|
} |
@ -0,0 +1,170 @@ |
|||||
|
package xkeypairs |
||||
|
|
||||
|
import ( |
||||
|
"crypto" |
||||
|
"crypto/ecdsa" |
||||
|
"crypto/rand" |
||||
|
"crypto/rsa" |
||||
|
"crypto/sha256" |
||||
|
"encoding/base64" |
||||
|
"encoding/json" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
mathrand "math/rand" |
||||
|
"time" |
||||
|
|
||||
|
"git.rootprojects.org/root/keypairs" |
||||
|
) |
||||
|
|
||||
|
var RandomReader = rand.Reader |
||||
|
|
||||
|
type JWS struct { |
||||
|
Header Object `json:"header"` // JSON
|
||||
|
Claims Object `json:"claims"` // JSON
|
||||
|
Protected string `json:"protected"` // base64
|
||||
|
Payload string `json:"payload"` // base64
|
||||
|
Signature string `json:"signature"` // base64
|
||||
|
} |
||||
|
|
||||
|
type Object = map[string]interface{} |
||||
|
|
||||
|
func SignClaims(privkey keypairs.PrivateKey, header Object, claims Object) (*JWS, error) { |
||||
|
var randsrc io.Reader = RandomReader |
||||
|
seed, _ := header["_seed"].(int64) |
||||
|
if 0 != seed { |
||||
|
randsrc = mathrand.New(mathrand.NewSource(seed)) |
||||
|
//delete(header, "_seed")
|
||||
|
} |
||||
|
|
||||
|
protected, err := headerToProtected(keypairs.NewPublicKey(privkey.Public()), header) |
||||
|
if nil != err { |
||||
|
return nil, err |
||||
|
} |
||||
|
protected64 := base64.RawURLEncoding.EncodeToString(protected) |
||||
|
|
||||
|
payload, err := claimsToPayload(claims) |
||||
|
if nil != err { |
||||
|
return nil, err |
||||
|
} |
||||
|
payload64 := base64.RawURLEncoding.EncodeToString(payload) |
||||
|
|
||||
|
hash := sha256.Sum256([]byte(fmt.Sprintf( |
||||
|
`%s.%s`, |
||||
|
protected64, |
||||
|
payload64, |
||||
|
))) |
||||
|
|
||||
|
sig := Sign(randsrc, privkey, hash[:]) |
||||
|
sig64 := base64.RawURLEncoding.EncodeToString(sig) |
||||
|
|
||||
|
return &JWS{ |
||||
|
Header: header, |
||||
|
Claims: claims, |
||||
|
Protected: protected64, |
||||
|
Payload: payload64, |
||||
|
Signature: sig64, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
func headerToProtected(pub keypairs.PublicKey, header Object) ([]byte, error) { |
||||
|
if nil == header { |
||||
|
header = Object{} |
||||
|
} |
||||
|
|
||||
|
// Only supporting 2048-bit and P256 keys right now
|
||||
|
// because that's all that's practical and well-supported.
|
||||
|
// No security theatre here.
|
||||
|
alg := "ES256" |
||||
|
switch pub.Key().(type) { |
||||
|
case *rsa.PublicKey: |
||||
|
alg = "RS256" |
||||
|
} |
||||
|
|
||||
|
if selfSign, _ := header["_jwk"].(bool); selfSign { |
||||
|
delete(header, "_jwk") |
||||
|
any := Object{} |
||||
|
_ = json.Unmarshal(keypairs.MarshalJWKPublicKey(pub), &any) |
||||
|
header["jwk"] = any |
||||
|
} |
||||
|
|
||||
|
// TODO what are the acceptable values? JWT. JWS? others?
|
||||
|
header["typ"] = "JWT" |
||||
|
if _, ok := header["jwk"]; !ok { |
||||
|
thumbprint := keypairs.ThumbprintPublicKey(pub) |
||||
|
kid, _ := header["kid"].(string) |
||||
|
if "" != kid && thumbprint != kid { |
||||
|
return nil, errors.New("'kid' should be the key's thumbprint") |
||||
|
} |
||||
|
header["kid"] = thumbprint |
||||
|
} |
||||
|
header["alg"] = alg |
||||
|
|
||||
|
protected, err := json.Marshal(header) |
||||
|
if nil != err { |
||||
|
return nil, err |
||||
|
} |
||||
|
return protected, nil |
||||
|
} |
||||
|
|
||||
|
func claimsToPayload(claims Object) ([]byte, error) { |
||||
|
if nil == claims { |
||||
|
claims = Object{} |
||||
|
} |
||||
|
|
||||
|
jti, _ := claims["jti"].(string) |
||||
|
exp, _ := claims["exp"].(int64) |
||||
|
dur, _ := claims["exp"].(string) |
||||
|
insecure, _ := claims["insecure"].(bool) |
||||
|
|
||||
|
// parse if exp is actually a duration, such as "15m"
|
||||
|
if 0 == exp && "" != dur { |
||||
|
s, err := ParseDuration(dur) |
||||
|
if nil != err { |
||||
|
return nil, err |
||||
|
} |
||||
|
exp = time.Now().Add(time.Duration(s) * time.Second).Unix() |
||||
|
claims["exp"] = exp |
||||
|
} |
||||
|
if "" == jti && 0 == exp && !insecure { |
||||
|
return nil, errors.New("token must have jti or exp as to be expirable / cancellable") |
||||
|
} |
||||
|
|
||||
|
return json.Marshal(claims) |
||||
|
} |
||||
|
|
||||
|
func JWSToJWT(jwt *JWS) string { |
||||
|
return fmt.Sprintf( |
||||
|
"%s.%s.%s", |
||||
|
jwt.Protected, |
||||
|
jwt.Payload, |
||||
|
jwt.Signature, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func Sign(rand io.Reader, privkey keypairs.PrivateKey, hash []byte) []byte { |
||||
|
var sig []byte |
||||
|
|
||||
|
if len(hash) != 32 { |
||||
|
panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported") |
||||
|
} |
||||
|
|
||||
|
switch k := privkey.(type) { |
||||
|
case *rsa.PrivateKey: |
||||
|
sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash) |
||||
|
case *ecdsa.PrivateKey: |
||||
|
r, s, _ := ecdsa.Sign(rand, k, hash[:]) |
||||
|
rb := r.Bytes() |
||||
|
fmt.Println("debug:") |
||||
|
fmt.Println(r, s) |
||||
|
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 |
||||
|
} |
Loading…
Reference in new issue