Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support secp256k1 and ed25519 keys #2564

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"official" name is P-256? What happened to 384 en 521?

// 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
Loading