Skip to content

Commit

Permalink
Support for secp256k1
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Dec 12, 2023
1 parent 91104d5 commit 9bda90a
Show file tree
Hide file tree
Showing 41 changed files with 439 additions and 161 deletions.
5 changes: 2 additions & 3 deletions auth/api/iam/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 30 additions & 13 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"path"
"regexp"
"time"
Expand Down Expand Up @@ -153,13 +156,13 @@ func (client *Crypto) Configure(config core.ServerConfig) error {
// New generates a new key pair.
// Stores the private key, returns the public basicKey.
// It returns an error when a key with the resulting ID already exists.
func (client *Crypto) New(ctx context.Context, namingFunc KIDNamingFunc) (Key, error) {
keyPair, kid, err := generateKeyPairAndKID(namingFunc)
func (client *Crypto) New(ctx context.Context, keyType KeyType, namingFunc KIDNamingFunc) (Key, error) {
keyPair, kid, err := generateKeyPairAndKID(keyType, namingFunc)
if err != nil {
return nil, err
}

audit.Log(ctx, log.Logger(), audit.CryptoNewKeyEvent).Infof("Generating new key pair: %s", kid)
audit.Log(ctx, log.Logger(), audit.CryptoNewKeyEvent).Infof("Generating new key pair (type: %s): %s", keyType, kid)
if client.storage.PrivateKeyExists(ctx, kid) {
return nil, errors.New("key with the given ID already exists")
}
Expand All @@ -172,24 +175,38 @@ func (client *Crypto) New(ctx context.Context, namingFunc KIDNamingFunc) (Key, e
}, nil
}

func generateKeyPairAndKID(namingFunc KIDNamingFunc) (*ecdsa.PrivateKey, string, error) {
keyPair, err := generateECKeyPair()
if err != nil {
return nil, "", err
func generateKeyPairAndKID(keyType KeyType, namingFunc KIDNamingFunc) (crypto.Signer, string, error) {
var privateKey crypto.Signer
var err error
switch keyType {
case ECP256Key:
privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
case ECP256k1Key:
var pk *secp256k1.PrivateKey
pk, err = secp256k1.GeneratePrivateKey()
if err == nil {
privateKey = pk.ToECDSA()
}
case Ed25519Key:
_, privateKey, err = ed25519.GenerateKey(rand.Reader)
case RSA2048Key:
privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
case RSA3072Key:
privateKey, err = rsa.GenerateKey(rand.Reader, 3072)
case RSA4096Key:
privateKey, err = rsa.GenerateKey(rand.Reader, 4096)
default:
return nil, "", fmt.Errorf("invalid key type: %s", keyType)
}

kid, err := namingFunc(keyPair.Public())
kid, err := namingFunc(privateKey.Public())
if err != nil {
return nil, "", err
}
log.Logger().
WithField(core.LogFieldKeyID, kid).
Debug("Generated new key pair")
return keyPair, kid, nil
}

func generateECKeyPair() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
return privateKey, kid, nil
}

// Exists checks storage for an entry for the given legal entity and returns true if it exists
Expand Down
38 changes: 27 additions & 11 deletions crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestCrypto_Exists(t *testing.T) {
client := createCrypto(t)

kid := "kid"
client.New(audit.TestContext(), StringNamingFunc(kid))
client.New(audit.TestContext(), ECP256Key, StringNamingFunc(kid))

t.Run("returns true for existing key", func(t *testing.T) {
assert.True(t, client.Exists(ctx, kid))
Expand All @@ -65,32 +65,48 @@ func TestCrypto_New(t *testing.T) {
logrus.StandardLogger().SetFormatter(&logrus.JSONFormatter{})
ctx := audit.TestContext()

t.Run("ok", func(t *testing.T) {
kid := "kid"
testCases := []KeyType{ECP256Key, ECP256k1Key, Ed25519Key, RSA2048Key}
for _, keyType := range testCases {
t.Run(string(keyType), func(t *testing.T) {
key, err := client.New(ctx, keyType, StringNamingFunc(string(keyType)))
require.NoError(t, err)
assert.NotNil(t, key.Public())
})
}

t.Run("audit logs", func(t *testing.T) {
const kid = "kid"
auditLogs := audit.CaptureLogs(t)

key, err := client.New(ctx, StringNamingFunc(kid))
key, err := client.New(ctx, ECP256Key, StringNamingFunc(kid))

assert.NoError(t, err)
require.NoError(t, err)
assert.NotNil(t, key.Public())
assert.Equal(t, kid, key.KID())
auditLogs.AssertContains(t, ModuleName, "CreateNewKey", audit.TestActor, "Generating new key pair: kid")
auditLogs.AssertContains(t, ModuleName, "CreateNewKey", audit.TestActor, "Generating new key pair (type: secp256r1): kid")
})

t.Run("error - invalid KID", func(t *testing.T) {
kid := "../certificate"

key, err := client.New(ctx, StringNamingFunc(kid))
key, err := client.New(ctx, ECP256Key, StringNamingFunc(kid))

assert.ErrorContains(t, err, "invalid key ID")
assert.Nil(t, key)
})

t.Run("error - invalid key type", func(t *testing.T) {
key, err := client.New(ctx, "caesar", StringNamingFunc("foo"))

assert.EqualError(t, err, "invalid key type: caesar")
assert.Nil(t, key)
})

t.Run("error - NamingFunction returns err", func(t *testing.T) {
errorNamingFunc := func(key crypto.PublicKey) (string, error) {
return "", errors.New("b00m!")
}
_, err := client.New(ctx, errorNamingFunc)
_, err := client.New(ctx, ECP256Key, errorNamingFunc)
assert.Error(t, err)
})

Expand All @@ -101,7 +117,7 @@ func TestCrypto_New(t *testing.T) {
storageMock.EXPECT().SavePrivateKey(ctx, gomock.Any(), gomock.Any()).Return(errors.New("foo"))

client := &Crypto{storage: storageMock}
key, err := client.New(ctx, StringNamingFunc("123"))
key, err := client.New(ctx, ECP256Key, StringNamingFunc("123"))
assert.Nil(t, key)
assert.Error(t, err)
assert.Equal(t, "could not create new keypair: could not save private key: foo", err.Error())
Expand All @@ -113,7 +129,7 @@ func TestCrypto_New(t *testing.T) {
storageMock.EXPECT().PrivateKeyExists(ctx, "123").Return(true)

client := &Crypto{storage: storageMock}
key, err := client.New(ctx, StringNamingFunc("123"))
key, err := client.New(ctx, ECP256Key, StringNamingFunc("123"))
assert.Nil(t, key)
assert.EqualError(t, err, "key with the given ID already exists", err)
})
Expand All @@ -123,7 +139,7 @@ func TestCrypto_Resolve(t *testing.T) {
ctx := context.Background()
client := createCrypto(t)
kid := "kid"
key, _ := client.New(audit.TestContext(), StringNamingFunc(kid))
key, _ := client.New(audit.TestContext(), ECP256Key, StringNamingFunc(kid))

t.Run("ok", func(t *testing.T) {
resolvedKey, err := client.Resolve(ctx, "kid")
Expand Down
2 changes: 1 addition & 1 deletion crypto/decrypter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestCrypto_Decrypt(t *testing.T) {
t.Run("ok", func(t *testing.T) {
client := createCrypto(t)
kid := "kid"
key, _ := client.New(audit.TestContext(), StringNamingFunc(kid))
key, _ := client.New(audit.TestContext(), ECP256Key, StringNamingFunc(kid))
pubKey := key.Public().(*ecdsa.PublicKey)

cipherText, err := EciesEncrypt(pubKey, []byte("hello!"))
Expand Down
7 changes: 7 additions & 0 deletions crypto/ecies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
package crypto

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -49,3 +52,7 @@ func TestEciesDecrypt(t *testing.T) {

assert.Equal(t, []byte("hello world"), plainText)
}

func generateECKeyPair() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
2 changes: 1 addition & 1 deletion crypto/ephemeral.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ package crypto

// NewEphemeralKey returns a Key for single use.
func NewEphemeralKey(namingFunc KIDNamingFunc) (Key, error) {
keyPair, kid, err := generateKeyPairAndKID(namingFunc)
keyPair, kid, err := generateKeyPairAndKID(ECP256Key, namingFunc)
if err != nil {
return nil, err
}
Expand Down
19 changes: 18 additions & 1 deletion crypto/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,28 @@ var ErrPrivateKeyNotFound = errors.New("private key not found")
// KIDNamingFunc is a function passed to New() which generates the kid for the pub/priv key
type KIDNamingFunc func(key crypto.PublicKey) (string, error)

type KeyType string

const (
// ECP256Key is the key type for EC P-256
ECP256Key KeyType = "secp256r1"
// ECP256k1Key is the key type for EC P-256K (Koblitz curve)
ECP256k1Key = "secp256k1"
// Ed25519Key is the key type for ed25519
Ed25519Key = "ed25519"
// RSA2048Key is the key type for rsa2048
RSA2048Key = "rsa2048"
// RSA3072Key is the key type for rsa3072
RSA3072Key = "rsa3072"
// RSA4096Key is the key type for rsa4096
RSA4096Key = "rsa4096"
)

// KeyCreator is the interface for creating key pairs.
type KeyCreator interface {
// New generates a keypair and returns a Key. The context is used to pass audit information.
// The KIDNamingFunc will provide the kid.
New(ctx context.Context, namingFunc KIDNamingFunc) (Key, error)
New(ctx context.Context, keyType KeyType, namingFunc KIDNamingFunc) (Key, error)
}

// KeyResolver is the interface for resolving keys.
Expand Down
34 changes: 24 additions & 10 deletions crypto/jwx.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwe"
"github.com/lestrrat-go/jwx/v2/jwk"
Expand All @@ -41,7 +42,7 @@ import (
// ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported
var ErrUnsupportedSigningKey = errors.New("signing key algorithm not supported")

var supportedAlgorithms = []jwa.SignatureAlgorithm{jwa.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.EdDSA, jwa.ES384, jwa.ES512}
var supportedAlgorithms = []jwa.SignatureAlgorithm{jwa.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.EdDSA, jwa.ES256K, jwa.ES384, jwa.ES512}

const defaultRsaEncryptionAlgorithm = jwa.RSA_OAEP_256
const defaultEcEncryptionAlgorithm = jwa.ECDH_ES_A256KW
Expand Down Expand Up @@ -135,11 +136,15 @@ func jwkKey(signer crypto.Signer) (key jwk.Key, err error) {
case *rsa.PrivateKey:
key.Set(jwk.AlgorithmKey, jwa.PS256)
case *ecdsa.PrivateKey:
var alg jwa.SignatureAlgorithm
alg, err = ecAlg(k)
alg, err := ecAlgUsingPublicKey(k.PublicKey)
if err != nil {
return nil, err
}
key.Set(jwk.AlgorithmKey, alg)
case ed25519.PrivateKey:
key.Set(jwk.AlgorithmKey, jwa.EdDSA)
default:
err = errors.New("unsupported signing private key")
err = fmt.Errorf("unsupported signing private key: %T", k)
}
return
}
Expand All @@ -159,7 +164,17 @@ func signJWT(key jwk.Key, claims map[string]interface{}, headers map[string]inte
return "", fmt.Errorf("invalid JWT headers: %w", err)
}

sig, err = jwt.Sign(t, jwt.WithKey(jwa.SignatureAlgorithm(key.Algorithm().String()), key, jws.WithProtectedHeaders(hdr)))
var publicKey crypto.PublicKey
if err = key.Raw(&publicKey); err != nil {
return "", err
}
alg, err := SignatureAlgorithm(publicKey)
if err != nil {
return "", err
}

sig, err = jwt.Sign(t, jwt.WithKey(alg, key, jws.WithProtectedHeaders(hdr)))

token = string(sig)

return
Expand Down Expand Up @@ -367,12 +382,11 @@ func convertHeaders(headers map[string]interface{}) (jws.Headers, error) {
return hdr, nil
}

func ecAlg(key *ecdsa.PrivateKey) (alg jwa.SignatureAlgorithm, err error) {
alg, err = ecAlgUsingPublicKey(key.PublicKey)
return
}

func ecAlgUsingPublicKey(key ecdsa.PublicKey) (alg jwa.SignatureAlgorithm, err error) {
if key.Curve == secp256k1.S256() {
return jwa.ES256K, nil
}
// Otherwise, assume it's a NIST curve
switch key.Params().BitSize {
case 256:
alg = jwa.ES256
Expand Down
8 changes: 4 additions & 4 deletions crypto/jwx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestCrypto_SignJWT(t *testing.T) {
client := createCrypto(t)

kid := "kid"
key, _ := client.New(audit.TestContext(), StringNamingFunc(kid))
key, _ := client.New(audit.TestContext(), ECP256Key, StringNamingFunc(kid))

t.Run("creates valid JWT", func(t *testing.T) {
tokenString, err := client.SignJWT(audit.TestContext(), map[string]interface{}{"iss": "nuts", "sub": "subject"}, nil, key)
Expand Down Expand Up @@ -197,7 +197,7 @@ func TestCrypto_SignJWS(t *testing.T) {
client := createCrypto(t)

kid := "kid"
key, _ := client.New(audit.TestContext(), StringNamingFunc(kid))
key, _ := client.New(audit.TestContext(), ECP256Key, StringNamingFunc(kid))

t.Run("creates valid JWS", func(t *testing.T) {
payload, _ := json.Marshal(map[string]interface{}{"iss": "nuts"})
Expand Down Expand Up @@ -244,7 +244,7 @@ func TestCrypto_SignJWS(t *testing.T) {
func TestCrypto_EncryptJWE(t *testing.T) {
client := createCrypto(t)

key, _ := client.New(audit.TestContext(), StringNamingFunc("did:nuts:1234#key-1"))
key, _ := client.New(audit.TestContext(), ECP256Key, StringNamingFunc("did:nuts:1234#key-1"))
public := key.Public()

headers := map[string]interface{}{"typ": "JWT", "kid": key.KID()}
Expand Down Expand Up @@ -327,7 +327,7 @@ func TestCrypto_DecryptJWE(t *testing.T) {
client := createCrypto(t)

kid := "did:nuts:1234#key-1"
key, _ := client.New(audit.TestContext(), StringNamingFunc(kid))
key, _ := client.New(audit.TestContext(), ECP256Key, StringNamingFunc(kid))

t.Run("decrypts valid JWE", func(t *testing.T) {
payload, _ := json.Marshal(map[string]interface{}{"iss": "nuts"})
Expand Down
Loading

0 comments on commit 9bda90a

Please sign in to comment.