164 lines
3.2 KiB
Go
164 lines
3.2 KiB
Go
package jwk
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil/base58"
|
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
|
"github.com/lestrrat-go/jwx/v2/jws"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const DefaultKeySize = 2048
|
|
|
|
type (
|
|
Key = jwk.Key
|
|
Set = jwk.Set
|
|
ParseOption = jwk.ParseOption
|
|
)
|
|
|
|
var (
|
|
FromRaw = jwk.FromRaw
|
|
NewSet = jwk.NewSet
|
|
)
|
|
|
|
const AlgorithmKey = jwk.AlgorithmKey
|
|
|
|
func Parse(src []byte, options ...jwk.ParseOption) (Set, error) {
|
|
return jwk.Parse(src, options...)
|
|
}
|
|
|
|
func RS256PublicKeySet(keys ...jwk.Key) (jwk.Set, error) {
|
|
set := jwk.NewSet()
|
|
|
|
for _, k := range keys {
|
|
pubkey, err := k.PublicKey()
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
if err := pubkey.Set(jwk.AlgorithmKey, jwa.RS256); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
if err := set.AddKey(pubkey); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
return set, nil
|
|
}
|
|
|
|
func LoadOrGenerate(path string, size int) (jwk.Key, error) {
|
|
data, err := ioutil.ReadFile(path)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
key, err := Generate(size)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
data, err = json.Marshal(key)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
if err := ioutil.WriteFile(path, data, 0o640); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
key, err := jwk.ParseKey(data)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
func CreateCachedRemoteKeySet(ctx context.Context, url string, refreshInterval time.Duration) (func(context.Context) (jwk.Set, error), error) {
|
|
cache := jwk.NewCache(ctx)
|
|
|
|
if err := cache.Register(url, jwk.WithMinRefreshInterval(refreshInterval)); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
if _, err := cache.Refresh(ctx, url); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return func(ctx context.Context) (jwk.Set, error) {
|
|
keySet, err := cache.Get(ctx, url)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return keySet, nil
|
|
}, nil
|
|
}
|
|
|
|
func Generate(size int) (jwk.Key, error) {
|
|
privKey, err := rsa.GenerateKey(rand.Reader, size)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
key, err := jwk.FromRaw(privKey)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
func Sign(key jwk.Key, payload ...any) (string, error) {
|
|
json, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", errors.WithStack(err)
|
|
}
|
|
|
|
rawSignature, err := jws.Sign(
|
|
nil,
|
|
jws.WithKey(jwa.RS256, key),
|
|
jws.WithDetachedPayload(json),
|
|
)
|
|
if err != nil {
|
|
return "", errors.WithStack(err)
|
|
}
|
|
|
|
signature := base58.Encode(rawSignature)
|
|
|
|
return signature, nil
|
|
}
|
|
|
|
func Verify(jwks jwk.Set, signature string, payload ...any) (bool, error) {
|
|
json, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return false, errors.WithStack(err)
|
|
}
|
|
|
|
decoded := base58.Decode(signature)
|
|
|
|
_, err = jws.Verify(
|
|
decoded,
|
|
jws.WithKeySet(jwks, jws.WithRequireKid(false)),
|
|
jws.WithDetachedPayload(json),
|
|
)
|
|
if err != nil {
|
|
return false, errors.WithStack(err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|