go-mockid/mockid/mockid.go

176 lines
4.3 KiB
Go

package mockid
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/url"
"os"
"strconv"
"time"
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
"git.rootprojects.org/root/keypairs"
//jwt "github.com/dgrijalva/jwt-go"
)
// TestMain will overwrite this
var rndsrc io.Reader = rand.Reader
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"`
}
type KVDB interface {
Load(key interface{}) (value interface{}, ok bool, err error)
Store(key interface{}) (value interface{}, err error)
Delete(key interface{}) (err error)
vacuum() (err error)
}
type InspectableToken struct {
Public keypairs.PublicKey `json:"jwk"`
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(
`{"jwk":%s,"protected":%s,"payload":%s,"signature":%q,"verified":%t,"errors":%s}`,
pub, header, payload, t.Signature, t.Verified, errs,
)), nil
}
var defaultFrom string
var defaultReplyTo string
var salt []byte
func Init() {
var err error
salt64 := os.Getenv("SALT")
salt, err = base64.RawURLEncoding.DecodeString(salt64)
if len(salt64) < 22 || nil != err {
panic("SALT must be set as 22+ character base64")
}
defaultFrom = os.Getenv("MAILER_FROM")
defaultReplyTo = os.Getenv("MAILER_REPLY_TO")
//nonces = make(map[string]int64)
//nonCh = make(chan string)
/*
go func() {
for {
nonce := <- nonCh
nonces[nonce] = time.Now().Unix()
}
}()
*/
go func() {
for {
time.Sleep(15 * time.Second)
hashcashes.vacuum()
}
}()
}
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 := xkeypairs.ParseDuration(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)))
sig := JOSESign(privkey, hash[:])
sig64 := base64.RawURLEncoding.EncodeToString(sig)
token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
return protected, payload, token
}
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(rndsrc, 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
}
// TODO: move to keypairs
func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
switch pub := pubkey.Key().(type) {
case *rsa.PublicKey:
// TODO keypairs.Size(key) to detect key size ?
//alg := "SHA256"
// TODO: this hasn't been tested yet
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err {
return false
}
return true
case *ecdsa.PublicKey:
r := &big.Int{}
r.SetBytes(sig[0:32])
s := &big.Int{}
s.SetBytes(sig[32:])
fmt.Println("debug: sig len:", len(sig))
fmt.Println("debug: r, s:", r, s)
return ecdsa.Verify(pub, hash, r, s)
default:
panic("impossible condition: non-rsa/non-ecdsa key")
return false
}
}