diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60709304d..28a08dccd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ When you're ready you may: | Requirement | Tested Version | Installation Instructions | |----------------|----------------|---------------------------------------------------------------------------------------------------| -| Go | 1.20.5 | [go.dev](https://go.dev/doc/tutorial/compile-install) | +| Go | 1.20.5 | [go.dev](https://go.dev/doc/install) | | Mage | 1.13.0-6 | [magefile.org](https://magefile.org/) | | golangci-lint | 1.52.2 | [golangci-lint.run](https://golangci-lint.run/usage/install/#local-installation) | diff --git a/pkg/server/router/credential_test.go b/pkg/server/router/credential_test.go index e7ab601e8..e4aa9abb1 100644 --- a/pkg/server/router/credential_test.go +++ b/pkg/server/router/credential_test.go @@ -18,6 +18,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/keystore" "github.com/tbd54566975/ssi-service/pkg/service/schema" "github.com/tbd54566975/ssi-service/pkg/storage" "github.com/tbd54566975/ssi-service/pkg/testutil" @@ -199,6 +200,69 @@ func TestCredentialRouter(t *testing.T) { assert.Contains(tt, err.Error(), fmt.Sprintf("credential not found with id: %s", cred.ID)) }) + t.Run("Credential Service Test Revoked Key", func(tt *testing.T) { + s := test.ServiceStorage(t) + assert.NotEmpty(tt, s) + + // Initialize services + serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} + keyStoreService := testKeyStoreService(tt, s) + didService := testDIDService(tt, s, keyStoreService) + schemaService := testSchemaService(tt, s, keyStoreService, didService) + credService, err := credential.NewCredentialService(serviceConfig, s, keyStoreService, didService.GetResolver(), schemaService) + assert.NoError(tt, err) + assert.NotEmpty(tt, credService) + + // Create a DID + controllerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) + assert.NoError(tt, err) + assert.NotEmpty(tt, controllerDID) + didID := controllerDID.DID.ID + + // Create a key controlled by the DID + keyID := "MyKeyId" + privateKey := "2dEPd7mA3aiuh2gky8tTPiCkyMwf8tBNUMZwRzeVxVJnJFGTbdLGUBcx51DCNyFWRjTG9bduvyLRStXSCDMFXULY" + + err = keyStoreService.StoreKey(context.Background(), keystore.StoreKeyRequest{ID: keyID, Type: crypto.Ed25519, Controller: didID, PrivateKeyBase58: privateKey}) + assert.NoError(tt, err) + + // Create a crendential + subject := "did:test:42" + createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: didID, + IssuerKID: keyID, + Subject: subject, + Data: map[string]any{ + "firstName": "Satoshi", + "lastName": "Nakamoto", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdCred) + assert.NotEmpty(tt, createdCred.CredentialJWT) + + // Revoke the key + err = keyStoreService.RevokeKey(context.Background(), keystore.RevokeKeyRequest{ID: keyID}) + assert.NoError(tt, err) + + // Create a crendential with the revoked key, it fails + subject = "did:test:43" + createdCred, err = credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ + Issuer: didID, + IssuerKID: keyID, + Subject: subject, + Data: map[string]any{ + "firstName": "John", + "lastName": "Doe", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + }) + assert.Empty(tt, createdCred) + assert.Error(tt, err) + assert.ErrorContains(tt, err, "cannot use revoked key") + }) + t.Run("Credential Status List Test", func(tt *testing.T) { s := test.ServiceStorage(t) assert.NotEmpty(tt, s) diff --git a/pkg/server/server.go b/pkg/server/server.go index 1ef0e33eb..0f040598a 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -280,6 +280,7 @@ func KeyStoreAPI(rg *gin.RouterGroup, service svcframework.Service) (err error) keyStoreAPI := rg.Group(KeyStorePrefix) keyStoreAPI.PUT("", keyStoreRouter.StoreKey) keyStoreAPI.GET("/:id", keyStoreRouter.GetKeyDetails) + keyStoreAPI.DELETE("/:id", keyStoreRouter.RevokeKey) return } diff --git a/pkg/service/credential/service.go b/pkg/service/credential/service.go index 427740070..d28fa2796 100644 --- a/pkg/service/credential/service.go +++ b/pkg/service/credential/service.go @@ -264,6 +264,9 @@ func (s Service) signCredentialJWT(ctx context.Context, issuerKID string, cred c if gotKey.Controller != cred.Issuer.(string) { return nil, sdkutil.LoggingNewErrorf("key controller<%s> does not match credential issuer<%s> for key<%s>", gotKey.Controller, cred.Issuer, issuerKID) } + if gotKey.Revoked { + return nil, sdkutil.LoggingNewErrorf("cannot use revoked key<%s>", gotKey.ID) + } keyAccess, err := keyaccess.NewJWKKeyAccess(issuerKID, gotKey.ID, gotKey.Key) if err != nil { return nil, errors.Wrapf(err, "creating key access for signing credential with key<%s>", gotKey.ID) diff --git a/pkg/service/keystore/service.go b/pkg/service/keystore/service.go index bae12de45..471a21102 100644 --- a/pkg/service/keystore/service.go +++ b/pkg/service/keystore/service.go @@ -124,7 +124,6 @@ func (s Service) GetKey(ctx context.Context, request GetKeyRequest) (*GetKeyResp }, nil } -// TODO(gabe): expose this endpoint https://github.com/TBD54566975/ssi-service/issues/451 func (s Service) RevokeKey(ctx context.Context, request RevokeKeyRequest) error { logrus.Debugf("revoking key: %+v", request) diff --git a/pkg/service/keystore/service_test.go b/pkg/service/keystore/service_test.go index a8649aad0..e202e1571 100644 --- a/pkg/service/keystore/service_test.go +++ b/pkg/service/keystore/service_test.go @@ -4,9 +4,11 @@ import ( "context" "os" "testing" + "time" "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/crypto/jwx" + "github.com/benbjohnson/clock" "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -94,6 +96,72 @@ func TestEncryptDecryptAllKeyTypes(t *testing.T) { } func TestStoreAndGetKey(t *testing.T) { + keyStore, err := createKeyStoreService(t) + assert.NoError(t, err) + assert.NotEmpty(t, keyStore) + + // store the key + _, privKey, err := crypto.GenerateEd25519Key() + assert.NoError(t, err) + err = keyStore.StoreKey(context.Background(), StoreKeyRequest{ + ID: "test-id", + Type: crypto.Ed25519, + Controller: "test-controller", + PrivateKeyBase58: base58.Encode(privKey), + }) + assert.NoError(t, err) + + // get it back + keyResponse, err := keyStore.GetKey(context.Background(), GetKeyRequest{ID: "test-id"}) + assert.NoError(t, err) + assert.NotEmpty(t, keyResponse) + assert.Equal(t, privKey, keyResponse.Key) + + // make sure can create a signer properly + signer, err := jwx.NewJWXSigner("test-id", "kid", keyResponse.Key) + assert.NoError(t, err) + assert.NotEmpty(t, signer) +} + +func TestRevokeKey(t *testing.T) { + keyStore, err := createKeyStoreService(t) + assert.NoError(t, err) + assert.NotEmpty(t, keyStore) + + // store the key + _, privKey, err := crypto.GenerateEd25519Key() + assert.NoError(t, err) + keyID := "test-revocation-id" + err = keyStore.StoreKey(context.Background(), StoreKeyRequest{ + ID: keyID, + Type: crypto.Ed25519, + Controller: "test-revocation-controller", + PrivateKeyBase58: base58.Encode(privKey), + }) + assert.NoError(t, err) + + // get it back + keyResponse, err := keyStore.GetKey(context.Background(), GetKeyRequest{ID: keyID}) + assert.NoError(t, err) + assert.NotEmpty(t, keyResponse) + assert.Equal(t, privKey, keyResponse.Key) + assert.False(t, keyResponse.Revoked) + assert.Empty(t, keyResponse.RevokedAt) + + // revoke the key + err = keyStore.RevokeKey(context.Background(), RevokeKeyRequest{ID: keyID}) + assert.NoError(t, err) + + // get the key after revocation + keyResponse, err = keyStore.GetKey(context.Background(), GetKeyRequest{ID: keyID}) + assert.NoError(t, err) + assert.NotEmpty(t, keyResponse) + assert.Equal(t, privKey, keyResponse.Key) + assert.True(t, keyResponse.Revoked) + assert.Equal(t, "2023-06-23T00:00:00Z", keyResponse.RevokedAt) +} + +func createKeyStoreService(t *testing.T) (*Service, error) { file, err := os.CreateTemp("", "bolt") require.NoError(t, err) name := file.Name() @@ -119,28 +187,10 @@ func TestStoreAndGetKey(t *testing.T) { MasterKeyPassword: "test-password", }, s) - assert.NoError(t, err) - assert.NotEmpty(t, keyStore) - - // store the key - _, privKey, err := crypto.GenerateEd25519Key() - assert.NoError(t, err) - err = keyStore.StoreKey(context.Background(), StoreKeyRequest{ - ID: "test-id", - Type: crypto.Ed25519, - Controller: "test-controller", - PrivateKeyBase58: base58.Encode(privKey), - }) - assert.NoError(t, err) - // get it back - keyResponse, err := keyStore.GetKey(context.Background(), GetKeyRequest{ID: "test-id"}) - assert.NoError(t, err) - assert.NotEmpty(t, keyResponse) - assert.Equal(t, privKey, keyResponse.Key) + mockClock := clock.NewMock() + mockClock.Set(time.Date(2023, 06, 23, 0, 0, 0, 0, time.UTC)) + keyStore.storage.Clock = mockClock - // make sure can create a signer properly - signer, err := jwx.NewJWXSigner("test-id", "kid", keyResponse.Key) - assert.NoError(t, err) - assert.NotEmpty(t, signer) + return keyStore, err } diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go index b59ee0e5a..199afe662 100644 --- a/pkg/service/keystore/storage.go +++ b/pkg/service/keystore/storage.go @@ -8,6 +8,7 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/crypto/jwx" sdkutil "github.com/TBD54566975/ssi-sdk/util" + "github.com/benbjohnson/clock" "github.com/goccy/go-json" "github.com/google/tink/go/aead" "github.com/google/tink/go/core/registry" @@ -63,6 +64,7 @@ type Storage struct { db storage.ServiceStorage encrypter Encrypter decrypter Decrypter + Clock clock.Clock } func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter) (*Storage, error) { @@ -70,6 +72,7 @@ func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter) (*S db: db, encrypter: e, decrypter: d, + Clock: clock.New(), } return s, nil @@ -290,7 +293,7 @@ func (kss *Storage) RevokeKey(ctx context.Context, id string) error { } key.Revoked = true - key.RevokedAt = time.Now().UTC().Format(time.RFC3339) + key.RevokedAt = kss.Clock.Now().Format(time.RFC3339) return kss.StoreKey(ctx, *key) }