diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go index 8ed32173de..04af47a13b 100644 --- a/auth/api/iam/generated.go +++ b/auth/api/iam/generated.go @@ -123,7 +123,6 @@ type RequestAccessTokenJSONBody struct { Scope string `json:"scope"` // UserID The ID of the user for which this access token is requested. - // It's handled as opaque ID and is scoped to the requester DID. UserID *string `json:"userID,omitempty"` Verifier string `json:"verifier"` } @@ -160,7 +159,7 @@ type ServerInterface interface { // Introspection endpoint to retrieve information from an Access Token as described by RFC7662 // (POST /internal/auth/v2/accesstoken/introspect) IntrospectAccessToken(ctx echo.Context) error - // Requests an access token using the vp_token-bearer grant. + // Start the authorization flow to get an access token from a remote authorization server. // (POST /internal/auth/v2/{did}/request-access-token) RequestAccessToken(ctx echo.Context, did string) error } @@ -686,7 +685,7 @@ type StrictServerInterface interface { // Introspection endpoint to retrieve information from an Access Token as described by RFC7662 // (POST /internal/auth/v2/accesstoken/introspect) IntrospectAccessToken(ctx context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) - // Requests an access token using the vp_token-bearer grant. + // Start the authorization flow to get an access token from a remote authorization server. // (POST /internal/auth/v2/{did}/request-access-token) RequestAccessToken(ctx context.Context, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) } diff --git a/crypto/crypto.go b/crypto/crypto.go index d0c6060f8b..72d30d18f9 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -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" @@ -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") } @@ -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 diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index cf922ecf62..5e6b9b63ac 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -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)) @@ -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) }) @@ -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()) @@ -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) }) @@ -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") diff --git a/crypto/decrypter_test.go b/crypto/decrypter_test.go index d07a18a241..bd125fceaa 100644 --- a/crypto/decrypter_test.go +++ b/crypto/decrypter_test.go @@ -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!")) diff --git a/crypto/ecies_test.go b/crypto/ecies_test.go index 8a902a03a3..c6c00b5362 100644 --- a/crypto/ecies_test.go +++ b/crypto/ecies_test.go @@ -19,6 +19,9 @@ package crypto import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "testing" "github.com/stretchr/testify/assert" @@ -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) +} diff --git a/crypto/ephemeral.go b/crypto/ephemeral.go index 43262498f4..66e6f525a0 100644 --- a/crypto/ephemeral.go +++ b/crypto/ephemeral.go @@ -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 } diff --git a/crypto/interface.go b/crypto/interface.go index 03e18e541f..6b0175470d 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -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. diff --git a/crypto/jwx.go b/crypto/jwx.go index b3c452ee1b..28134adaa6 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -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" @@ -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 @@ -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 } @@ -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 @@ -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 diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 495a258c08..85bd2ae2a7 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -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) @@ -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"}) @@ -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()} @@ -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"}) diff --git a/crypto/mock.go b/crypto/mock.go index a1bd24b75f..80e4d9f566 100644 --- a/crypto/mock.go +++ b/crypto/mock.go @@ -40,18 +40,18 @@ func (m *MockKeyCreator) EXPECT() *MockKeyCreatorMockRecorder { } // New mocks base method. -func (m *MockKeyCreator) New(ctx context.Context, namingFunc KIDNamingFunc) (Key, error) { +func (m *MockKeyCreator) New(ctx context.Context, keyType KeyType, namingFunc KIDNamingFunc) (Key, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "New", ctx, namingFunc) + ret := m.ctrl.Call(m, "New", ctx, keyType, namingFunc) ret0, _ := ret[0].(Key) ret1, _ := ret[1].(error) return ret0, ret1 } // New indicates an expected call of New. -func (mr *MockKeyCreatorMockRecorder) New(ctx, namingFunc any) *gomock.Call { +func (mr *MockKeyCreatorMockRecorder) New(ctx, keyType, namingFunc any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockKeyCreator)(nil).New), ctx, namingFunc) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockKeyCreator)(nil).New), ctx, keyType, namingFunc) } // MockKeyResolver is a mock of KeyResolver interface. @@ -218,18 +218,18 @@ func (mr *MockKeyStoreMockRecorder) List(ctx any) *gomock.Call { } // New mocks base method. -func (m *MockKeyStore) New(ctx context.Context, namingFunc KIDNamingFunc) (Key, error) { +func (m *MockKeyStore) New(ctx context.Context, keyType KeyType, namingFunc KIDNamingFunc) (Key, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "New", ctx, namingFunc) + ret := m.ctrl.Call(m, "New", ctx, keyType, namingFunc) ret0, _ := ret[0].(Key) ret1, _ := ret[1].(error) return ret0, ret1 } // New indicates an expected call of New. -func (mr *MockKeyStoreMockRecorder) New(ctx, namingFunc any) *gomock.Call { +func (mr *MockKeyStoreMockRecorder) New(ctx, keyType, namingFunc any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockKeyStore)(nil).New), ctx, namingFunc) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockKeyStore)(nil).New), ctx, keyType, namingFunc) } // Resolve mocks base method. diff --git a/crypto/test/ed25519.sk b/crypto/test/private_ed25519_pkcs8.pem similarity index 100% rename from crypto/test/ed25519.sk rename to crypto/test/private_ed25519_pkcs8.pem diff --git a/crypto/test/private_invalid_key.pem b/crypto/test/private_invalid_key.pem new file mode 100644 index 0000000000..7a47bec95a --- /dev/null +++ b/crypto/test/private_invalid_key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +1LGfzY1juuygBwYFK4EEACKhZANiAATAncgtph3ACHSPXWyvyYop/71skjBK6Q1T +UB6WFs6pusiUD1pYDMZ01IjBx/cMJaJP/VoyYl24Wbf2/mBnKt1lfDzYYVf0kFxT +dtTkGJrJAzbtHuysgU+GrEdjYSfhDKc= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/crypto/test/private_rsa1024_pkcs8.pem b/crypto/test/private_rsa1024_pkcs8.pem new file mode 100644 index 0000000000..8b7d86873d --- /dev/null +++ b/crypto/test/private_rsa1024_pkcs8.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAM2I2YtS2GfUakR0 +HEoTSpV+kYDXf/uDbtUkfrD0v5PYziMdunUHKaXD+AEUncy9kV7Gt5p7ZmR/5DHo +NbgrBBjY+u7naHU7BUiT5SQieOF3oXGfuTmdnHX1ymKWY062x+IqMyy7vmhpFTIc +9EaJ1oKMUL9ewEmIscuGvmsy9aCPAgMBAAECgYA011ceo6jxYMIFYViYjschEg40 +crL7pbnL4HsV4YaTayzsCEuUpMfHT0+mb3d2WNJT7IDtnYYglmTDk/CjraN6je0z +eDlKH+LsbvwoUN58obMkvjrX7195+xgEraFA+3hhoLPQjb2ux6cz13GR3RC1D/QH +VsN6BRJYkcoxe/G2IQJBAOCbCssPtMd7BAGpR+M4Y1zUkNLQyltmDUfaPuoyQpyi +3KlJ2yofiefGBBgpvS9BoJFBw0VOqLuxeCDnnGY09n8CQQDqQ2WOJtmf74OsIVe2 +GcVvyFQVzH2g8IMboxw/iroTOdXijhtCAGfCuAJjNtksHD3/19t3X+Z4nAWahQAW +Gu3xAkEAhUn9Abx0X90U55d53dHcxX4v46ucKtlJEFbn9zuUZDgSEzSNJ1ZIFI9i +ZqR+bMjZbNpF859WauxKidxo6A6OKQJAB4u6NrT7p5IwfJfqWlxEJtCeHMGkfk2g ++3/qhgVy7vGa+Rw4toyKyxPgR8/ZePlD6fzK/fJh2xqzd4G3Of8OEQJAGhfkO+MI +TZvIL9/qm4UHyxjnJ+0zhD2H6IdbxpHKOByK5fmtIYE193tw+DdO7Es45Ns2c9y/ +i7UkrL+HBMftgw== +-----END PRIVATE KEY----- diff --git a/crypto/test/rsa.sk b/crypto/test/private_rsa2048_pkcs1.pem similarity index 100% rename from crypto/test/rsa.sk rename to crypto/test/private_rsa2048_pkcs1.pem diff --git a/crypto/test/private_secp256k1_asn1.pem b/crypto/test/private_secp256k1_asn1.pem new file mode 100644 index 0000000000..20f86481d9 --- /dev/null +++ b/crypto/test/private_secp256k1_asn1.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MHQCAQEEIMgBmsXo5xfj6GMDUpXY9PWZpLV+NKIDfos1MYqYDZb9oAcGBSuBBAAK +oUQDQgAEDQGDPxSLLfrcnFM8HZLhyexQrr1BZkLx0awd1URadysAdqxeMY6irYn8 +Hj2Qb7tlhDCFBfOeGKxZjNyf2fuSvQ== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/crypto/test/private_secp256k1_asn1_echeader.pem b/crypto/test/private_secp256k1_asn1_echeader.pem new file mode 100644 index 0000000000..7364f5b452 --- /dev/null +++ b/crypto/test/private_secp256k1_asn1_echeader.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIMgBmsXo5xfj6GMDUpXY9PWZpLV+NKIDfos1MYqYDZb9oAcGBSuBBAAK +oUQDQgAEDQGDPxSLLfrcnFM8HZLhyexQrr1BZkLx0awd1URadysAdqxeMY6irYn8 +Hj2Qb7tlhDCFBfOeGKxZjNyf2fuSvQ== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/crypto/test/private_secp256r1_asn1.pem b/crypto/test/private_secp256r1_asn1.pem new file mode 100644 index 0000000000..86d19292ed --- /dev/null +++ b/crypto/test/private_secp256r1_asn1.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGkAgEBBDDkxTzFUFbIvBZWH6UlibvdXrhqYOzDb1lKTVSWX8P8OLbuu7Dzp9Ru +1LGfzY1juuygBwYFK4EEACKhZANiAATAncgtph3ACHSPXWyvyYop/71skjBK6Q1T +UB6WFs6pusiUD1pYDMZ01IjBx/cMJaJP/VoyYl24Wbf2/mBnKt1lfDzYYVf0kFxT +dtTkGJrJAzbtHuysgU+GrEdjYSfhDKc= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/crypto/test/ec.sk b/crypto/test/private_secp256r1_asn1_echeader.pem similarity index 100% rename from crypto/test/ec.sk rename to crypto/test/private_secp256r1_asn1_echeader.pem diff --git a/crypto/test/private_secp256r1_pkcs8.pem b/crypto/test/private_secp256r1_pkcs8.pem new file mode 100644 index 0000000000..13e82a62ab --- /dev/null +++ b/crypto/test/private_secp256r1_pkcs8.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgam8mCp1lEcqIa7hy +iBy861mhscSeDP2W+KLm1tn2ZI2hRANCAAS+xlCLMK1ymtevTsDs44XghmOcp1Rf +9qmwJDj5YX5zNGDqvYVb0NVyTJ01tCOBBHNQY+UJYjoi+sTYP0Q3wnqB +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/crypto/test/sk.pem b/crypto/test/sk.pem deleted file mode 100644 index 86ade6e98e..0000000000 --- a/crypto/test/sk.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwMR1Stjz9CFiZ -ZX9TfbigNNpj/UdLapdi5j7ZMrCydHSdMX4+cvJHYHZ7UUrVM9N/Pn8OJpTqBQi/ -wH/9EPnu5r4KGKKK+1i+FlFf8AZPs6aiH7EIgZGJWAxQfxIVvViMJmcDnAiDNYk9 -uyPCunm+eT4Q30Lka6gbT/Q33MhngaLqd82i/w5amuNcxmcnsl6yJwvoRp9gAKM+ -a8E9ds3qYPDDeUW6R3XMSVw+9S+FePiLx4hOA6JDSiAGhzcKVRaJHnWE3+dMUSyK -wW/tNUUsoTGBVsGzcDjeuxXHhJlROsQuDSyukeeKRmHOvM/qXP7D+RPjK2eXVtnX -cKaf3CBPAgMBAAECggEBAI4KPXEAXCi2ms57+0QAgQIcv1mKGnM64bOWvWvsUhn2 -TE/5SNLdXvFe1jIu7LYQI+6HotNXdZC+0WG0EPwy4dqpkZCe6OCPqvcec5jsdI8F -inEtlJ+XH928t+uSebtpccfPnfPAfjg5nKNo28j4Ra6iPFX18bTrBUrBiYbPhaPP -HISw/FiZeBsvzY9/dbx3/i36W3R0utqgeQ6cZWxwIT5RVInazLo+s+E4w6hOb9bO -HeO7jtLu8SLaJZwqooZ0cI5abYpDNJI9OU4J489M3zo6ZJiWMwhvwUwok15JKbZ/ -MsrekHaiu7iUd50D9Mirg6iB0bJDR3xQmpN2tZIcnCECgYEA/0Et0h1vG2wmG2VL -gt3b8HU4GKF5UkPjksYEys/HLXuW3oNygiqJWWZNwy8pDnHqd4urunxuFK/prMxm -43eWEzQIHdEK0rfpfak1FUP4FGlOMp3vMvw0yuPVvaRzpRN94SHirYUVwDR5lrZ0 -T9+j3LpSfcTB31eGOkn/ln6lEOUCgYEA8OSs0b0P9R7Bc7ZliOwwCRvt24gCcJky -yPqtlm7P2Z6fP854Yvu0nItP+6kVFB8dz4o7VammOYQwYs3Lrg92poVXDNGazUIJ -YxT/JPAY+d1WwVvee/BdKJ+mOda6+lIGEQi440QY0aCtiQEdPGXt0NE0RYFvM4s/ -k0rZtmwvfSMCgYEA3yZONpiA38pmbiDaKOhoNQllJzNTavXq6A+xdNS83ihjttfX -rbAeL0fex7pc/EHepvA2C2xomDFJ6kUv1cBgNR2R0u9DtQAPYkohHBw1rzJ4qIul -6D7QsGcKHya76x7lN4J2NxhX8ZZujbGocYOkL328TDNNAkH0GNVEWn8RM3kCgYA4 -EaG/97d9IDl6y1t6sS7FEAEe9dtLhfzyFpbMyuIKDweV/GK890UkorBtLP/A/TUd -F1mUKLaN8JyqgqgDzYmaXLLUQv07BUHWFA8G8/N8RO5qdw2j32BvkilIkRhYJztO -P695BmKYeEOr/dxmMHtX/Tmja+sMHj8f824VLb0n7QKBgEuAqdCao4fm9WKFhUPV -XdqXqgqoRgvOVP6qwleIhv/TwFEdW9ZVC1K1h7nvXhlvW71Cm95jyv8DDCfvqlG5 -Bupi1fe4UHfb7h5vldOzY2qr8wfJ5+I+vcqo7p4s8Y7P0ZmTEO/1bVG0GFVa32Gq -B6Moz2dS/7ICo0Itc2OI7RC8 ------END PRIVATE KEY----- \ No newline at end of file diff --git a/crypto/util/pem.go b/crypto/util/pem.go index 33d971afbf..a2fb0498a2 100644 --- a/crypto/util/pem.go +++ b/crypto/util/pem.go @@ -21,7 +21,10 @@ import ( "crypto/ed25519" "crypto/rsa" "crypto/x509" + "encoding/asn1" "encoding/pem" + "errors" + "github.com/decred/dcrd/dcrec/secp256k1/v4" ) // PemToPublicKey converts a PEM encoded public key to a crypto.PublicKey @@ -59,45 +62,117 @@ func PublicKeyToPem(pub crypto.PublicKey) (string, error) { } // PrivateKeyToPem converts an public key to PEM encoding -func PrivateKeyToPem(pub crypto.PrivateKey) (string, error) { - pubASN1, err := x509.MarshalPKCS8PrivateKey(pub) +func PrivateKeyToPem(privateKey crypto.PrivateKey) (string, error) { + var pubASN1 []byte + var err error + if pk, ok := privateKey.(*ecdsa.PrivateKey); ok && pk.Curve == secp256k1.S256() { + privateKey = secp256k1.PrivKeyFromBytes(pk.D.Bytes()) + } + + var pemType string + switch pk := privateKey.(type) { + case *secp256k1.PrivateKey: + pubASN1, err = marshalSecp256k1(pk) + pemType = "EC PRIVATE KEY" + default: + pubASN1, err = x509.MarshalPKCS8PrivateKey(privateKey) + pemType = "PRIVATE KEY" + } if err != nil { return "", err } - pubBytes := pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", + Type: pemType, Bytes: pubASN1, }) return string(pubBytes), err } -// PemToPrivateKey converts a PEM encoded private key to a Signer interface. It supports EC, RSA and PKIX PEM encoded strings -func PemToPrivateKey(bytes []byte) (signer crypto.Signer, err error) { +// PemToPrivateKey converts a PEM encoded private key to a Signer interface. It supports EC, RSA (PKCS1) and PKCS8 PEM encoded strings +func PemToPrivateKey(bytes []byte) (crypto.Signer, error) { block, _ := pem.Decode(bytes) if block == nil { - err = ErrWrongPrivateKey - return + return nil, ErrWrongPrivateKey } - + var err error + var result crypto.Signer switch block.Type { case "RSA PRIVATE KEY": - signer, err = x509.ParsePKCS1PrivateKey(block.Bytes) + result, err = x509.ParsePKCS1PrivateKey(block.Bytes) case "EC PRIVATE KEY": - signer, err = x509.ParseECPrivateKey(block.Bytes) + result, err = parseECPrivateKey(block) case "PRIVATE KEY": var key interface{} key, err = x509.ParsePKCS8PrivateKey(block.Bytes) - switch k := key.(type) { - case *rsa.PrivateKey: - signer = k - case *ecdsa.PrivateKey: - signer = k - case ed25519.PrivateKey: - signer = k + if err != nil { + if err.Error() == "x509: failed to parse private key (use ParseECPrivateKey instead for this key format)" { + result, err = parseECPrivateKey(block) + } + } else { + switch k := key.(type) { + case *rsa.PrivateKey: + result = k + case *ecdsa.PrivateKey: + result = k + case ed25519.PrivateKey: + result = k + } + } + } + if result == nil { + return nil, errors.Join(ErrWrongPrivateKey, err) + } + return result, nil +} + +// parseECPrivateKey parses EC private keys, trying Golang's x509 package first, +// and then trying other, by default unsupported keys (secp256k1 +func parseECPrivateKey(block *pem.Block) (crypto.Signer, error) { + pk, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + if err.Error() == "x509: unknown elliptic curve" { + // Might be secp256k1 + return unmarshalSecp256k1(block.Bytes) } + return nil, err } - return + return pk, nil +} + +// unmarshalSecp256k1 unmarshals into a secp256k1 private key, provided in ASN.1 format, according to section 4.3.6 of ANSI X9.62 +func unmarshalSecp256k1(data []byte) (crypto.Signer, error) { + var privateKeyDER asn1ECPrivateKey + _, err := asn1.Unmarshal(data, &privateKeyDER) + if err != nil { + return nil, err + } + if !privateKeyDER.Curve.Equal(oidNamedCurveP256k1) { + return nil, errors.New("unknown elliptic curve") + } + return secp256k1.PrivKeyFromBytes(privateKeyDER.Data).ToECDSA(), nil +} + +// oidNamedCurveP256k1 is the ASN.1 object identifier of the secp256k1 curve. +// See http://oidref.com/1.3.132.0.10 +var oidNamedCurveP256k1 = asn1.ObjectIdentifier{1, 3, 132, 0, 10} + +type asn1ECPrivateKey struct { + Version int + Data []byte + Curve asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` +} + +// marshalSecp256k1 marshals a secp256k1 private key into ASN.1 format, according to section 4.3.6 of ANSI X9.62 +func marshalSecp256k1(privateKey *secp256k1.PrivateKey) ([]byte, error) { + pubKey := privateKey.PubKey() + if !secp256k1.S256().IsOnCurve(pubKey.X(), pubKey.Y()) { + return nil, errors.New("invalid secp256k1 public key") + } + return asn1.Marshal(asn1ECPrivateKey{ + Version: 1, + Data: privateKey.Serialize(), + Curve: oidNamedCurveP256k1, + }) } diff --git a/crypto/util/pem_test.go b/crypto/util/pem_test.go index 332720a3fe..40516db9dc 100644 --- a/crypto/util/pem_test.go +++ b/crypto/util/pem_test.go @@ -24,6 +24,7 @@ import ( "encoding/pem" "github.com/stretchr/testify/require" "os" + "path" "testing" "github.com/nuts-foundation/nuts-node/crypto/test" @@ -75,36 +76,70 @@ func TestCrypto_pemToPublicKey(t *testing.T) { }) } -func TestPemToSigner(t *testing.T) { - t.Run("Convert ED25519 key", func(t *testing.T) { - pem, _ := os.ReadFile("../test/ed25519.sk") - signer, err := PemToPrivateKey(pem) - assert.NoError(t, err) - assert.NotNil(t, signer) - }) - - t.Run("Convert EC key", func(t *testing.T) { - pem, _ := os.ReadFile("../test/ec.sk") - signer, err := PemToPrivateKey(pem) - assert.NoError(t, err) - assert.NotNil(t, signer) - }) - - t.Run("Convert RSA key", func(t *testing.T) { - pem, _ := os.ReadFile("../test/rsa.sk") - signer, err := PemToPrivateKey(pem) - assert.NoError(t, err) - assert.NotNil(t, signer) - }) - - t.Run("Convert PKIX key", func(t *testing.T) { - pem, _ := os.ReadFile("../test/sk.pem") - signer, err := PemToPrivateKey(pem) - assert.NoError(t, err) - assert.NotNil(t, signer) - }) +func TestPrivateKeyMarshalling(t *testing.T) { + type testCase struct { + file string + err string + } + + testCases := []testCase{ + { + file: "../test/private_rsa2048_pkcs1.pem", + }, + { + file: "../test/private_rsa1024_pkcs8.pem", + }, + { + file: "../test/private_ed25519_pkcs8.pem", + }, + { + file: "../test/private_secp256k1_asn1_echeader.pem", + }, + { + file: "../test/private_secp256k1_asn1.pem", + }, + { + file: "../test/private_secp256r1_asn1_echeader.pem", + }, + { + file: "../test/private_secp256r1_asn1.pem", + }, + { + file: "../test/private_secp256r1_pkcs8.pem", + }, + { + file: "../test/private_invalid_key.pem", + err: "failed to decode PEM block containing private key\nasn1: structure error: length too large", + }, + } + + for _, testCase := range testCases { + t.Run(path.Base(testCase.file), func(t *testing.T) { + pemData, err := os.ReadFile(testCase.file) + require.NoError(t, err) + expectedKey, err := PemToPrivateKey(pemData) + + if testCase.err != "" { + assert.EqualError(t, err, testCase.err) + } else { + require.NoError(t, err) + require.NotNil(t, expectedKey) + + marshalledPEM, err := PrivateKeyToPem(expectedKey) + require.NoError(t, err) + require.NotNil(t, marshalledPEM) + + actualKey, err := PemToPrivateKey(pemData) + require.NoError(t, err) + require.NotNil(t, actualKey) + require.Equal(t, expectedKey, actualKey) + } + }) + } +} - t.Run("Convert garbage", func(t *testing.T) { +func TestPemToPrivateKey(t *testing.T) { + t.Run("garbage", func(t *testing.T) { _, err := PemToPrivateKey([]byte{}) require.ErrorIs(t, err, ErrWrongPrivateKey) }) diff --git a/docs/_static/vdr/v2.yaml b/docs/_static/vdr/v2.yaml index 309b2da6a3..2316d3d624 100644 --- a/docs/_static/vdr/v2.yaml +++ b/docs/_static/vdr/v2.yaml @@ -255,6 +255,17 @@ components: type: string description: The ID of the DID document. If not given, a random UUID is generated. example: "did:web:example.com:iam:013c6fda-73e8-45ee-9220-48652dba854b" + verificationMethodType: + type: string + description: | + The type of the verification method to be generated. Defaults to JsonWebKey2020 with a P-256 EC key. + See [the did core spec](https://www.w3.org/TR/did-core/#verification-method-types) for more information. + default: JsonWebKey2020 + enum: + - JsonWebKey2020 + - EcdsaSecp256k1VerificationKey2019 + - RsaVerificationKey2018 + - Ed25519VerificationKey2018 DIDDocument: $ref: '../common/ssi_types.yaml#/components/schemas/DIDDocument' DIDDocumentMetadata: diff --git a/go.mod b/go.mod index 2a39fd8470..dd99f6bf28 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/avast/retry-go/v4 v4.5.1 github.com/cbroglie/mustache v1.4.0 github.com/chromedp/chromedp v0.9.3 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 github.com/dlclark/regexp2 v1.10.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/goodsign/monday v1.0.1 @@ -72,7 +73,6 @@ require ( github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998 // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect diff --git a/http/cmd/cmd.go b/http/cmd/cmd.go index 5815a2875d..7b4ed0e14b 100644 --- a/http/cmd/cmd.go +++ b/http/cmd/cmd.go @@ -23,6 +23,7 @@ import ( "fmt" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/nuts-foundation/nuts-node/audit" + nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" cryptoCmd "github.com/nuts-foundation/nuts-node/crypto/cmd" "github.com/nuts-foundation/nuts-node/http" "github.com/nuts-foundation/nuts-node/network" @@ -81,7 +82,7 @@ func createTokenCommand() *cobra.Command { if !instance.Exists(cmd.Context(), http.AdminTokenSigningKID) { cmd.Println("Token signing key not found, generating new key...") - _, err := instance.New(ctx, func(key crypto.PublicKey) (string, error) { + _, err := instance.New(ctx, nutsCrypto.ECP256Key, func(key crypto.PublicKey) (string, error) { return http.AdminTokenSigningKID, nil }) if err != nil { diff --git a/network/dag/transaction.go b/network/dag/transaction.go index 39cdeaf0d1..0c56e5b59d 100644 --- a/network/dag/transaction.go +++ b/network/dag/transaction.go @@ -45,7 +45,7 @@ const ( lamportClockHeader = "lc" ) -var allowedAlgos = []jwa.SignatureAlgorithm{jwa.ES256, jwa.ES384, jwa.ES512, jwa.PS256, jwa.PS384, jwa.PS512} +var allowedAlgos = []jwa.SignatureAlgorithm{jwa.ES256, jwa.ES384, jwa.ES512, jwa.ES256K, jwa.PS256, jwa.PS384, jwa.PS512, jwa.EdDSA} var errInvalidPayloadType = errors.New("payload type must be formatted as MIME type") var errInvalidPrevs = errors.New("prevs contains an empty hash") diff --git a/network/network_integration_test.go b/network/network_integration_test.go index d689dd7231..c1cfd21f29 100644 --- a/network/network_integration_test.go +++ b/network/network_integration_test.go @@ -1008,7 +1008,7 @@ func resetIntegrationTest(t *testing.T) { document := did.Document{ID: nodeDID} kid := did.DIDURL{DID: nodeDID} kid.Fragment = "key-1" - key, _ := keyStore.New(audit.TestContext(), func(_ crypto.PublicKey) (string, error) { + key, _ := keyStore.New(audit.TestContext(), nutsCrypto.ECP256Key, func(_ crypto.PublicKey) (string, error) { return kid.String(), nil }) verificationMethod, _ := did.NewVerificationMethod(kid, ssi.JsonWebKey2020, nodeDID, key.Public()) diff --git a/vcr/issuer/issuer_test.go b/vcr/issuer/issuer_test.go index e70d89c998..c82f994f0b 100644 --- a/vcr/issuer/issuer_test.go +++ b/vcr/issuer/issuer_test.go @@ -73,7 +73,7 @@ func Test_issuer_buildVC(t *testing.T) { }}, } keyStore := crypto.NewMemoryCryptoInstance() - signingKey, err := keyStore.New(ctx, func(key crypt.PublicKey) (string, error) { + signingKey, err := keyStore.New(ctx, crypto.ECP256Key, func(key crypt.PublicKey) (string, error) { return kid, nil }) require.NoError(t, err) diff --git a/vcr/issuer/openid_test.go b/vcr/issuer/openid_test.go index 25875df74a..af9ff69441 100644 --- a/vcr/issuer/openid_test.go +++ b/vcr/issuer/openid_test.go @@ -119,7 +119,7 @@ func Test_memoryIssuer_ProviderMetadata(t *testing.T) { func Test_memoryIssuer_HandleCredentialRequest(t *testing.T) { keyStore := crypto.NewMemoryCryptoInstance() ctx := audit.TestContext() - signerKey, _ := keyStore.New(ctx, func(key crypt.PublicKey) (string, error) { + signerKey, _ := keyStore.New(ctx, crypto.ECP256Key, func(key crypt.PublicKey) (string, error) { return keyID, nil }) ctrl := gomock.NewController(t) diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go index 2b18e30c04..174cad01bc 100644 --- a/vcr/verifier/verifier_test.go +++ b/vcr/verifier/verifier_test.go @@ -89,7 +89,7 @@ func Test_verifier_Validate(t *testing.T) { t.Run("JWT", func(t *testing.T) { // Create did:jwk for issuer, and sign credential keyStore := crypto.NewMemoryCryptoInstance() - key, err := keyStore.New(audit.TestContext(), func(key crypt.PublicKey) (string, error) { + key, err := keyStore.New(audit.TestContext(), crypto.ECP256Key, func(key crypt.PublicKey) (string, error) { keyAsJWK, _ := jwk.FromRaw(key) keyJSON, _ := json.Marshal(keyAsJWK) return "did:jwk:" + base64.RawStdEncoding.EncodeToString(keyJSON) + "#0", nil diff --git a/vdr/api/v2/api.go b/vdr/api/v2/api.go index 7624edb885..4e764dc3c2 100644 --- a/vdr/api/v2/api.go +++ b/vdr/api/v2/api.go @@ -22,6 +22,7 @@ package v2 import ( "context" "github.com/labstack/echo/v4" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" @@ -66,11 +67,14 @@ func (a *Wrapper) Routes(router core.EchoRouter) { })) } -func (w Wrapper) CreateDID(ctx context.Context, _ CreateDIDRequestObject) (CreateDIDResponseObject, error) { +func (w Wrapper) CreateDID(ctx context.Context, request CreateDIDRequestObject) (CreateDIDResponseObject, error) { options := management.DIDCreationOptions{ KeyFlags: management.AssertionMethodUsage | management.CapabilityInvocationUsage | management.KeyAgreementUsage | management.AuthenticationUsage | management.CapabilityDelegationUsage, SelfControl: true, } + if request.Body != nil && request.Body.VerificationMethodType != nil { + options.VerificationMethodType = ssi.KeyType(*request.Body.VerificationMethodType) + } doc, _, err := w.VDR.Create(ctx, didweb.MethodName, options) // if this operation leads to an error, it may return a 500 diff --git a/vdr/api/v2/api_test.go b/vdr/api/v2/api_test.go index c4cb2e85dd..d5b67bb4dc 100644 --- a/vdr/api/v2/api_test.go +++ b/vdr/api/v2/api_test.go @@ -21,7 +21,9 @@ package v2 import ( "context" + "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/vdr/didweb" + "github.com/nuts-foundation/nuts-node/vdr/management" "testing" "github.com/nuts-foundation/go-did/did" @@ -49,6 +51,26 @@ func TestWrapper_CreateDID(t *testing.T) { assert.Equal(t, id, response.(CreateDID200JSONResponse).ID) }) + t.Run("custom VerificationMethodType", func(t *testing.T) { + ctx := newMockContext(t) + ctx.vdr.EXPECT().Create(gomock.Any(), didweb.MethodName, gomock.Any()).DoAndReturn( + func(ctx context.Context, method string, options management.DIDCreationOptions) (*did.Document, crypto.Key, error) { + assert.Equal(t, "Ed25519VerificationKey2018", string(options.VerificationMethodType)) + return didDoc, nil, nil + }, + ) + + methodType := CreateDIDOptionsVerificationMethodType("Ed25519VerificationKey2018") + response, err := ctx.client.CreateDID(nil, CreateDIDRequestObject{ + Body: &CreateDIDJSONRequestBody{ + VerificationMethodType: &methodType, + }, + }) + + require.NoError(t, err) + assert.Equal(t, id, response.(CreateDID200JSONResponse).ID) + }) + t.Run("error - create fails", func(t *testing.T) { ctx := newMockContext(t) ctx.vdr.EXPECT().Create(gomock.Any(), didweb.MethodName, gomock.Any()).Return(nil, nil, assert.AnError) diff --git a/vdr/api/v2/generated.go b/vdr/api/v2/generated.go index adc806d164..779e2aa3da 100644 --- a/vdr/api/v2/generated.go +++ b/vdr/api/v2/generated.go @@ -22,12 +22,28 @@ const ( JwtBearerAuthScopes = "jwtBearerAuth.Scopes" ) +// Defines values for CreateDIDOptionsVerificationMethodType. +const ( + EcdsaSecp256k1VerificationKey2019 CreateDIDOptionsVerificationMethodType = "EcdsaSecp256k1VerificationKey2019" + Ed25519VerificationKey2018 CreateDIDOptionsVerificationMethodType = "Ed25519VerificationKey2018" + JsonWebKey2020 CreateDIDOptionsVerificationMethodType = "JsonWebKey2020" + RsaVerificationKey2018 CreateDIDOptionsVerificationMethodType = "RsaVerificationKey2018" +) + // CreateDIDOptions defines model for CreateDIDOptions. type CreateDIDOptions struct { // Id The ID of the DID document. If not given, a random UUID is generated. Id *string `json:"id,omitempty"` + + // VerificationMethodType The type of the verification method to be generated. Defaults to JsonWebKey2020 with a P-256 EC key. + // See [the did core spec](https://www.w3.org/TR/did-core/#verification-method-types) for more information. + VerificationMethodType *CreateDIDOptionsVerificationMethodType `json:"verificationMethodType,omitempty"` } +// CreateDIDOptionsVerificationMethodType The type of the verification method to be generated. Defaults to JsonWebKey2020 with a P-256 EC key. +// See [the did core spec](https://www.w3.org/TR/did-core/#verification-method-types) for more information. +type CreateDIDOptionsVerificationMethodType string + // DIDResolutionResult defines model for DIDResolutionResult. type DIDResolutionResult struct { // Document A DID document according to the W3C spec following the Nuts Method rules as defined in [Nuts RFC006] diff --git a/vdr/didnuts/ambassador_test.go b/vdr/didnuts/ambassador_test.go index d7df3c624d..6d78a347cd 100644 --- a/vdr/didnuts/ambassador_test.go +++ b/vdr/didnuts/ambassador_test.go @@ -53,7 +53,7 @@ type mockKeyCreator struct { } // New creates a new valid key with the correct KID -func (m *mockKeyCreator) New(_ context.Context, fn crypto.KIDNamingFunc) (crypto.Key, error) { +func (m *mockKeyCreator) New(_ context.Context, _ crypto.KeyType, fn crypto.KIDNamingFunc) (crypto.Key, error) { if m.key == nil { privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) kid, _ := fn(privateKey.Public()) diff --git a/vdr/didnuts/creator.go b/vdr/didnuts/creator.go index a271b29a9a..e526c11bcd 100644 --- a/vdr/didnuts/creator.go +++ b/vdr/didnuts/creator.go @@ -138,7 +138,8 @@ func (n Creator) Create(ctx context.Context, options management.DIDCreationOptio // Currently, always keep the key in the keystore. This allows us to change the transaction format and regenerate transactions at a later moment. // Relevant issue: // https://github.com/nuts-foundation/nuts-node/issues/1947 - key, err = n.KeyStore.New(ctx, didKIDNamingFunc) + const keyType = nutsCrypto.ECP256Key + key, err = n.KeyStore.New(ctx, keyType, didKIDNamingFunc) // } else { // key, err = nutsCrypto.NewEphemeralKey(didKIDNamingFunc) // } @@ -170,7 +171,7 @@ func (n Creator) Create(ctx context.Context, options management.DIDCreationOptio } } else { // Generate new key for other key capabilities, store the private key - capKey, err := n.KeyStore.New(ctx, didSubKIDNamingFunc(didID)) + capKey, err := n.KeyStore.New(ctx, keyType, didSubKIDNamingFunc(didID)) if err != nil { return nil, nil, err } diff --git a/vdr/didnuts/creator_test.go b/vdr/didnuts/creator_test.go index 9eb4cec779..71401580da 100644 --- a/vdr/didnuts/creator_test.go +++ b/vdr/didnuts/creator_test.go @@ -112,7 +112,7 @@ func TestCreator_Create(t *testing.T) { keyCreator := nutsCrypto.NewMockKeyCreator(ctrl) creator := Creator{KeyStore: keyCreator} - keyCreator.EXPECT().New(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, fn nutsCrypto.KIDNamingFunc) (nutsCrypto.Key, error) { + keyCreator.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, fn nutsCrypto.KIDNamingFunc) (nutsCrypto.Key, error) { key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) keyName, _ := fn(key.Public()) return nutsCrypto.TestKey{ @@ -161,7 +161,7 @@ func TestCreator_Create(t *testing.T) { ctrl := gomock.NewController(t) mockKeyStore := nutsCrypto.NewMockKeyStore(ctrl) creator := Creator{KeyStore: mockKeyStore} - mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any()).Return(nil, errors.New("b00m!")) + mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("b00m!")) _, _, err := creator.Create(nil, DefaultCreationOptions()) @@ -176,7 +176,7 @@ func TestCreator_Create(t *testing.T) { KeyFlags: management.AssertionMethodUsage, SelfControl: false, } - mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any()).Return(nil, errors.New("b00m!")) + mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("b00m!")) _, _, err := creator.Create(nil, ops) diff --git a/vdr/didnuts/manipulator.go b/vdr/didnuts/manipulator.go index 4702cee121..c92e61f5c1 100644 --- a/vdr/didnuts/manipulator.go +++ b/vdr/didnuts/manipulator.go @@ -94,7 +94,7 @@ func (u Manipulator) RemoveVerificationMethod(ctx context.Context, id did.DID, k // CreateNewVerificationMethodForDID creates a new VerificationMethod of type JsonWebKey2020 // with a freshly generated key for a given DID. func CreateNewVerificationMethodForDID(ctx context.Context, id did.DID, keyCreator nutsCrypto.KeyCreator) (*did.VerificationMethod, error) { - key, err := keyCreator.New(ctx, didSubKIDNamingFunc(id)) + key, err := keyCreator.New(ctx, nutsCrypto.ECP256Key, didSubKIDNamingFunc(id)) if err != nil { return nil, err } diff --git a/vdr/didweb/manager.go b/vdr/didweb/manager.go index 8e70c638a4..e98baf8a0f 100644 --- a/vdr/didweb/manager.go +++ b/vdr/didweb/manager.go @@ -54,16 +54,19 @@ type Manager struct { } // Create creates a new did:web document. -func (m Manager) Create(ctx context.Context, _ management.DIDCreationOptions) (*did.Document, crypto.Key, error) { - return m.create(ctx, uuid.NewString()) +func (m Manager) Create(ctx context.Context, options management.DIDCreationOptions) (*did.Document, crypto.Key, error) { + if options.VerificationMethodType == "" { + options.VerificationMethodType = ssi.JsonWebKey2020 + } + return m.create(ctx, uuid.NewString(), options.VerificationMethodType) } -func (m Manager) create(ctx context.Context, mostSignificantBits string) (*did.Document, crypto.Key, error) { +func (m Manager) create(ctx context.Context, mostSignificantBits string, methodType ssi.KeyType) (*did.Document, crypto.Key, error) { newDID, err := URLToDID(*m.baseURL.JoinPath(mostSignificantBits)) if err != nil { return nil, nil, err } - verificationMethodKey, verificationMethod, err := m.createVerificationMethod(ctx, *newDID) + verificationMethodKey, verificationMethod, err := m.createVerificationMethod(ctx, *newDID, methodType) if err != nil { return nil, nil, err } @@ -75,18 +78,22 @@ func (m Manager) create(ctx context.Context, mostSignificantBits string) (*did.D return &document, verificationMethodKey, nil } -func (m Manager) createVerificationMethod(ctx context.Context, ownerDID did.DID) (crypto.Key, *did.VerificationMethod, error) { +func (m Manager) createVerificationMethod(ctx context.Context, ownerDID did.DID, methodType ssi.KeyType) (crypto.Key, *did.VerificationMethod, error) { verificationMethodID := did.DIDURL{ DID: ownerDID, Fragment: "0", // TODO: Which fragment should we use? Thumbprint, UUID, index, etc... } - verificationMethodKey, err := m.keyStore.New(ctx, func(key crypt.PublicKey) (string, error) { + keyType, err := cryptoKeyType(methodType) + if err != nil { + return nil, nil, err + } + verificationMethodKey, err := m.keyStore.New(ctx, keyType, func(key crypt.PublicKey) (string, error) { return verificationMethodID.String(), nil }) if err != nil { return nil, nil, err } - verificationMethod, err := did.NewVerificationMethod(verificationMethodID, ssi.JsonWebKey2020, ownerDID, verificationMethodKey.Public()) + verificationMethod, err := did.NewVerificationMethod(verificationMethodID, methodType, ownerDID, verificationMethodKey.Public()) if err != nil { return nil, nil, err } @@ -137,3 +144,20 @@ func buildDocument(subject did.DID, verificationMethods []did.VerificationMethod } return document } + +func cryptoKeyType(verificationMethodType ssi.KeyType) (crypto.KeyType, error) { + var keyType crypto.KeyType + switch verificationMethodType { + case ssi.JsonWebKey2020: + keyType = crypto.ECP256Key + case ssi.ECDSASECP256K1VerificationKey2019: + keyType = crypto.ECP256k1Key + case ssi.ED25519VerificationKey2018: + keyType = crypto.Ed25519Key + case ssi.RSAVerificationKey2018: + keyType = crypto.RSA2048Key + default: + return "", fmt.Errorf("unsupported verification method type: %s", verificationMethodType) + } + return keyType, nil +} diff --git a/vdr/didweb/manager_test.go b/vdr/didweb/manager_test.go index 09f1306bba..e06864a9ae 100644 --- a/vdr/didweb/manager_test.go +++ b/vdr/didweb/manager_test.go @@ -22,6 +22,7 @@ import ( "crypto" "encoding/json" "github.com/lestrrat-go/jwx/v2/jwk" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" @@ -55,12 +56,12 @@ func TestManager_Create(t *testing.T) { resetStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) keyStore := nutsCrypto.NewMockKeyStore(ctrl) - keyStore.EXPECT().New(gomock.Any(), gomock.Any()).Return(nutsCrypto.TestPublicKey{ + keyStore.EXPECT().New(gomock.Any(), nutsCrypto.ECP256Key, gomock.Any()).Return(nutsCrypto.TestPublicKey{ PublicKey: publicKey, }, nil) m := NewManager(*baseURL, keyStore, storageEngine.GetSQLDatabase()) - document, key, err := m.create(audit.TestContext(), "e9d4b80d-59eb-4f35-ada8-c75f6e14bbc4") + document, key, err := m.create(audit.TestContext(), "e9d4b80d-59eb-4f35-ada8-c75f6e14bbc4", ssi.JsonWebKey2020) require.NoError(t, err) require.NotNil(t, document) require.NotNil(t, key) diff --git a/vdr/management/management.go b/vdr/management/management.go index 76d53eb145..abef463946 100644 --- a/vdr/management/management.go +++ b/vdr/management/management.go @@ -20,6 +20,7 @@ package management import ( "context" + ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/crypto" ) @@ -80,6 +81,9 @@ type DIDCreationOptions struct { // SelfControl indicates whether the generated DID Document can be altered with its own capabilityInvocation key. // Defaults to true when not given. SelfControl bool + + // VerificationMethodType specifies the type of verification method to generate. + VerificationMethodType ssi.KeyType } // DIDKeyFlags is a bitmask used for specifying for what purposes a key in a DID document can be used (a.k.a. Verification Method relationships). diff --git a/vdr/test/integration_test.go b/vdr/test/integration_test.go new file mode 100644 index 0000000000..059e3120fe --- /dev/null +++ b/vdr/test/integration_test.go @@ -0,0 +1,88 @@ +package test + +import ( + crypt "crypto" + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/nuts-node/audit" + "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/network" + "github.com/nuts-foundation/nuts-node/storage" + "github.com/nuts-foundation/nuts-node/vdr" + "github.com/nuts-foundation/nuts-node/vdr/didweb" + "github.com/nuts-foundation/nuts-node/vdr/management" + "github.com/stretchr/testify/require" + "testing" +) + +func TestVerificationMethodTypes(t *testing.T) { + keyStore := crypto.NewMemoryCryptoInstance() + networkInstance := network.NewTestNetworkInstance(t) + storageEngine := storage.NewTestStorageEngine(t) + module := vdr.NewVDR(keyStore, networkInstance, nil, nil, storageEngine) + cfg := core.NewServerConfig() + cfg.URL = "https://example.com" + err := module.Configure(*cfg) + require.NoError(t, err) + + type testCase struct { + name string + VerificationMethodType ssi.KeyType + ExpectedVerificationMethodType ssi.KeyType + } + testCases := []testCase{ + { + name: "default", + ExpectedVerificationMethodType: ssi.JsonWebKey2020, + }, + { + name: "JsonWebKey2020", + VerificationMethodType: ssi.JsonWebKey2020, + }, + { + name: "EcdsaSecp256k1VerificationKey2019", + VerificationMethodType: ssi.ECDSASECP256K1VerificationKey2019, + }, + { + name: "Ed25519VerificationKey2018", + VerificationMethodType: ssi.ED25519VerificationKey2018, + }, + // go-did VerificationMethod.PublicKey() is missing support for RsaVerificationKey2018 + //{ + // name: "RsaVerificationKey2018", + // VerificationMethodType: ssi.RSAVerificationKey2018, + //}, + } + + ctx := audit.TestContext() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create DID document + opts := management.DIDCreationOptions{VerificationMethodType: tc.VerificationMethodType} + document, key, err := module.Create(ctx, didweb.MethodName, opts) + require.NoError(t, err) + require.NotNil(t, key) + require.NotNil(t, document) + + // Assert right verification method is created + expected := tc.ExpectedVerificationMethodType + if expected == "" { + expected = tc.VerificationMethodType + } + method := document.VerificationMethod[0] + require.Equal(t, expected, method.Type) + publicKey, err := method.PublicKey() + require.NoError(t, err) + require.NotNil(t, publicKey) + + // Sign and verify signature + token, err := keyStore.SignJWT(ctx, nil, nil, key) + require.NoError(t, err) + parsedToken, err := crypto.ParseJWT(token, func(kid string) (crypt.PublicKey, error) { + return publicKey, nil + }) + require.NoError(t, err) + require.NotNil(t, parsedToken) + }) + } +} diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index 6506a30def..d236f178bd 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -206,7 +206,7 @@ func TestVDR_Create(t *testing.T) { test := newVDRTestCtx(t) expectedPayload, _ := json.Marshal(DIDDocument) - test.mockKeyStore.EXPECT().New(test.ctx, gomock.Any()).Return(key, nil) + test.mockKeyStore.EXPECT().New(test.ctx, crypto.ECP256Key, gomock.Any()).Return(key, nil) test.mockNetwork.EXPECT().CreateTransaction(test.ctx, network.TransactionTemplate(expectedPayloadType, expectedPayload, key).WithAttachKey().WithAdditionalPrevs([]hash.SHA256Hash{})) didDoc, key, err := test.vdr.Create(test.ctx, didnuts.MethodName, didnuts.DefaultCreationOptions()) @@ -228,7 +228,7 @@ func TestVDR_Create(t *testing.T) { KeyFlags: management.AssertionMethodUsage | management.CapabilityInvocationUsage | management.KeyAgreementUsage, SelfControl: true, } - test.mockKeyStore.EXPECT().New(test.ctx, gomock.Any()).Return(key, nil) + test.mockKeyStore.EXPECT().New(test.ctx, crypto.ECP256Key, gomock.Any()).Return(key, nil) test.mockStore.EXPECT().Resolve(controllerID, gomock.Any()).Return(&controllerDocument, &resolver.DocumentMetadata{SourceTransactions: refs}, nil) test.mockNetwork.EXPECT().CreateTransaction(test.ctx, network.TransactionTemplate(expectedPayloadType, expectedPayload, key).WithAttachKey().WithAdditionalPrevs(refs)) @@ -255,7 +255,7 @@ func TestVDR_Create(t *testing.T) { t.Run("error - doc creation", func(t *testing.T) { test := newVDRTestCtx(t) - test.mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any()).Return(nil, errors.New("b00m!")) + test.mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("b00m!")) _, _, err := test.vdr.Create(test.ctx, didnuts.MethodName, didnuts.DefaultCreationOptions()) @@ -265,7 +265,7 @@ func TestVDR_Create(t *testing.T) { t.Run("error - transaction failed", func(t *testing.T) { test := newVDRTestCtx(t) key := crypto.NewTestKey("did:nuts:123#key-1") - test.mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any()).Return(key, nil) + test.mockKeyStore.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(key, nil) test.mockNetwork.EXPECT().CreateTransaction(gomock.Any(), gomock.Any()).Return(nil, errors.New("b00m!")) _, _, err := test.vdr.Create(test.ctx, didnuts.MethodName, didnuts.DefaultCreationOptions()) @@ -343,7 +343,7 @@ func TestVDR_ConflictingDocuments(t *testing.T) { client := crypto.NewMemoryCryptoInstance() keyID := did.DIDURL{DID: TestDIDA} keyID.Fragment = "1" - _, _ = client.New(audit.TestContext(), crypto.StringNamingFunc(keyID.String())) + _, _ = client.New(audit.TestContext(), crypto.ECP256Key, crypto.StringNamingFunc(keyID.String())) vdr := NewVDR(client, nil, didstore.NewTestStore(t), nil, storage.NewTestStorageEngine(t)) _ = vdr.Configure(*core.NewServerConfig()) //vdr.didResolver.Register(didnuts.MethodName, didnuts.Resolver{Store: vdr.store}) @@ -363,14 +363,14 @@ func TestVDR_ConflictingDocuments(t *testing.T) { // vendor test := newVDRTestCtx(t) keyVendor := crypto.NewTestKey("did:nuts:vendor#keyVendor-1") - test.mockKeyStore.EXPECT().New(test.ctx, gomock.Any()).Return(keyVendor, nil) + test.mockKeyStore.EXPECT().New(test.ctx, gomock.Any(), gomock.Any()).Return(keyVendor, nil) test.mockNetwork.EXPECT().CreateTransaction(test.ctx, gomock.Any()).AnyTimes() didDocVendor, keyVendor, err := test.vdr.Create(test.ctx, didnuts.MethodName, didnuts.DefaultCreationOptions()) require.NoError(t, err) // organization keyOrg := crypto.NewTestKey("did:nuts:org#keyOrg-1") - test.mockKeyStore.EXPECT().New(test.ctx, gomock.Any()).Return(keyOrg, nil).Times(2) + test.mockKeyStore.EXPECT().New(test.ctx, gomock.Any(), gomock.Any()).Return(keyOrg, nil).Times(2) test.mockStore.EXPECT().Resolve(didDocVendor.ID, nil).Return(didDocVendor, &resolver.DocumentMetadata{}, nil) didDocOrg, keyOrg, err := test.vdr.Create(test.ctx, didnuts.MethodName, management.DIDCreationOptions{ Controllers: []did.DID{didDocVendor.ID}, @@ -380,8 +380,8 @@ func TestVDR_ConflictingDocuments(t *testing.T) { require.NoError(t, err) client := crypto.NewMemoryCryptoInstance() - _, _ = client.New(audit.TestContext(), crypto.StringNamingFunc(keyVendor.KID())) - _, _ = client.New(audit.TestContext(), crypto.StringNamingFunc(keyOrg.KID())) + _, _ = client.New(audit.TestContext(), crypto.ECP256Key, crypto.StringNamingFunc(keyVendor.KID())) + _, _ = client.New(audit.TestContext(), crypto.ECP256Key, crypto.StringNamingFunc(keyOrg.KID())) vdr := NewVDR(client, nil, didstore.NewTestStore(t), nil, storage.NewTestStorageEngine(t)) _ = vdr.Configure(*core.NewServerConfig()) vdr.didResolver.Register(didnuts.MethodName, didnuts.Resolver{Store: vdr.store})