From f836a8114fe3f65f03dd393141f9fe1aed9ca83a Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Wed, 26 Jul 2023 12:39:33 -0400 Subject: [PATCH 01/20] Added app level encryption feature. (#599) * Added app level encryption feature. * Add validation of config parameters. * test * better name * naming * nil encrypter for kek * docs. * integration fix --- cmd/ssiservice/main.go | 5 +- config/config.go | 74 +++++++++- config/config_test.go | 30 ++-- config/dev.toml | 4 + config/prod.toml | 8 +- config/test.toml | 6 + config/testdata/test1.toml | 6 + doc/STORAGE.md | 53 +++++++- pkg/encryption/encryption.go | 164 ++++++++++++++++++++++ pkg/encryption/encryption_test.go | 93 +++++++++++++ pkg/server/router/did_test.go | 16 +++ pkg/server/server_test.go | 10 +- pkg/server/server_webhook_test.go | 2 +- pkg/service/did/web.go | 2 +- pkg/service/keystore/service.go | 40 ++---- pkg/service/keystore/service_test.go | 66 --------- pkg/service/keystore/storage.go | 173 ++++++------------------ pkg/service/manifest/storage/storage.go | 4 +- pkg/service/operation/storage.go | 6 +- pkg/service/presentation/storage.go | 3 +- pkg/service/service.go | 19 ++- pkg/storage/bolt.go | 52 ------- pkg/storage/db_test.go | 25 +++- pkg/storage/encrypt.go | 151 +++++++++++++++++++++ pkg/storage/storage.go | 74 +++++++++- 25 files changed, 764 insertions(+), 322 deletions(-) create mode 100644 config/testdata/test1.toml create mode 100644 pkg/encryption/encryption.go create mode 100644 pkg/encryption/encryption_test.go create mode 100644 pkg/storage/encrypt.go diff --git a/cmd/ssiservice/main.go b/cmd/ssiservice/main.go index 796e13057..560e07086 100644 --- a/cmd/ssiservice/main.go +++ b/cmd/ssiservice/main.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/signal" + "path" "strconv" "syscall" "time" @@ -52,7 +53,9 @@ func run() error { logrus.Infof("loading config from env var path: %s", envConfigPath) configPath = envConfigPath } - cfg, err := config.LoadConfig(configPath) + + dir, file := path.Split(configPath) + cfg, err := config.LoadConfig(file, os.DirFS(dir)) if err != nil { logrus.Fatalf("could not instantiate config: %s", err.Error()) } diff --git a/config/config.go b/config/config.go index 7d1433a1a..370256be4 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "io/fs" "os" "path/filepath" "reflect" @@ -72,6 +73,10 @@ type ServicesConfig struct { StorageOptions []storage.Option `toml:"storage_option"` ServiceEndpoint string `toml:"service_endpoint"` + // Application level encryption configuration. Defines how values are encrypted before they are stored in the + // configured KV store. + AppLevelEncryptionConfiguration EncryptionConfig `toml:"storage_encryption,omitempty"` + // Embed all service-specific configs here. The order matters: from which should be instantiated first, to last KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"` DIDConfig DIDServiceConfig `toml:"did,omitempty"` @@ -94,14 +99,34 @@ type BaseServiceConfig struct { type KeyStoreServiceConfig struct { *BaseServiceConfig - // The URI for the master key. We use tink for envelope encryption as described in https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-with-tink - // When left empty, then a random key is generated and used. + // Configuration describing the encryption of the private keys that are under ssi-service's custody. + EncryptionConfig +} + +type EncryptionConfig struct { + DisableEncryption bool `toml:"disable_encryption"` + + // The URI for a master key. We use tink for envelope encryption as described in https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-with-tink + // When left empty and DisableEncryption is off, then a random key is generated and used. This random key is persisted unencrypted in the + // configured storage. Production deployments should never leave this field empty. MasterKeyURI string `toml:"master_key_uri"` - // Path for credentials. Required when using an external KMS. More info at https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials + // Path for credentials. Required when MasterKeyURI is set. More info at https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials KMSCredentialsPath string `toml:"kms_credentials_path"` } +func (e EncryptionConfig) GetMasterKeyURI() string { + return e.MasterKeyURI +} + +func (e EncryptionConfig) GetKMSCredentialsPath() string { + return e.KMSCredentialsPath +} + +func (e EncryptionConfig) EncryptionEnabled() bool { + return !e.DisableEncryption +} + func (k *KeyStoreServiceConfig) IsEmpty() bool { if k == nil { return true @@ -109,6 +134,18 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool { return reflect.DeepEqual(k, &KeyStoreServiceConfig{}) } +func (k *KeyStoreServiceConfig) GetMasterKeyURI() string { + return k.MasterKeyURI +} + +func (k *KeyStoreServiceConfig) GetKMSCredentialsPath() string { + return k.KMSCredentialsPath +} + +func (k *KeyStoreServiceConfig) EncryptionEnabled() bool { + return !k.DisableEncryption +} + type DIDServiceConfig struct { *BaseServiceConfig Methods []string `toml:"methods"` @@ -211,7 +248,10 @@ func (p *WebhookServiceConfig) IsEmpty() bool { // LoadConfig attempts to load a TOML config file from the given path, and coerce it into our object model. // Before loading, defaults are applied on certain properties, which are overwritten if specified in the TOML file. -func LoadConfig(path string) (*SSIServiceConfig, error) { +func LoadConfig(path string, fs fs.FS) (*SSIServiceConfig, error) { + if fs == nil { + fs = os.DirFS(".") + } loadDefaultConfig, err := checkValidConfigPath(path) if err != nil { return nil, errors.Wrap(err, "validate config path") @@ -226,7 +266,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) { if loadDefaultConfig { defaultServicesConfig := getDefaultServicesConfig() config.Services = defaultServicesConfig - } else if err = loadTOMLConfig(path, &config); err != nil { + } else if err = loadTOMLConfig(path, &config, fs); err != nil { return nil, errors.Wrap(err, "load toml config") } @@ -234,9 +274,25 @@ func LoadConfig(path string) (*SSIServiceConfig, error) { return nil, errors.Wrap(err, "apply env variables") } + if err = validateConfig(&config); err != nil { + return nil, errors.Wrap(err, "validating config values") + } + return &config, nil } +func validateConfig(s *SSIServiceConfig) error { + if s.Server.Environment == EnvironmentProd { + if s.Services.KeyStoreConfig.DisableEncryption { + return errors.New("prod environment cannot disable key encryption") + } + if s.Services.AppLevelEncryptionConfiguration.DisableEncryption { + logrus.Warn("prod environment detected without app level encryption. This is strongly discouraged.") + } + } + return nil +} + func checkValidConfigPath(path string) (bool, error) { // no path, load default config defaultConfig := false @@ -314,9 +370,13 @@ func getDefaultServicesConfig() ServicesConfig { } } -func loadTOMLConfig(path string, config *SSIServiceConfig) error { +func loadTOMLConfig(path string, config *SSIServiceConfig, fs fs.FS) error { // load from TOML file - if _, err := toml.DecodeFile(path, &config); err != nil { + file, err := fs.Open(path) + if err != nil { + return errors.Wrapf(err, "opening path %s", path) + } + if _, err := toml.NewDecoder(file).Decode(&config); err != nil { return errors.Wrapf(err, "could not load config: %s", path) } diff --git a/config/config_test.go b/config/config_test.go index 12cefe6da..d629f9470 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,20 +1,32 @@ package config import ( + "embed" "testing" "github.com/stretchr/testify/assert" ) -func TestConfig(t *testing.T) { - config, err := LoadConfig(Filename) - assert.NoError(t, err) - assert.NotEmpty(t, config) +//go:embed testdata +var testdata embed.FS - assert.False(t, config.Server.ReadTimeout.String() == "") - assert.False(t, config.Server.WriteTimeout.String() == "") - assert.False(t, config.Server.ShutdownTimeout.String() == "") - assert.False(t, config.Server.APIHost == "") +func TestLoadConfig(t *testing.T) { + t.Run("returns no errors when passed in file", func(t *testing.T) { + config, err := LoadConfig(Filename, nil) + assert.NoError(t, err) + assert.NotEmpty(t, config) - assert.NotEmpty(t, config.Services.StorageProvider) + assert.False(t, config.Server.ReadTimeout.String() == "") + assert.False(t, config.Server.WriteTimeout.String() == "") + assert.False(t, config.Server.ShutdownTimeout.String() == "") + assert.False(t, config.Server.APIHost == "") + + assert.NotEmpty(t, config.Services.StorageProvider) + }) + + t.Run("returns errors when prod disables encryption", func(t *testing.T) { + _, err := LoadConfig("testdata/test1.toml", testdata) + assert.Error(t, err) + assert.ErrorContains(t, err, "prod environment cannot disable key encryption") + }) } diff --git a/config/dev.toml b/config/dev.toml index a3f41683f..70e87d59b 100644 --- a/config/dev.toml +++ b/config/dev.toml @@ -29,6 +29,10 @@ service_endpoint = "http://localhost:3000" # example bolt config with filepath option storage = "bolt" +[services.storage_encryption] +# encryption +disable_encryption = true + [[services.storage_option]] id = "boltdb-filepath-option" option = "bolt.db" diff --git a/config/prod.toml b/config/prod.toml index 025a003ee..dc11c4413 100644 --- a/config/prod.toml +++ b/config/prod.toml @@ -20,7 +20,12 @@ log_location = "log" log_level = "info" enable_schema_caching = true -enable_allow_all_cors = true +enable_allow_all_cors = false + +[services.storage_encryption] +# master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" +# kms_credentials_path = "credentials.json" +disable_encryption = false # Storage Configuration [services] @@ -38,6 +43,7 @@ option = "password" # per-service configuration [services.keystore] name = "keystore" +disable_encryption = false # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" # kms_credentials_path = "credentials.json" diff --git a/config/test.toml b/config/test.toml index 141c6a4c6..02399cc05 100644 --- a/config/test.toml +++ b/config/test.toml @@ -25,6 +25,11 @@ log_level = "warn" enable_schema_caching = true enable_allow_all_cors = true +[services.storage_encryption] +# master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" +# kms_credentials_path = "credentials.json" +disable_encryption = false + # Storage Configuration [services] service_endpoint = "http://localhost:8080" @@ -43,6 +48,7 @@ option = "password" name = "keystore" # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" # kms_credentials_path = "credentials.json" +disable_encryption = false [services.did] name = "did" diff --git a/config/testdata/test1.toml b/config/testdata/test1.toml new file mode 100644 index 000000000..f96f2191e --- /dev/null +++ b/config/testdata/test1.toml @@ -0,0 +1,6 @@ +[server] +env = "prod" # either 'dev', 'test', or 'prod' + +[services.keystore] +name = "keystore" +disable_encryption = true \ No newline at end of file diff --git a/doc/STORAGE.md b/doc/STORAGE.md index 7d539f51e..34420f9c4 100644 --- a/doc/STORAGE.md +++ b/doc/STORAGE.md @@ -70,4 +70,55 @@ For a working example, see this [dev.toml file](https://github.com/TBD54566975/s You need to implement the [ServiceStorage interface](../pkg/storage/storage.go), similar to how [Redis](../pkg/storage/redis.go) is implemented. For an example, see [this PR](https://github.com/TBD54566975/ssi-service/pull/590/files#diff-606358579107e7ad1221525001aed8c776a141d4cc5aab9ef7a3ddbcec10d9f9) -which introduces the SQL based implementation. \ No newline at end of file +which introduces the SQL based implementation. + +## Encryption + +SSI Service supports application level encryption of values before sending them to the configured KV store. Please note +that keys (i.e. the key of the KV store) are not currently encrypted. See the [Privacy Considerations](#privacy-considerations) for more information. +A MasterKey is used (a.k.a. a Data Encryption Key or DEK) to encrypt all data before it's sent to the configured storage. +The MasterKey can be stored in the configured storage system or in an external Key Management System (KMS) like GCP KMS or AWS KMS. +When storing locally, the key will be automatically generated if it doesn't exist already. + +**For production deployments, it is strongly recommended to store the MasterKey in an external KMS.** + +To use an external KMS: +1. Create a symmetric encryption key in your KMS. You MUST select the algorithm that uses AES-256 block cipher in Galois/Counter Mode (GCM). At the time of writing, this is the only algorithm supported by AWS and GCP. +2. Set the `master_key_uri` field of the `[services.storage_encryption]` section using the format described in [tink](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-systems) + (we use the tink library under the hood). +3. Set the `kms_credentials_path` field of the `[services.storage_encryption]` section to point to your credentials file, according to [this section](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials). +4. Win! + +Below, there is an example snippet of what the TOML configuration should look like. +```toml +[services.storage_encryption] +# Make sure the following values are valid. +master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" +kms_credentials_path = "credentials.json" +disable_encryption = false +``` + +Storing the MasterKey in the configured storage system is done with the following options in your TOML configuration. + +```toml +[services.storage_encryption] +# ensure that master_key_uri is NOT set. +disable_encryption = false +``` + +Disabling app level encryption is also possible using the following options in your TOML configuration: + +```toml +[services.storage_encryption] +# encryption +disable_encryption = true +``` + +### Privacy Considerations + +From the perspective of SSI-Service, all keys are stored in plaintext (this doesn't preclude configuring encryption at rest +in your deployment of the storage configuration). Making all keys readable by any actor may have an impact in your organization's +use cases around privacy. You should consider whether this is acceptable. Notably, a DID that was created by SSI Service +is stored as a key. This can fit some definition of PII, as it could be correlated to identify and individual. + +Encrypting keys is being considered in https://github.com/TBD54566975/ssi-service/issues/603. \ No newline at end of file diff --git a/pkg/encryption/encryption.go b/pkg/encryption/encryption.go new file mode 100644 index 000000000..64a0feba1 --- /dev/null +++ b/pkg/encryption/encryption.go @@ -0,0 +1,164 @@ +package encryption + +import ( + "context" + "strings" + + sdkutil "github.com/TBD54566975/ssi-sdk/util" + "github.com/google/tink/go/aead" + "github.com/google/tink/go/core/registry" + "github.com/google/tink/go/integration/awskms" + "github.com/google/tink/go/integration/gcpkms" + "github.com/google/tink/go/keyset" + "github.com/google/tink/go/tink" + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/internal/util" + "google.golang.org/api/option" +) + +// Encrypter the interface for any encrypter implementation. +type Encrypter interface { + Encrypt(ctx context.Context, plaintext, contextData []byte) ([]byte, error) +} + +// Decrypter is the interface for any decrypter. May be AEAD or Hybrid. +type Decrypter interface { + // Decrypt decrypts ciphertext. The second parameter may be treated as associated data for AEAD (as abstracted in + // https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfofor HPKE (https://www.rfc-editor.org/rfc/rfc9180.html) + Decrypt(ctx context.Context, ciphertext, contextInfo []byte) ([]byte, error) +} + +type KeyResolver func(ctx context.Context) ([]byte, error) + +type XChaCha20Poly1305Encrypter struct { + keyResolver KeyResolver +} + +func NewXChaCha20Poly1305EncrypterWithKey(key []byte) *XChaCha20Poly1305Encrypter { + return &XChaCha20Poly1305Encrypter{func(ctx context.Context) ([]byte, error) { + return key, nil + }} +} + +func NewXChaCha20Poly1305EncrypterWithKeyResolver(resolver KeyResolver) *XChaCha20Poly1305Encrypter { + return &XChaCha20Poly1305Encrypter{resolver} +} + +func (k XChaCha20Poly1305Encrypter) Encrypt(ctx context.Context, plaintext, _ []byte) ([]byte, error) { + // encrypt key before storing + key, err := k.keyResolver(ctx) + if err != nil { + return nil, errors.Wrap(err, "resolving key") + } + encryptedKey, err := util.XChaCha20Poly1305Encrypt(key, plaintext) + if err != nil { + return nil, sdkutil.LoggingErrorMsgf(err, "could not encrypt key") + } + return encryptedKey, nil +} + +func (k XChaCha20Poly1305Encrypter) Decrypt(ctx context.Context, ciphertext, _ []byte) ([]byte, error) { + if ciphertext == nil { + return nil, nil + } + + key, err := k.keyResolver(ctx) + if err != nil { + return nil, errors.Wrap(err, "resolving key") + } + // decrypt key before unmarshaling + decryptedKey, err := util.XChaCha20Poly1305Decrypt(key, ciphertext) + if err != nil { + return nil, sdkutil.LoggingErrorMsgf(err, "could not decrypt key") + } + + return decryptedKey, nil +} + +var _ Decrypter = (*XChaCha20Poly1305Encrypter)(nil) +var _ Encrypter = (*XChaCha20Poly1305Encrypter)(nil) + +type noopDecrypter struct{} + +func (n noopDecrypter) Decrypt(_ context.Context, ciphertext, _ []byte) ([]byte, error) { + return ciphertext, nil +} + +type noopEncrypter struct{} + +func (n noopEncrypter) Encrypt(_ context.Context, plaintext, _ []byte) ([]byte, error) { + return plaintext, nil +} + +var _ Decrypter = (*noopDecrypter)(nil) +var _ Encrypter = (*noopEncrypter)(nil) + +var ( + NoopDecrypter = noopDecrypter{} + NoopEncrypter = noopEncrypter{} +) + +type wrappedEncrypter struct { + tink.AEAD +} + +func (w wrappedEncrypter) Encrypt(_ context.Context, plaintext, contextData []byte) ([]byte, error) { + return w.AEAD.Encrypt(plaintext, contextData) +} + +var _ Encrypter = (*wrappedEncrypter)(nil) + +type wrappedDecrypter struct { + tink.AEAD +} + +func (w wrappedDecrypter) Decrypt(_ context.Context, ciphertext, contextInfo []byte) ([]byte, error) { + return w.AEAD.Decrypt(ciphertext, contextInfo) +} + +var _ Decrypter = (*wrappedDecrypter)(nil) + +const ( + gcpKMSScheme = "gcp-kms" + awsKMSScheme = "aws-kms" +) + +type ExternalEncryptionConfig interface { + GetMasterKeyURI() string + GetKMSCredentialsPath() string + EncryptionEnabled() bool +} + +func NewExternalEncrypter(ctx context.Context, cfg ExternalEncryptionConfig) (Encrypter, Decrypter, error) { + if !cfg.EncryptionEnabled() { + return NoopEncrypter, NoopDecrypter, nil + } + var client registry.KMSClient + var err error + switch { + case strings.HasPrefix(cfg.GetMasterKeyURI(), gcpKMSScheme): + client, err = gcpkms.NewClientWithOptions(ctx, cfg.GetMasterKeyURI(), option.WithCredentialsFile(cfg.GetKMSCredentialsPath())) + if err != nil { + return nil, nil, errors.Wrap(err, "creating gcp kms client") + } + case strings.HasPrefix(cfg.GetMasterKeyURI(), awsKMSScheme): + client, err = awskms.NewClientWithCredentials(cfg.GetMasterKeyURI(), cfg.GetKMSCredentialsPath()) + if err != nil { + return nil, nil, errors.Wrap(err, "creating aws kms client") + } + default: + return nil, nil, errors.Errorf("master_key_uri value %q is not supported", cfg.GetMasterKeyURI()) + } + // TODO: move client registration to be per request (i.e. when things are encrypted/decrypted). https://github.com/TBD54566975/ssi-service/issues/598 + registry.RegisterKMSClient(client) + dek := aead.AES256GCMKeyTemplate() + kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(cfg.GetMasterKeyURI(), dek)) + if err != nil { + return nil, nil, errors.Wrap(err, "creating keyset handle") + } + a, err := aead.New(kh) + if err != nil { + return nil, nil, errors.Wrap(err, "creating aead from key handl") + } + return wrappedEncrypter{a}, wrappedDecrypter{a}, nil +} diff --git a/pkg/encryption/encryption_test.go b/pkg/encryption/encryption_test.go new file mode 100644 index 000000000..ad1eddbe4 --- /dev/null +++ b/pkg/encryption/encryption_test.go @@ -0,0 +1,93 @@ +package encryption + +import ( + "context" + "testing" + + "github.com/TBD54566975/ssi-sdk/crypto" + sdkutil "github.com/TBD54566975/ssi-sdk/util" + "github.com/mr-tron/base58" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/internal/util" + "golang.org/x/crypto/chacha20poly1305" +) + +func createServiceKey() (key string, err error) { + keyBytes, err := util.GenerateSalt(chacha20poly1305.KeySize) + if err != nil { + err = errors.Wrap(err, "generating bytes for service key") + return "", sdkutil.LoggingError(err) + } + + key = base58.Encode(keyBytes) + return +} + +func TestEncryptDecryptAllKeyTypes(t *testing.T) { + serviceKeyEncoded, err := createServiceKey() + assert.NoError(t, err) + serviceKey, err := base58.Decode(serviceKeyEncoded) + assert.NoError(t, err) + assert.NotEmpty(t, serviceKey) + encrypter := NewXChaCha20Poly1305EncrypterWithKeyResolver(func(ctx context.Context) ([]byte, error) { + return serviceKey, nil + }) + + tests := []struct { + kt crypto.KeyType + }{ + { + kt: crypto.Ed25519, + }, + { + kt: crypto.X25519, + }, + { + kt: crypto.SECP256k1, + }, + { + kt: crypto.P224, + }, + { + kt: crypto.P256, + }, + { + kt: crypto.P384, + }, + { + kt: crypto.P521, + }, + { + kt: crypto.RSA, + }, + } + for _, test := range tests { + t.Run(string(test.kt), func(t *testing.T) { + // generate a new key based on the given key type + _, privKey, err := crypto.GenerateKeyByKeyType(test.kt) + assert.NoError(t, err) + assert.NotEmpty(t, privKey) + + // serialize the key before encryption + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(t, err) + assert.NotEmpty(t, privKeyBytes) + + // encrypt the serviceKey using our service serviceKey + encryptedKey, err := encrypter.Encrypt(context.Background(), privKeyBytes, nil) + assert.NoError(t, err) + assert.NotEmpty(t, encryptedKey) + + // decrypt the serviceKey using our service serviceKey + decryptedKey, err := encrypter.Decrypt(context.Background(), encryptedKey, nil) + assert.NoError(t, err) + assert.NotEmpty(t, decryptedKey) + + // reconstruct the key from its serialized form + privKeyReconstructed, err := crypto.BytesToPrivKey(decryptedKey, test.kt) + assert.NoError(t, err) + assert.EqualValues(t, privKey, privKeyReconstructed) + }) + } +} diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 8952ad9f5..97d25c89d 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/pkg/testutil" + "gopkg.in/h2non/gock.v1" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -189,12 +190,22 @@ func TestDIDRouter(t *testing.T) { assert.ElementsMatch(tt, supported.Methods, []didsdk.Method{didsdk.KeyMethod, didsdk.WebMethod}) + gock.Off() + gock.New("https://example.com"). + Get("/.well-known/did.json"). + Reply(200). + BodyString("") // bad key type createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", Options: createOpts}) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not generate key for did:web") + gock.Off() + gock.New("https://example.com"). + Get("/.well-known/did.json"). + Reply(200). + BodyString("") // good key type createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) assert.NoError(tt, err) @@ -211,6 +222,11 @@ func TestDIDRouter(t *testing.T) { // make sure it's the same value assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) + gock.Off() + gock.New("https://tbd.website"). + Get("/.well-known/did.json"). + Reply(200). + BodyString("") // create a second DID createOpts = did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"} createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 2b1e89c2c..dca596fad 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -11,7 +11,6 @@ import ( "github.com/TBD54566975/ssi-sdk/credential/exchange" "github.com/gin-gonic/gin" - "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/service/issuance" "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" @@ -49,7 +48,7 @@ func TestMain(t *testing.M) { func TestHealthCheckAPI(t *testing.T) { shutdown := make(chan os.Signal, 1) - serviceConfig, err := config.LoadConfig("") + serviceConfig, err := config.LoadConfig("", nil) assert.NoError(t, err) server, err := NewSSIServer(shutdown, *serviceConfig) assert.NoError(t, err) @@ -77,7 +76,7 @@ func TestReadinessAPI(t *testing.T) { }) shutdown := make(chan os.Signal, 1) - serviceConfig, err := config.LoadConfig("") + serviceConfig, err := config.LoadConfig("", nil) assert.NoError(t, err) serviceConfig.Services.StorageOptions = []storage.Option{ { @@ -230,8 +229,9 @@ func testKeyStoreService(t *testing.T, db storage.ServiceStorage) (*keystore.Ser } // create a keystore service - require.NoError(t, keystore.EnsureServiceKeyExists(serviceConfig, db)) - factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db) + encrypter, decrypter, err := keystore.NewServiceEncryption(db, serviceConfig.EncryptionConfig, keystore.ServiceKeyEncryptionKey) + require.NoError(t, err) + factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db, encrypter, decrypter) keystoreService, err := factory(db) require.NoError(t, err) require.NotEmpty(t, keystoreService) diff --git a/pkg/server/server_webhook_test.go b/pkg/server/server_webhook_test.go index ec6f15300..09d7229c3 100644 --- a/pkg/server/server_webhook_test.go +++ b/pkg/server/server_webhook_test.go @@ -48,7 +48,7 @@ func TestSimpleWebhook(t *testing.T) { defer testServer.Close() shutdown := make(chan os.Signal, 1) - serviceConfig, err := config.LoadConfig("") + serviceConfig, err := config.LoadConfig("", nil) assert.NoError(t, err) serviceConfig.Server.APIHost = "0.0.0.0:" + freePort() diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go index 11601c995..158b207e8 100644 --- a/pkg/service/did/web.go +++ b/pkg/service/did/web.go @@ -65,7 +65,7 @@ func (h *webHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* err := didWeb.Validate(ctx) if err != nil { - return nil, errors.Wrap(err, "could not validate did:web") + return nil, errors.Wrap(err, "could not validate if did:web exists externally") } exists, err := h.storage.DIDExists(ctx, opts.DIDWebID) diff --git a/pkg/service/keystore/service.go b/pkg/service/keystore/service.go index a773de0f2..cb8c4b5c2 100644 --- a/pkg/service/keystore/service.go +++ b/pkg/service/keystore/service.go @@ -10,14 +10,13 @@ import ( "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "golang.org/x/crypto/chacha20poly1305" - - "github.com/tbd54566975/ssi-service/internal/keyaccess" - "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/encryption" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/storage" + "golang.org/x/crypto/chacha20poly1305" ) type ServiceFactory func(storage.Tx) (*Service, error) @@ -50,20 +49,17 @@ func (s Service) Config() config.KeyStoreServiceConfig { } func NewKeyStoreService(config config.KeyStoreServiceConfig, s storage.ServiceStorage) (*Service, error) { - if err := EnsureServiceKeyExists(config, s); err != nil { - return nil, sdkutil.LoggingErrorMsg(err, "initializing keystore") + encrypter, decrypter, err := NewServiceEncryption(s, config.EncryptionConfig, ServiceKeyEncryptionKey) + if err != nil { + return nil, errors.Wrap(err, "creating new encryption") } - factory := NewKeyStoreServiceFactory(config, s) + + factory := NewKeyStoreServiceFactory(config, s, encrypter, decrypter) return factory(s) } -func NewKeyStoreServiceFactory(config config.KeyStoreServiceConfig, s storage.ServiceStorage) ServiceFactory { +func NewKeyStoreServiceFactory(config config.KeyStoreServiceConfig, s storage.ServiceStorage, encrypter encryption.Encrypter, decrypter encryption.Decrypter) ServiceFactory { return func(tx storage.Tx) (*Service, error) { - encrypter, decrypter, err := newEncryption(s, config) - if err != nil { - return nil, errors.Wrap(err, "creating new encryption") - } - // Next, instantiate the key storage keyStoreStorage, err := NewKeyStoreStorage(s, encrypter, decrypter, tx) if err != nil { @@ -180,24 +176,6 @@ func GenerateServiceKey() (key string, err error) { return } -// EncryptKey encrypts another key with the service key using xchacha20-poly1305 -func EncryptKey(serviceKey, key []byte) ([]byte, error) { - encryptedKey, err := util.XChaCha20Poly1305Encrypt(serviceKey, key) - if err != nil { - return nil, errors.Wrap(err, "encrypting key with service key") - } - return encryptedKey, nil -} - -// DecryptKey encrypts another key with the service key using xchacha20-poly1305 -func DecryptKey(serviceKey, encryptedKey []byte) ([]byte, error) { - decryptedKey, err := util.XChaCha20Poly1305Decrypt(serviceKey, encryptedKey) - if err != nil { - return nil, errors.Wrap(err, "decrypting key with service key") - } - return decryptedKey, nil -} - // Sign fetches the key in the store, and uses it to sign data. Data should be json or json-serializable. func (s Service) Sign(ctx context.Context, keyID string, data any) (*keyaccess.JWT, error) { gotKey, err := s.GetKey(ctx, GetKeyRequest{ID: keyID}) diff --git a/pkg/service/keystore/service_test.go b/pkg/service/keystore/service_test.go index 16c28ba58..7adf5eafe 100644 --- a/pkg/service/keystore/service_test.go +++ b/pkg/service/keystore/service_test.go @@ -12,7 +12,6 @@ import ( "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -23,71 +22,6 @@ func TestGenerateServiceKey(t *testing.T) { assert.NotEmpty(t, key) } -func TestEncryptDecryptAllKeyTypes(t *testing.T) { - serviceKeyEncoded, err := GenerateServiceKey() - assert.NoError(t, err) - serviceKey, err := base58.Decode(serviceKeyEncoded) - assert.NoError(t, err) - assert.NotEmpty(t, serviceKey) - - tests := []struct { - kt crypto.KeyType - }{ - { - kt: crypto.Ed25519, - }, - { - kt: crypto.X25519, - }, - { - kt: crypto.SECP256k1, - }, - { - kt: crypto.P224, - }, - { - kt: crypto.P256, - }, - { - kt: crypto.P384, - }, - { - kt: crypto.P521, - }, - { - kt: crypto.RSA, - }, - } - for _, test := range tests { - t.Run(string(test.kt), func(t *testing.T) { - // generate a new key based on the given key type - _, privKey, err := crypto.GenerateKeyByKeyType(test.kt) - assert.NoError(t, err) - assert.NotEmpty(t, privKey) - - // serialize the key before encryption - privKeyBytes, err := crypto.PrivKeyToBytes(privKey) - assert.NoError(t, err) - assert.NotEmpty(t, privKeyBytes) - - // encrypt the serviceKey using our service serviceKey - encryptedKey, err := EncryptKey(serviceKey, privKeyBytes) - assert.NoError(t, err) - assert.NotEmpty(t, encryptedKey) - - // decrypt the serviceKey using our service serviceKey - decryptedKey, err := DecryptKey(serviceKey, encryptedKey) - assert.NoError(t, err) - assert.NotEmpty(t, decryptedKey) - - // reconstruct the key from its serialized form - privKeyReconstructed, err := crypto.BytesToPrivKey(decryptedKey, test.kt) - assert.NoError(t, err) - assert.EqualValues(t, privKey, privKeyReconstructed) - }) - } -} - func TestStoreAndGetKey(t *testing.T) { keyStore, err := createKeyStoreService(t) assert.NoError(t, err) diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go index ad25ba1d3..097c893ef 100644 --- a/pkg/service/keystore/storage.go +++ b/pkg/service/keystore/storage.go @@ -2,7 +2,6 @@ package keystore import ( "context" - "strings" "time" "github.com/TBD54566975/ssi-sdk/crypto" @@ -10,19 +9,9 @@ import ( 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" - "github.com/google/tink/go/integration/awskms" - "github.com/google/tink/go/integration/gcpkms" - "github.com/google/tink/go/keyset" - "github.com/google/tink/go/tink" "github.com/mr-tron/base58" "github.com/pkg/errors" - "google.golang.org/api/option" - - "github.com/tbd54566975/ssi-service/config" - - "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/encryption" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -57,24 +46,26 @@ const ( namespace = "keystore" serviceInternalSuffix = "service-internal" publicNamespaceSuffix = "public-keys" - skKey = "ssi-service-key" keyNotFoundErrMsg = "key not found" + + ServiceKeyEncryptionKey = "ssi-service-key-encryption-key" + ServiceDataEncryptionKey = "ssi-service-data-key" ) var ( - serviceNamespace = storage.Join(namespace, serviceInternalSuffix) - publicKeyNamespace = storage.Join(namespace, publicNamespaceSuffix) + serviceInternalNamespace = storage.Join(namespace, serviceInternalSuffix) + publicKeyNamespace = storage.Join(namespace, publicNamespaceSuffix) ) type Storage struct { db storage.ServiceStorage tx storage.Tx - encrypter Encrypter - decrypter Decrypter + encrypter encryption.Encrypter + decrypter encryption.Decrypter Clock clock.Clock } -func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter, writer storage.Tx) (*Storage, error) { +func NewKeyStoreStorage(db storage.ServiceStorage, e encryption.Encrypter, d encryption.Decrypter, writer storage.Tx) (*Storage, error) { s := &Storage{ db: db, encrypter: e, @@ -85,41 +76,26 @@ func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter, wri if writer != nil { s.tx = writer } + if s.encrypter == nil { + s.encrypter = encryption.NoopEncrypter + } + if s.decrypter == nil { + s.decrypter = encryption.NoopDecrypter + } return s, nil } -type wrappedEncrypter struct { - tink.AEAD -} - -func (w wrappedEncrypter) Encrypt(_ context.Context, plaintext, contextData []byte) ([]byte, error) { - return w.AEAD.Encrypt(plaintext, contextData) -} - -type wrappedDecrypter struct { - tink.AEAD -} - -func (w wrappedDecrypter) Decrypt(_ context.Context, ciphertext, contextInfo []byte) ([]byte, error) { - return w.AEAD.Decrypt(ciphertext, contextInfo) -} - -const ( - gcpKMSScheme = "gcp-kms" - awsKMSScheme = "aws-kms" -) - -// EnsureServiceKeyExists makes sure that the service key that will be used for encryption exists. This function is +// ensureEncryptionKeyExists makes sure that the service key that will be used for encryption exists. This function is // idempotent, so that multiple instances of ssi-service can call it on boot. -func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storage.ServiceStorage) error { - if config.MasterKeyURI != "" { +func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provider storage.ServiceStorage, namespace, encryptionMaterialKey string) error { + if config.GetMasterKeyURI() != "" { return nil } watchKeys := []storage.WatchKey{{ Namespace: namespace, - Key: skKey, + Key: encryptionMaterialKey, }} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -127,7 +103,7 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag _, err := provider.Execute(ctx, func(ctx context.Context, tx storage.Tx) (any, error) { // Create the key only if it doesn't already exist. - gotKey, err := getServiceKey(ctx, provider) + gotKey, err := getServiceKey(ctx, provider, namespace, encryptionMaterialKey) if gotKey == nil && err.Error() == keyNotFoundErrMsg { serviceKey, err := GenerateServiceKey() if err != nil { @@ -137,7 +113,7 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag key := ServiceKey{ Base58Key: serviceKey, } - if err := storeServiceKey(ctx, tx, key); err != nil { + if err := storeServiceKey(ctx, tx, key, namespace, encryptionMaterialKey); err != nil { return nil, err } return nil, nil @@ -150,62 +126,41 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag return nil } -// newEncryption creates a pair of Encrypter and Decrypter. The service key must have been created before this function -// is called. EnsureServiceKeyExists can be used to make sure the service key exists. -func newEncryption(db storage.ServiceStorage, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) { - if len(cfg.MasterKeyURI) != 0 { +// NewServiceEncryption creates a pair of Encrypter and Decrypter with the given configuration. +func NewServiceEncryption(db storage.ServiceStorage, cfg encryption.ExternalEncryptionConfig, key string) (encryption.Encrypter, encryption.Decrypter, error) { + if !cfg.EncryptionEnabled() { + return nil, nil, nil + } + + if len(cfg.GetMasterKeyURI()) != 0 { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - return NewExternalEncrypter(ctx, cfg) + return encryption.NewExternalEncrypter(ctx, cfg) } - return &encrypter{db}, &decrypter{db}, nil -} - -func NewExternalEncrypter(ctx context.Context, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) { - var client registry.KMSClient - var err error - switch { - case strings.HasPrefix(cfg.MasterKeyURI, gcpKMSScheme): - client, err = gcpkms.NewClientWithOptions(ctx, cfg.MasterKeyURI, option.WithCredentialsFile(cfg.KMSCredentialsPath)) - if err != nil { - return nil, nil, errors.Wrap(err, "creating gcp kms client") - } - case strings.HasPrefix(cfg.MasterKeyURI, awsKMSScheme): - client, err = awskms.NewClientWithCredentials(cfg.MasterKeyURI, cfg.KMSCredentialsPath) - if err != nil { - return nil, nil, errors.Wrap(err, "creating aws kms client") - } - default: - return nil, nil, errors.Errorf("master_key_uri value %q is not supported", cfg.MasterKeyURI) - } - registry.RegisterKMSClient(client) - dek := aead.AES256GCMKeyTemplate() - kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(cfg.MasterKeyURI, dek)) - if err != nil { - return nil, nil, errors.Wrap(err, "creating keyset handle") - } - a, err := aead.New(kh) - if err != nil { - return nil, nil, errors.Wrap(err, "creating aead from key handl") + if err := ensureEncryptionKeyExists(cfg, db, serviceInternalNamespace, key); err != nil { + return nil, nil, errors.Wrap(err, "ensuring that the encryption key exists") } - return wrappedEncrypter{a}, wrappedDecrypter{a}, nil + encSuite := encryption.NewXChaCha20Poly1305EncrypterWithKeyResolver(func(ctx context.Context) ([]byte, error) { + return getServiceKey(ctx, db, serviceInternalNamespace, key) + }) + return encSuite, encSuite, nil } // TODO(gabe): support more robust service key operations, including rotation, and caching -func storeServiceKey(ctx context.Context, tx storage.Tx, key ServiceKey) error { +func storeServiceKey(ctx context.Context, tx storage.Tx, key ServiceKey, namespace string, skKey string) error { keyBytes, err := json.Marshal(key) if err != nil { return sdkutil.LoggingErrorMsg(err, "could not marshal service key") } - if err = tx.Write(ctx, serviceNamespace, skKey, keyBytes); err != nil { + if err = tx.Write(ctx, namespace, skKey, keyBytes); err != nil { return sdkutil.LoggingErrorMsg(err, "could store marshal service key") } return nil } -func getServiceKey(ctx context.Context, db storage.ServiceStorage) ([]byte, error) { - storedKeyBytes, err := db.Read(ctx, serviceNamespace, skKey) +func getServiceKey(ctx context.Context, db storage.ServiceStorage, namespace, skKey string) ([]byte, error) { + storedKeyBytes, err := db.Read(ctx, namespace, skKey) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not get service key") } @@ -226,56 +181,6 @@ func getServiceKey(ctx context.Context, db storage.ServiceStorage) ([]byte, erro return keyBytes, nil } -// Encrypter the interface for any encrypter implementation. -type Encrypter interface { - Encrypt(ctx context.Context, plaintext, contextData []byte) ([]byte, error) -} - -// Decrypter is the interface for any decrypter. May be AEAD or Hybrid. -type Decrypter interface { - // Decrypt decrypts ciphertext. The second parameter may be treated as associated data for AEAD (as abstracted in - // https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfofor HPKE (https://www.rfc-editor.org/rfc/rfc9180.html) - Decrypt(ctx context.Context, ciphertext, contextInfo []byte) ([]byte, error) -} - -type encrypter struct { - db storage.ServiceStorage -} - -func (e encrypter) Encrypt(ctx context.Context, plaintext, _ []byte) ([]byte, error) { - // get service key - serviceKey, err := getServiceKey(ctx, e.db) - if err != nil { - return nil, err - } - // encrypt key before storing - encryptedKey, err := util.XChaCha20Poly1305Encrypt(serviceKey, plaintext) - if err != nil { - return nil, sdkutil.LoggingErrorMsgf(err, "could not encrypt key") - } - return encryptedKey, nil -} - -type decrypter struct { - db storage.ServiceStorage -} - -func (d decrypter) Decrypt(ctx context.Context, ciphertext, _ []byte) ([]byte, error) { - // get service key - serviceKey, err := getServiceKey(ctx, d.db) - if err != nil { - return nil, err - } - - // decrypt key before unmarshaling - decryptedKey, err := util.XChaCha20Poly1305Decrypt(serviceKey, ciphertext) - if err != nil { - return nil, sdkutil.LoggingErrorMsgf(err, "could not decrypt key") - } - - return decryptedKey, nil -} - func (kss *Storage) StoreKey(ctx context.Context, key StoredKey) error { // TODO(gabe): conflict checking on key id id := key.ID diff --git a/pkg/service/manifest/storage/storage.go b/pkg/service/manifest/storage/storage.go index 534f9e30a..e4a4050d5 100644 --- a/pkg/service/manifest/storage/storage.go +++ b/pkg/service/manifest/storage/storage.go @@ -246,7 +246,7 @@ func (ms *Storage) StoreReviewApplication(ctx context.Context, applicationID str if approved { m["status"] = opsubmission.StatusApproved } - if _, err := ms.db.Update(ctx, credential.ApplicationNamespace, applicationID, m); err != nil { + if _, err := storage.Update(ctx, ms.db, credential.ApplicationNamespace, applicationID, m); err != nil { return nil, nil, errors.Wrap(err, "updating application") } @@ -254,7 +254,7 @@ func (ms *Storage) StoreReviewApplication(ctx context.Context, applicationID str return nil, nil, errors.Wrap(err, "storing credential response") } - responseData, operationData, err := ms.db.UpdateValueAndOperation(ctx, responseNamespace, response.ID, + responseData, operationData, err := storage.UpdateValueAndOperation(ctx, ms.db, responseNamespace, response.ID, storage.NewUpdater(m), namespace.FromID(opID), opID, opsubmission.OperationUpdater{UpdaterWithMap: storage.NewUpdater(map[string]any{"done": true})}) if err != nil { diff --git a/pkg/service/operation/storage.go b/pkg/service/operation/storage.go index a49fd5381..c9319eaa7 100644 --- a/pkg/service/operation/storage.go +++ b/pkg/service/operation/storage.go @@ -30,8 +30,9 @@ func (s Storage) CancelOperation(ctx context.Context, id string) (*opstorage.Sto var err error switch { case strings.HasPrefix(id, submission.ParentResource): - _, opData, err = s.db.UpdateValueAndOperation( + _, opData, err = storage.UpdateValueAndOperation( ctx, + s.db, submission.Namespace, opstorage.StatusObjectID(id), storage.NewUpdater(map[string]any{ "status": submission.StatusCancelled, "reason": cancelledReason, @@ -42,8 +43,9 @@ func (s Storage) CancelOperation(ctx context.Context, id string) (*opstorage.Sto }), }) case strings.HasPrefix(id, credential.ParentResource): - _, opData, err = s.db.UpdateValueAndOperation( + _, opData, err = storage.UpdateValueAndOperation( ctx, + s.db, credential.ApplicationNamespace, opstorage.StatusObjectID(id), storage.NewUpdater(map[string]any{ "status": credential.StatusCancelled, "reason": cancelledReason, diff --git a/pkg/service/presentation/storage.go b/pkg/service/presentation/storage.go index 009d33760..431154d15 100644 --- a/pkg/service/presentation/storage.go +++ b/pkg/service/presentation/storage.go @@ -35,8 +35,9 @@ func (ps *Storage) UpdateSubmission(ctx context.Context, id string, approved boo if approved { m["status"] = opsubmission.StatusApproved } - submissionData, operationData, err := ps.db.UpdateValueAndOperation( + submissionData, operationData, err := storage.UpdateValueAndOperation( ctx, + ps.db, opsubmission.Namespace, id, storage.NewUpdater(m), diff --git a/pkg/service/service.go b/pkg/service/service.go index a93e61e2b..d3962bc36 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -4,6 +4,7 @@ import ( "fmt" sdkutil "github.com/TBD54566975/ssi-sdk/util" + "github.com/pkg/errors" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -81,20 +82,30 @@ func validateServiceConfig(config config.ServicesConfig) error { // instantiateServices begins all instantiates and their dependencies func instantiateServices(config config.ServicesConfig) (*SSIService, error) { - storageProvider, err := storage.NewStorage(storage.Type(config.StorageProvider), config.StorageOptions...) + unencryptedStorageProvider, err := storage.NewStorage(storage.Type(config.StorageProvider), config.StorageOptions...) if err != nil { return nil, sdkutil.LoggingErrorMsgf(err, "could not instantiate storage provider: %s", config.StorageProvider) } + storageEncrypter, storageDecrypter, err := keystore.NewServiceEncryption(unencryptedStorageProvider, config.AppLevelEncryptionConfiguration, keystore.ServiceDataEncryptionKey) + if err != nil { + return nil, errors.Wrap(err, "creating app level encrypter") + } + storageProvider := unencryptedStorageProvider + if storageEncrypter != nil && storageDecrypter != nil { + storageProvider = storage.NewEncryptedWrapper(unencryptedStorageProvider, storageEncrypter, storageDecrypter) + } + webhookService, err := webhook.NewWebhookService(config.WebhookConfig, storageProvider) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the webhook service") } - if err := keystore.EnsureServiceKeyExists(config.KeyStoreConfig, storageProvider); err != nil { - return nil, sdkutil.LoggingErrorMsg(err, "could not ensure the service key exists") + keyEncrypter, keyDecrypter, err := keystore.NewServiceEncryption(unencryptedStorageProvider, config.KeyStoreConfig.EncryptionConfig, keystore.ServiceKeyEncryptionKey) + if err != nil { + return nil, errors.Wrap(err, "creating keystore encrypter") } - keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider) + keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider, keyEncrypter, keyDecrypter) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the keystore service factory") } diff --git a/pkg/storage/bolt.go b/pkg/storage/bolt.go index b7705a45e..2b964432a 100644 --- a/pkg/storage/bolt.go +++ b/pkg/storage/bolt.go @@ -355,55 +355,3 @@ type ResponseSettingUpdater interface { // SetUpdatedResponse sets the response that the Update method will later use to modify the data. SetUpdatedResponse([]byte) } - -// UpdateValueAndOperation updates the value stored in (namespace,key) with the new values specified in the map. -// The updated value is then stored inside the (opNamespace, opKey), and the "done" value is set to true. -func (b *BoltDB) UpdateValueAndOperation(_ context.Context, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) { - err = b.db.Update(func(tx *bolt.Tx) error { - if err = updateTxFn(namespace, key, updater, &first)(tx); err != nil { - return err - } - opUpdater.SetUpdatedResponse(first) - return updateTxFn(opNamespace, opKey, opUpdater, &op)(tx) - }) - return first, op, err -} - -func (b *BoltDB) Update(_ context.Context, namespace string, key string, values map[string]any) ([]byte, error) { - var updatedData []byte - err := b.db.Update(updateTxFn(namespace, key, NewUpdater(values), &updatedData)) - return updatedData, err -} - -func updateTxFn(namespace string, key string, updater Updater, updatedData *[]byte) func(tx *bolt.Tx) error { - return func(tx *bolt.Tx) error { - data, err := updateTx(tx, namespace, key, updater) - if err != nil { - return err - } - *updatedData = data - return nil - } -} - -func updateTx(tx *bolt.Tx, namespace string, key string, updater Updater) ([]byte, error) { - bucket := tx.Bucket([]byte(namespace)) - if bucket == nil { - return nil, sdkutil.LoggingNewErrorf("namespace<%s> does not exist", namespace) - } - v := bucket.Get([]byte(key)) - if v == nil { - return nil, sdkutil.LoggingNewErrorf("key not found %s", key) - } - if err := updater.Validate(v); err != nil { - return nil, sdkutil.LoggingErrorMsg(err, "validating update") - } - data, err := updater.Update(v) - if err != nil { - return nil, err - } - if err = bucket.Put([]byte(key), data); err != nil { - return nil, errors.Wrap(err, "writing to db") - } - return data, nil -} diff --git a/pkg/storage/db_test.go b/pkg/storage/db_test.go index 5c8980b53..17151c9ea 100644 --- a/pkg/storage/db_test.go +++ b/pkg/storage/db_test.go @@ -15,6 +15,7 @@ import ( "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/pkg/encryption" ) func getDBImplementations(t *testing.T) []ServiceStorage { @@ -29,6 +30,13 @@ func getDBImplementations(t *testing.T) []ServiceStorage { postgresDB := setupPostgresDB(t) dbImpls = append(dbImpls, postgresDB) + key := make([]byte, 32) + dbImpls = append(dbImpls, NewEncryptedWrapper( + boltDB, + encryption.NewXChaCha20Poly1305EncrypterWithKey(key), + encryption.NewXChaCha20Poly1305EncrypterWithKey(key), + )) + return dbImpls } @@ -498,7 +506,7 @@ func TestDB_Update(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - data, err = db.Update(context.Background(), namespace, tt.args.key, tt.args.values) + data, err = Update(context.Background(), db, namespace, tt.args.key, tt.args.values) if !tt.expectedError(t, err) { return } @@ -520,6 +528,19 @@ func (f testOpUpdater) SetUpdatedResponse(bytes []byte) { f.UpdaterWithMap.Values["response"] = bytes } +func TestDB_Execute(t *testing.T) { + for _, dbImpl := range getDBImplementations(t) { + db := dbImpl + _, err := db.Execute(context.Background(), func(ctx context.Context, tx Tx) (any, error) { + return nil, tx.Write(ctx, "hello", "my_key", []byte(`some bytes`)) + }, nil) + assert.NoError(t, err) + result, err := db.Read(context.Background(), "hello", "my_key") + assert.NoError(t, err) + assert.Equal(t, []byte(`some bytes`), result) + } +} + func TestDB_UpdatedSubmissionAndOperationTxFn(t *testing.T) { for _, dbImpl := range getDBImplementations(t) { db := dbImpl @@ -609,7 +630,7 @@ func TestDB_UpdatedSubmissionAndOperationTxFn(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotFirstData, gotOpData, err := db.UpdateValueAndOperation(context.Background(), tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey, testOpUpdater{ + gotFirstData, gotOpData, err := UpdateValueAndOperation(context.Background(), db, tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey, testOpUpdater{ NewUpdater(map[string]any{ "done": true, }), diff --git a/pkg/storage/encrypt.go b/pkg/storage/encrypt.go new file mode 100644 index 000000000..006f6ac13 --- /dev/null +++ b/pkg/storage/encrypt.go @@ -0,0 +1,151 @@ +package storage + +import ( + "context" + + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/pkg/encryption" +) + +type EncryptedWrapper struct { + s ServiceStorage + encrypter encryption.Encrypter + decrypter encryption.Decrypter +} + +func NewEncryptedWrapper(s ServiceStorage, encrypter encryption.Encrypter, decrypter encryption.Decrypter) *EncryptedWrapper { + return &EncryptedWrapper{ + s: s, + encrypter: encrypter, + decrypter: decrypter, + } +} + +func (e EncryptedWrapper) Init(opts ...Option) error { + return e.s.Init(opts...) +} + +func (e EncryptedWrapper) Type() Type { + return e.s.Type() +} + +func (e EncryptedWrapper) URI() string { + return e.s.URI() +} + +func (e EncryptedWrapper) IsOpen() bool { + return e.s.IsOpen() +} + +func (e EncryptedWrapper) Close() error { + return e.s.Close() +} + +func (e EncryptedWrapper) Write(ctx context.Context, namespace, key string, value []byte) error { + encryptedData, err := e.encrypter.Encrypt(ctx, value, nil) + if err != nil { + return errors.Wrap(err, "encrypting data") + } + return e.s.Write(ctx, namespace, key, encryptedData) +} + +func (e EncryptedWrapper) WriteMany(ctx context.Context, namespace, keys []string, values [][]byte) error { + encryptedValues := make([][]byte, 0, len(values)) + for _, value := range values { + encryptedData, err := e.encrypter.Encrypt(ctx, value, nil) + if err != nil { + return errors.Wrap(err, "encrypting data") + } + encryptedValues = append(encryptedValues, encryptedData) + } + return e.s.WriteMany(ctx, namespace, keys, encryptedValues) +} + +func (e EncryptedWrapper) Read(ctx context.Context, namespace, key string) ([]byte, error) { + storedBytes, err := e.s.Read(ctx, namespace, key) + if err != nil { + return nil, err + } + decryptedData, err := e.decrypter.Decrypt(ctx, storedBytes, nil) + if err != nil { + return nil, errors.Wrap(err, "decrypting data") + } + return decryptedData, nil +} + +func (e EncryptedWrapper) Exists(ctx context.Context, namespace, key string) (bool, error) { + return e.s.Exists(ctx, namespace, key) +} + +func (e EncryptedWrapper) ReadAll(ctx context.Context, namespace string) (map[string][]byte, error) { + encryptedKeyedBytes, err := e.s.ReadAll(ctx, namespace) + if err != nil { + return nil, err + } + return e.decryptMap(ctx, encryptedKeyedBytes) +} + +func (e EncryptedWrapper) decryptMap(ctx context.Context, encryptedKeyedBytes map[string][]byte) (map[string][]byte, error) { + decryptedValues := make(map[string][]byte, len(encryptedKeyedBytes)) + for key, encryptedBytes := range encryptedKeyedBytes { + decryptedData, err := e.decrypter.Decrypt(ctx, encryptedBytes, nil) + if err != nil { + return nil, errors.Wrap(err, "decrypting data") + } + decryptedValues[key] = decryptedData + } + return decryptedValues, nil +} + +func (e EncryptedWrapper) ReadPage(ctx context.Context, namespace string, pageToken string, pageSize int) (results map[string][]byte, nextPageToken string, err error) { + encryptedResults, nextPageToken, err := e.s.ReadPage(ctx, namespace, pageToken, pageSize) + if err != nil { + return nil, "", err + } + decryptedMap, err := e.decryptMap(ctx, encryptedResults) + if err != nil { + return nil, "", err + } + return decryptedMap, nextPageToken, err +} + +func (e EncryptedWrapper) ReadPrefix(ctx context.Context, namespace, prefix string) (map[string][]byte, error) { + encryptedMap, err := e.s.ReadPrefix(ctx, namespace, prefix) + if err != nil { + return nil, err + } + return e.decryptMap(ctx, encryptedMap) +} + +func (e EncryptedWrapper) ReadAllKeys(ctx context.Context, namespace string) ([]string, error) { + return e.s.ReadAllKeys(ctx, namespace) +} + +func (e EncryptedWrapper) Delete(ctx context.Context, namespace, key string) error { + return e.s.Delete(ctx, namespace, key) +} + +func (e EncryptedWrapper) DeleteNamespace(ctx context.Context, namespace string) error { + return e.s.DeleteNamespace(ctx, namespace) +} + +type encryptedTx struct { + tx Tx + encrypter encryption.Encrypter +} + +func (m encryptedTx) Write(ctx context.Context, namespace, key string, value []byte) error { + encryptedData, err := m.encrypter.Encrypt(ctx, value, nil) + if err != nil { + return errors.Wrap(err, "encrypting data") + } + return m.tx.Write(ctx, namespace, key, encryptedData) +} + +func (e EncryptedWrapper) Execute(ctx context.Context, businessLogicFunc BusinessLogicFunc, watchKeys []WatchKey) (any, error) { + return e.s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) { + return businessLogicFunc(ctx, encryptedTx{tx: tx, encrypter: e.encrypter}) + }, watchKeys) +} + +var _ ServiceStorage = (*EncryptedWrapper)(nil) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 6674a60d7..89131e00f 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -6,6 +6,7 @@ import ( "reflect" "strings" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -71,8 +72,6 @@ type ServiceStorage interface { ReadAllKeys(ctx context.Context, namespace string) ([]string, error) Delete(ctx context.Context, namespace, key string) error DeleteNamespace(ctx context.Context, namespace string) error - Update(ctx context.Context, namespace string, key string, values map[string]any) ([]byte, error) - UpdateValueAndOperation(ctx context.Context, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) Execute(ctx context.Context, businessLogicFunc BusinessLogicFunc, watchKeys []WatchKey) (any, error) } @@ -137,3 +136,74 @@ func Join(parts ...string) string { func MakeNamespace(ns ...string) string { return strings.Join(ns, "-") } + +// UpdateValueAndOperation updates the value stored in (namespace,key) with the new values specified in the map. +// The updated value is then stored inside the (opNamespace, opKey), and the "done" value is set to true. +func UpdateValueAndOperation(ctx context.Context, s ServiceStorage, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) { + type pair struct { + first []byte + second []byte + } + watchKeys := []WatchKey{ + { + Namespace: namespace, + Key: key, + }, + { + Namespace: opNamespace, + Key: opKey, + }, + } + exec, err := s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) { + first, err = update(ctx, s, tx, namespace, key, updater) + if err != nil { + return nil, err + } + opUpdater.SetUpdatedResponse(first) + op, err = update(ctx, s, tx, opNamespace, opKey, opUpdater) + if err != nil { + return nil, err + } + return &pair{first: first, second: op}, err + }, watchKeys) + if err != nil { + return nil, nil, err + } + execPair := exec.(*pair) + return execPair.first, execPair.second, nil +} + +func Update(ctx context.Context, s ServiceStorage, namespace, key string, m map[string]any) ([]byte, error) { + watchKeys := []WatchKey{ + { + Namespace: namespace, + Key: key, + }, + } + exec, err := s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) { + return update(ctx, s, tx, namespace, key, NewUpdater(m)) + }, watchKeys) + if err != nil { + return nil, err + } + execBytes := exec.([]byte) + return execBytes, nil +} + +func update(ctx context.Context, s ServiceStorage, tx Tx, namespace, key string, updater Updater) ([]byte, error) { + readData, err := s.Read(ctx, namespace, key) + if err != nil { + return nil, err + } + if err = updater.Validate(readData); err != nil { + return nil, errors.Wrap(err, "validating update") + } + updatedData, err := updater.Update(readData) + if err != nil { + return nil, err + } + if err = tx.Write(ctx, namespace, key, updatedData); err != nil { + return nil, errors.Wrap(err, "writing to db") + } + return updatedData, nil +} From b3ad6d191cbfbd4cdc07e7a79a739b7d8b688962 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Thu, 27 Jul 2023 12:05:06 -0400 Subject: [PATCH 02/20] Documentation improvements. (#604) --- doc/swagger.yaml | 11 ++++++----- pkg/server/router/manifest.go | 10 ++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/swagger.yaml b/doc/swagger.yaml index ffbbce45f..2bec50baf 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -1935,9 +1935,9 @@ definitions: properties: applicationJwt: description: |- - Contains the following properties: - Application manifestsdk.CredentialApplication `json:"credential_application" validate:"required"` - Credentials []interface{} `json:"vcs" validate:"required"` + A JWT signed by the applicant. The payload MUST contain the following properties: + - `credential_application`: an object of type manifest.CredentialApplication (specified in https://identity.foundation/credential-manifest/#credential-application). + - `vcs`: an array of Verifiable Credentials. type: string required: - applicationJwt @@ -3121,8 +3121,9 @@ paths: put: consumes: - application/json - description: Submit a credential application in response to a credential manifest. - The request body is expected to + description: |- + Submit a credential application in response to a credential manifest. The request body is expected to + be a valid JWT signed by the applicant's DID, containing two top level properties: `credential_application` and `vcs`. parameters: - description: request body in: body diff --git a/pkg/server/router/manifest.go b/pkg/server/router/manifest.go index 8ac682001..bf64b9984 100644 --- a/pkg/server/router/manifest.go +++ b/pkg/server/router/manifest.go @@ -243,9 +243,9 @@ func (mr ManifestRouter) DeleteManifest(c *gin.Context) { } type SubmitApplicationRequest struct { - // Contains the following properties: - // Application manifestsdk.CredentialApplication `json:"credential_application" validate:"required"` - // Credentials []interface{} `json:"vcs" validate:"required"` + // A JWT signed by the applicant. The payload MUST contain the following properties: + // - `credential_application`: an object of type manifest.CredentialApplication (specified in https://identity.foundation/credential-manifest/#credential-application). + // - `vcs`: an array of Verifiable Credentials. ApplicationJWT keyaccess.JWT `json:"applicationJwt" validate:"required"` } @@ -317,9 +317,7 @@ type SubmitApplicationResponse struct { // // @Summary Submit application // @Description Submit a credential application in response to a credential manifest. The request body is expected to -// -// be a valid JWT signed by the applicant's DID, containing two top level properties: credential_application and vcs. -// +// @Description be a valid JWT signed by the applicant's DID, containing two top level properties: `credential_application` and `vcs`. // @Tags ApplicationAPI // @Accept json // @Produce json From b57a6568f59f958e8d3ff9d54b0641c052571bb7 Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Fri, 28 Jul 2023 10:27:29 -0700 Subject: [PATCH 03/20] Update READMEs, docs, add how-tos section, first how-to for DIDs (#607) * how tos * table of docs * consolidate and clean up docs * remove templating * did tutorial --- README.md | 169 +++++++----------- cmd/ssiservice/main.go | 3 +- config/README.md | 19 -- doc/README.md | 58 +++++- doc/config/kms.md | 27 +++ doc/{STORAGE.md => config/storage.md} | 2 +- doc/config/toml.md | 20 +++ doc/howto/credential.md | 0 doc/howto/did.md | 122 +++++++++++++ doc/howto/schema.md | 0 doc/howto/status.md | 0 doc/howto/verification.md | 0 doc/service/features.md | 19 ++ doc/{VERSIONING.md => service/versioning.md} | 0 doc/{VISION.md => service/vision.md} | 0 doc/{WEBHOOK.md => service/webhook.md} | 0 {sip => doc/sip}/README.md | 5 +- {sip => doc/sip}/sip_flow.png | Bin {sip => doc/sip}/sips/sip1/README.md | 0 {sip => doc/sip}/sips/sip2/README.md | 0 .../sip}/sips/sip2/assets/application.png | Bin .../sip}/sips/sip2/assets/manifest.png | Bin .../sip}/sips/sip2/assets/response.png | Bin .../sip}/sips/sip2/assets/toplevel.png | Bin {sip => doc/sip}/sips/sip3/README.md | 0 .../sip}/sips/sip3/assets/key-access.png | Bin {sip => doc/sip}/sips/sip4/README.md | 0 {sip => doc/sip}/sips/sip4/assets/diagram.png | Bin .../sip}/sips/sip4/assets/endpoints.png | Bin .../sip}/sips/sip4/assets/manifest.png | Bin {sip => doc/sip}/sips/sip5/README.md | 0 {sip => doc/sip}/sips/sip6/README.md | 0 {sip => doc/sip}/sips/sip7/README.md | 0 {sip => doc/sip}/sips/sip8/README.md | 0 .../sip8/assets/applicationsubmission.png | Bin {sip => doc/sip}/sips/sip9/README.md | 0 {sip => doc/sip}/sips/sip_template.md | 0 doc/swagger.yaml | 4 +- pkg/server/server.go | 47 +---- 39 files changed, 316 insertions(+), 179 deletions(-) delete mode 100644 config/README.md create mode 100644 doc/config/kms.md rename doc/{STORAGE.md => config/storage.md} (97%) create mode 100644 doc/config/toml.md create mode 100644 doc/howto/credential.md create mode 100644 doc/howto/did.md create mode 100644 doc/howto/schema.md create mode 100644 doc/howto/status.md create mode 100644 doc/howto/verification.md create mode 100644 doc/service/features.md rename doc/{VERSIONING.md => service/versioning.md} (100%) rename doc/{VISION.md => service/vision.md} (100%) rename doc/{WEBHOOK.md => service/webhook.md} (100%) rename {sip => doc/sip}/README.md (92%) rename {sip => doc/sip}/sip_flow.png (100%) rename {sip => doc/sip}/sips/sip1/README.md (100%) rename {sip => doc/sip}/sips/sip2/README.md (100%) rename {sip => doc/sip}/sips/sip2/assets/application.png (100%) rename {sip => doc/sip}/sips/sip2/assets/manifest.png (100%) rename {sip => doc/sip}/sips/sip2/assets/response.png (100%) rename {sip => doc/sip}/sips/sip2/assets/toplevel.png (100%) rename {sip => doc/sip}/sips/sip3/README.md (100%) rename {sip => doc/sip}/sips/sip3/assets/key-access.png (100%) rename {sip => doc/sip}/sips/sip4/README.md (100%) rename {sip => doc/sip}/sips/sip4/assets/diagram.png (100%) rename {sip => doc/sip}/sips/sip4/assets/endpoints.png (100%) rename {sip => doc/sip}/sips/sip4/assets/manifest.png (100%) rename {sip => doc/sip}/sips/sip5/README.md (100%) rename {sip => doc/sip}/sips/sip6/README.md (100%) rename {sip => doc/sip}/sips/sip7/README.md (100%) rename {sip => doc/sip}/sips/sip8/README.md (100%) rename {sip => doc/sip}/sips/sip8/assets/applicationsubmission.png (100%) rename {sip => doc/sip}/sips/sip9/README.md (100%) rename {sip => doc/sip}/sips/sip_template.md (100%) diff --git a/README.md b/README.md index 3f6d719d3..0f0233273 100644 --- a/README.md +++ b/README.md @@ -6,85 +6,56 @@ # ssi-service -A web service that exposes the ssi-sdk as an HTTP API. Support operations for Verifiable Credentials, Decentralized Identifiers and things Self Sovereign Identity! - -## Introduction -The Self Sovereign Identity Service (SSIS) facilitates all things relating to [DIDs](https://www.w3.org/TR/did-core/) -and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model) - in a box! The service is a part of a larger -Decentralized Web Platform architecture which you can learn more about in our -[collaboration repo](https://github.com/TBD54566975/collaboration). +The Self Sovereign Identity Service (SSIS) is a RESTful web service that facilitates all things relating +to [DIDs](https://www.w3.org/TR/did-core/), +[Verifiable Credentials](https://www.w3.org/TR/vc-data-model) and their related standards-based interactions. Most of +the functionality in this service +relies upon the SSI primitives exposed in the [SSI-SDK](https://github.com/TBD54566975/ssi-sdk) project. ## Core Functionality -- Create and manage Decentralized Identifiers -- Create and manage Verifiable Credentials -- Credential Suspension -- Interacting with the standards around Verifiable Credentials such as - - Credential Revocations - - Applying for Credentials - - Exchanging Credentials - - Data Schemas (for credentials and other verifiable data) - -## Use Cases (more to come!) -### Business: Issuing Verifiable Credentials
-[Follow Tutorial](https://developer.tbd.website/blog/issue-verifiable-credential-manually) - -Steps to issue an Employment Status Credential: -1. Spin up and host the SSI-Service -2. Add the ability for your employees to click 'apply for a credential' on your internal EMS (should we show a front end button code example) -3. [Create an Issuer DID](https://github.com/TBD54566975/ssi-service/blob/eabbb2a58eec06ce3998d088811c4afc53026afd/integration/common.go#L38) for your business -4. [Create a Schema](https://github.com/TBD54566975/ssi-service/blob/eabbb2a58eec06ce3998d088811c4afc53026afd/integration/common.go#L90) -5. [Create a Credential Manifest](https://github.com/TBD54566975/ssi-service/blob/main/integration/common.go#L180) -6. [Submit a Credential Application](https://github.com/TBD54566975/ssi-service/blob/eabbb2a58eec06ce3998d088811c4afc53026afd/integration/common.go#L199) - -## Configuration -Managed via: -[TOML](https://toml.io/en/) [file](config/dev.toml) - -There are sets of configuration values for the server (e.g. which port to listen on), the services (e.g. which database to use), -and each service. Each service may define specific configuration, such as which DID methods are enabled for the DID -service. - -### Key Management +- Lifecycle management for Decentralized Identifiers + - Multiple local, web, and blockchain methods supported +- Create and manage Verifiable Credentials + - Multiple securing mechanisms + - Data schemas using JSON Schema + - Lifecycle management with credential status (revocation, suspension, etc.) +- More robust DID and Verifiable Credential interactions such as... + - Enabling the application of credentials + - Verifying sets of credentials with custom logic + - Linking a DID to a web domain + - Integration into your existing systems with webhooks + - Trust management +- And much more! -SSI-service can store keys that are used to digitally sign credentials (and other data). All such keys are encrypted at -the application before being stored using a MasterKey (a.k.a. a Key Encryption Key or KEK). The MasterKey can be -generated automatically during boot time, or we can use the MasterKey housed in an external Key -Management System (KMS) like GCP KMS or AWS KMS. +## Documentation -For production deployments, using external KMS is strongly recommended. +### Vision, Features, and Development -To use an external KMS: -1. Create a symmetric encryption key in your KMS. You MUST select the algorithm that uses AES-256 block cipher in Galois/Counter Mode (GCM). At the time of writing, this is the only algorithm supported by AWS and GCP. -2. Set the `master_key_uri` field of the `[services.keystore]` section using the format described in [tink](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-systems) -(we use the tink library under the hood). -3. Set the `kms_credentials_path` field of the `[services.keystore]` section to point to your credentials file, according to [this section](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials). -4. Win! +The vision for the project is laid out in [this document](doc/service/vision.md). -To use a randomly generated encryption key (NOT RECOMMENDED FOR ANY PRODUCTION ENVIRONMENT): -1. Make sure that `master_key_uri` and `kms_credentials_path` of the `[services.keystore]` section are not set. +The project follows a proposal-based improvement format called [SIPs, outlined here](sip/README.md). -Note that at this time, we do not currently support rotating the master key. +Please [join Discord](https://discord.com/invite/tbd), or open an [issue](https://github.com/TBD54566975/ssi-service/issues) if you are interested in helping shape the future of the +project. -### Steps for SSI-Service to consume its configuration: -1. On startup: SSI-Service loads default values into the SSIServiceConfig -2. Checks for a TOML config file: -- If exists...load toml file -- If does not exist...it uses a default config defined in the code inline -3. Finally, it loads the config/.env file and adds the env variables defined in this file to the final SSIServiceConfig +### API Documentation -### Authentication and Authorization +API documentation is generated using [Swagger](https://swagger.io/). The most recent +docs [can be found here](doc/swagger.yaml). -The ssi server uses the Gin framework from Golang, which allows various kinds of middleware. Look in `pkg/middleware/Authentication.go` and `pkg/middleware/Authorization.go` for details on how you can wire up authentication and authorization for your use case. +When running the service you can find API documentation at: `http://localhost:8080/swagger/index.html` -## Pre-built images to use +**Note:** Your port may differ; swagger docs are hosted on the same endpoint as the ssi service itself. -There are pre-build images built by github actions on each merge to the main branch, which you can access here: -https://github.com/orgs/TBD54566975/packages?repo_name=ssi-service +### How To's +We have a set of tutorials and how-to documents, instructing you on how to create a DID, issue your first credential, +and more! The docs can be found [in our docs here](doc/README.md). ## Build & Test +### Local Development This project uses [mage](https://magefile.org/), please view [CONTRIBUTING](https://github.com/TBD54566975/ssi-service/blob/main/CONTRIBUTING.md) for more information. @@ -101,28 +72,61 @@ A utility is provided to run _clean, build, lint, and test_ in sequence with: mage cblt ``` +### Continuous Integration + +CI is managed via [GitHub Actions](https://github.com/TBD54566975/ssi-service/actions). Actions are triggered to run for +each Pull Request, and on merge to `main`. +You can run CI locally using a tool like [act](https://github.com/nektos/act). + ## Deployment The service is packaged as a [Docker container](https://www.docker.com/), runnable in a wide variety of environments. +There are pre-build images built by GitHub Actions on each merge to the `main` branch, +which [you can access here](https://github.com/orgs/TBD54566975/packages?repo_name=ssi-service). + [Docker Compose](https://docs.docker.com/compose/) is used for simplification and orchestration. To run the service, you can use the following command, which will start the service on port `8080`: + ```shell mage run ``` Or, you can run docker-compose yourself, building from source: + ```shell cd build && docker-compose up --build ``` To use the pre-published images: + ```shell cd build && docker-compose up -d ``` -## Health and Readiness Checks +## Using the Service + +### Configuration + +Managed via: +[TOML](https://toml.io/en/) [file](config/dev.toml). Configuration documentation and sample config +files [can be found here](config/README.md). + +There are sets of configuration values for the server (e.g. which port to listen on), the services (e.g. which database +to use), +and each service. Each service may define specific configuration, such as which DID methods are enabled for the DID +service. + +More information on configuration can be found in the [configuration section of our docs](doc/README.md). + +### Authentication and Authorization + +The SSI server uses the [Gin framework](https://github.com/gin-gonic/gin), which allows various kinds of middleware. +Look in `pkg/middleware/Authentication.go` and `pkg/middleware/Authorization.go` for details on how you can wire up +authentication and authorization for your use case. + +### Health and Readiness Checks Note: port 3000 is used by default, specified in `config.toml`, for the SSI Service process. If you're running via `mage run` or docker compose, the port to access will be `8080`. @@ -157,45 +161,6 @@ Run to check if all services are up and ready (credential, did, and schema): } ``` -## Continuous Integration - -CI is managed via [GitHub Actions](https://github.com/TBD54566975/ssi-service/actions). Actions are triggered to run -for each Pull Request, and on merge to `main`. You can run CI locally using a tool -like [act](https://github.com/nektos/act). - -## API Documentation -You can find all HTTP endpoints by checking out the swagger docs at: `http://localhost:8080/swagger/index.html` - -Note: Your port may differ; swagger docs are hosted on the same endpoint as the ssi service itself. - -## What's Supported? -- [x] [DID Management](https://www.w3.org/TR/did-core/) - - [x] [did:key](https://w3c-ccg.github.io/did-method-key/) - - [x] [did:web](https://w3c-ccg.github.io/did-method-web/) - - [x] [did:ion](https://identity.foundation/ion/) _Note: updates not yet supported_ - - [x] [did:pkh](https://w3c-ccg.github.io/did-method-pkh/) _Resolution only_ - - [x] [did:peer](https://identity.foundation/peer-did-method-spec/) _Resolution only_ -- [x] [Verifiable Credential Schema](https://w3c-ccg.github.io/vc-json-schemas/v2/index.html) Management -- [x] [Verifiable Credential](https://www.w3.org/TR/vc-data-model) Issuance & Verification - - [x] Signing and verification with [JWTs](https://w3c.github.io/vc-jwt/) - - [ ] Signing and verification with [Data Integrity Proofs](https://w3c.github.io/vc-data-integrity/) -- [x] Applying for Verifiable Credentials using [Credential Manifest](https://identity.foundation/credential-manifest/) -- [x] Requesting, Receiving, and the Validation of Verifiable Claims - using [Presentation Exchange](https://identity.foundation/presentation-exchange/) -- [x] Status of Verifiable Credentials using the [Status List 2021](https://w3c-ccg.github.io/vc-status-list-2021/) -- [x] [DID Well Known Configuration](https://identity.foundation/.well-known/resources/did-configuration/) documents -- [ ] Creating and managing Trust documents using [Trust Establishment](https://identity.foundation/trust-establishment/) - -## Vision, Features, and Development - -The vision for the project is laid out in [this document](doc/VISION.md). - -The project follows a proposal-based improvement format called [SIPs, outlined here.](sip/README.md). - -Please [join Discord](https://discord.com/invite/tbd), -or open an [issue](https://github.com/TBD54566975/ssi-service/issues) if you are interested in helping shape the future of the project. - - ## Project Resources | Resource | Description | diff --git a/cmd/ssiservice/main.go b/cmd/ssiservice/main.go index 560e07086..268635d6e 100644 --- a/cmd/ssiservice/main.go +++ b/cmd/ssiservice/main.go @@ -30,13 +30,12 @@ import ( // main godoc // // @title SSI Service API -// @description {{.Desc}} +// @description The Self Sovereign Identity Service: Managing DIDs, Verifiable Credentials, and more! // @contact.name TBD // @contact.url https://github.com/TBD54566975/ssi-service/issues // @contact.email tbd-developer@squareup.com // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @version {{.SVN}} func main() { logrus.Info("Starting up...") diff --git a/config/README.md b/config/README.md deleted file mode 100644 index d43d82bb7..000000000 --- a/config/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Configuration - -Configuration is managed using -a [TOML](https://toml.io/en/) [file](https://github.com/TBD54566975/ssi-service/blob/main/config/config.toml). There are -sets of configuration values for the server (e.g. which port to listen on), the services (e.g. which database to use), -and each service. - -Each service may define specific configuration, such as which DID methods are enabled for the DID service. - -# Usage - -The service, upon boot, looks for a file called `config.toml` to find its configuration. - -There are a number of configuration files in this directory provided as defaults. -Specifically, `config.toml` -is intended to be used when the service is run as a local go process. There is another -file, `compose.toml`, -which is intended to be used when the service is run via docker compose. To make this switch, it's recommended that one -renames the file to `config.toml` and then maintains the original `compose.toml` file as `local.toml` or similar. diff --git a/doc/README.md b/doc/README.md index e99d40779..8c4c21403 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,11 +1,57 @@ +# Docs + +Home for all content related to the SSI Service. ## Service Documentation -| Resource | Description | -|--------------------------------------------------------------------------------------|----------------------------------------------------| -| [Vision](https://github.com/TBD54566975/ssi-service/blob/main/doc/VISION.md) | Describes the vision for the SSI Service | -| [Versioning](https://github.com/TBD54566975/ssi-service/blob/main/doc/VERSIONING.md) | Describes versioning practices for the SSI Service | -| [Webhooks](https://github.com/TBD54566975/ssi-service/blob/main/doc/WEBHOOK.md) | Describes how to use webhooks in the SSI Service | -| [Storage](https://github.com/TBD54566975/ssi-service/blob/main/doc/STORAGE.md) | Describes alternatives for storage in SSI Service | +Service documentation is focused on explaining the "whys" and "hows" of the SSI Service. It is intended to be a +resource for developers and users of the SSI Service. + +| Resource | Description | +|----------------------------------------------------------------------------------------------|---------------------------------------------------| +| [Vision](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/vision.md) | Describes the vision for the service | +| [Versioning](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/versioning.md) | Describes versioning practices for the service | +| [Webhooks](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/webhook.md) | Describes how to use webhooks in the service | +| [Features](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/features.md) | Features currently supported by the service | + +## Service Improvement Proposals (SIPs) + +All feature proposal documents for the SSI Service follow a common format and are known as SSI Improvement Proposals or +SIPs. SIPs [have their own documentation which can be found here](https://github.com/TBD54566975/ssi-service/blob/main/doc/sip/README.md) + +## Configuration + +There are a few ways to configure the service. There are a few choices you can make, including which database to use, +which DID methods to enable, and which port to listen on. Read the docs below for more details! + +| Resource | Description | +|------------------------------------------------------------------------------------------------------------|----------------------------------------| +| [TOML Config Files](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/toml.md) | Describes how to use TOML config files | +| [Using a Cloud Key Management Service](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/kms.md) | Describes how to configure a KMS | +| [Storage](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/storage.md) | Describes alternatives for storage by the service | + +## API Documentation + +API documentation is generated using [Swagger](https://swagger.io/). The most recent API docs file [can be found here](doc/swagger.yaml), which can be pasted into the [Swagger Editor](https://editor.swagger.io/) for interaction. + +When running the service you can find API documentation at: `http://localhost:8080/swagger/index.html` + +**Note:** Your port may differ; swagger docs are hosted on the same endpoint as the ssi service itself. + +## How To's + +How to documentation is focused on explaining usage of the SSI Service. It is intended to be a resource for users of +the SSI Service to get up to speed with its functionality. + +| Resource | Description | +|----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| [Creating a DID](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/credential.md) | Get started with DID functionality | +| [Creating a Schema](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/schema.md) | Get started with schema functionality | +| [Issuing a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/credential.md) | Get started with credential issuance functionality | +| [Verify a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/verification.md) | Get started with credential verification functionality | +| [Revoke/Suspend a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/status.md) | Get started with credential status functionality | +| [[TODO] Requesting and Verifying Credentials with Presentation Exchange](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Presentation Exchange functionality | +| [[TODO] Accepting Applications for and Issuing Credentials using Credential Manifest](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Credential Manifest functionality | +| [[TODO] Creating a Well Known File for your DID](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with DID Well Known functionality | diff --git a/doc/config/kms.md b/doc/config/kms.md new file mode 100644 index 000000000..13149e952 --- /dev/null +++ b/doc/config/kms.md @@ -0,0 +1,27 @@ + +### Key Management + +The service can store keys that are used to digitally sign credentials (and other data). All such keys are encrypted at +the application before being stored using a MasterKey (a.k.a. a Key Encryption Key or KEK). The MasterKey can be +generated automatically during boot time, or we can use the MasterKey housed in an external Key +Management System (KMS) like GCP KMS or AWS KMS. + +For production deployments, using external KMS is strongly recommended. + +To use an external KMS: + +1. Create a symmetric encryption key in your KMS. You MUST select the algorithm that uses AES-256 block cipher in + Galois/Counter Mode (GCM). At the time of writing, this is the only algorithm supported by AWS and GCP. +2. Set the `master_key_uri` field of the `[services.keystore]` section using the format described + in [tink](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-systems) + (we use the tink library under the hood). +3. Set the `kms_credentials_path` field of the `[services.keystore]` section to point to your credentials file, + according + to [this section](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials). +4. Win! + +To use a randomly generated encryption key (NOT RECOMMENDED FOR ANY PRODUCTION ENVIRONMENT): + +1. Make sure that `master_key_uri` and `kms_credentials_path` of the `[services.keystore]` section are not set. + +Note that at this time, we do not currently support rotating the master key. \ No newline at end of file diff --git a/doc/STORAGE.md b/doc/config/storage.md similarity index 97% rename from doc/STORAGE.md rename to doc/config/storage.md index 34420f9c4..7ba5c2cab 100644 --- a/doc/STORAGE.md +++ b/doc/config/storage.md @@ -68,7 +68,7 @@ For a working example, see this [dev.toml file](https://github.com/TBD54566975/s ## Implementing a New Storage Provider -You need to implement the [ServiceStorage interface](../pkg/storage/storage.go), similar to how [Redis](../pkg/storage/redis.go) +You need to implement the [ServiceStorage interface](../../pkg/storage/storage.go), similar to how [Redis](../../pkg/storage/redis.go) is implemented. For an example, see [this PR](https://github.com/TBD54566975/ssi-service/pull/590/files#diff-606358579107e7ad1221525001aed8c776a141d4cc5aab9ef7a3ddbcec10d9f9) which introduces the SQL based implementation. diff --git a/doc/config/toml.md b/doc/config/toml.md new file mode 100644 index 000000000..0abb04b61 --- /dev/null +++ b/doc/config/toml.md @@ -0,0 +1,20 @@ +# TOML Config File +Config is managed using a [TOML](https://toml.io/en/) [file](https://github.com/TBD54566975/ssi-service/blob/main/config/config.toml). There are sets of configuration values for the server +(e.g. which port to listen on), the services (e.g. which database to use), and each service. + +Each service may define specific configuration, such as which DID methods are enabled for the DID service. + +## Usage + +How it works: +1. On startup: SSI-Service loads default values into the `SSIServiceConfig` +2. Checks for a TOML config file: + - If exists...load toml file + - If does not exist...it uses a default config defined in the code inline +3. Loads the `config/.env` file and adds the env variables defined in this file to the final `SSIServiceConfig` + +There are a number of configuration files in this directory provided as defaults. +Specifically, `config.toml`is intended to be used when the service is run as a local go process. There is another +file, `compose.toml`, which is intended to be used when the service is run via docker compose. To make this switch, +it's recommended that one renames the file to `config.toml` and then maintains the original `compose.toml` file as +`local.toml` or similar. diff --git a/doc/howto/credential.md b/doc/howto/credential.md new file mode 100644 index 000000000..e69de29bb diff --git a/doc/howto/did.md b/doc/howto/did.md new file mode 100644 index 000000000..414134c25 --- /dev/null +++ b/doc/howto/did.md @@ -0,0 +1,122 @@ +# How To: Create a DID + +## Background + +A [DID (Decentralized Identifier)](https://www.w3.org/TR/did-core/) is a unique identifier that can be used to identify a person, organization, or thing. DIDs are associated with documents called DID Documents. These documents contain +cryptographic key material, service endpoints, and other useful information. + +DIDs are a core component of SSI (Self-Sovereign Identity) systems. DIDs are specified according to "methods" which +define how the DID is created and how it can be used. A list of existing [DID methods can be found here](https://www.w3.org/TR/did-spec-registries/#did-methods). + +Importantly, DIDs in the SSI Service are _fully custodial_. This means the private keys associated with DID Documents are managed by the service and never leave its boundaries. For some DID methods, such as `did:web` it's possible to add multiple keys to the DID document, and it's possible for these additional keys to be added outside the service. This is a more advanced concept that the service may support in the future. + +## What DID Methods are There? + +The SSI Service supports a number of DID methods, including... + +* [DID Key](https://w3c-ccg.github.io/did-method-key/) a self-resolving method great for testing +* [DID Web](https://w3c-ccg.github.io/did-method-web/) a method designed to be hosted on your own domain +* [DID ION](https://identity.foundation/sidetree/spec/#value-locking) a Layer 2 Bitcoin method based on the Sidetree protocol. + +At runtime, the you can enable and disable which methods the service supports. Learn more about this by reading our [documentation on configuration](../README.md). + +Once the service is running you can see which DID methods are enabled by sending a `GET` request to `/v1/dids`. + +Upon a successful request you should see a response such as: + +```json +{ + "method": [ + "key", + "web" + ] +} +``` + +## Creating A DID + +You can create a DID by sending a `PUT` request to the `/v1/dids/{method}` endpoint. The request body needs two pieces of information: a method and a key type. The method must be supported by the service, and the key type must be supported by the method. You can find out more specifics about what each method supports [by looking at the SDK](https://github.com/TBD54566975/ssi-sdk/tree/main/did). Certain methods may support additional properties in an optional `options` fields. + +For now let's keep things simple and create a new `did:key` with the key type [`Ed25519`](https://ed25519.cr.yp.to/), a widely respected key type using [ellicptic curve cryptography](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography). + +**Create DID Key Request** + +`PUT` to `/v1/dids/key` + +```json +{ + "keyType": "Ed25519" +} +``` + +If successful, you should see a response such as... + +```json +{ + "did": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g", + "verificationMethod": [ + { + "id": "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g#z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "7VpY8ilUvfIgUVf9DOO58BD1kQKPN_NDrNvr8qyK-2M", + "alg": "EdDSA", + "kid": "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g" + } + }, + { + "id": "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g#z6LSgpucz3mqZYRRzBpx8zGtWQAKSkyLqfQMEpxr2PVaRX8V", + "type": "JsonWebKey2020", + "controller": "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g", + "publicKeyJwk": { + "kty": "OKP", + "crv": "X25519", + "x": "TIcWJZVD0_TUHYzm9eyc1s5bD3EmrjTlVQfaHDfrazo", + "alg": "X25519", + "kid": "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g" + } + } + ], + "authentication": [ + "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g#z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g" + ], + "assertionMethod": [ + "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g#z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g" + ], + "keyAgreement": [ + "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g#z6LSgpucz3mqZYRRzBpx8zGtWQAKSkyLqfQMEpxr2PVaRX8V" + ], + "capabilityInvocation": [ + "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g#z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g" + ], + "capabilityDelegation": [ + "did:key:z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g#z6MkvRnaDoRv4qn6scLAE2RVAv1Yj42S9HZkkJTYB8Lm2U5g" + ] + } +} +``` + +which is a fully complaint `did:key` document [according to its specification](https://w3c-ccg.github.io/did-method-key/). + +Now that you have a DID you can begin to use it with other pieces of the service, such as by [issuing a credential](credential.md). + +## Getting DIDs + +Once you've created muliple DIDs, you can view all DIDs under a given method by making a `GET` request to the method's endpoint, such as `/v1/dids/key`. + +You can get a specific DID's document by making a `GET` request to the method's endpoint, such as `/v1/dids/key/{did}`. + +## DIDs Outside the Service + +The [universal resolver](https://github.com/decentralized-identity/universal-resolver) is a project at the [Decentralized Identity Foundation](https://identity.foundation/) aiming to enable the resolution of _any_ DID Document. The service, when run with [Docker Compose, runs a select number of these drivers (and more can be configured). It's possible to leverage the resolution of DIDs not supported by the service by making `GET` requests to `/v1/dids/resolver/{did}`. + + + diff --git a/doc/howto/schema.md b/doc/howto/schema.md new file mode 100644 index 000000000..e69de29bb diff --git a/doc/howto/status.md b/doc/howto/status.md new file mode 100644 index 000000000..e69de29bb diff --git a/doc/howto/verification.md b/doc/howto/verification.md new file mode 100644 index 000000000..e69de29bb diff --git a/doc/service/features.md b/doc/service/features.md new file mode 100644 index 000000000..84849e712 --- /dev/null +++ b/doc/service/features.md @@ -0,0 +1,19 @@ +# Feature List + +- [x] [DID Management](https://www.w3.org/TR/did-core/) + - [x] [did:key](https://w3c-ccg.github.io/did-method-key/) + - [x] [did:web](https://w3c-ccg.github.io/did-method-web/) + - [x] [did:ion](https://identity.foundation/ion/) _Note: updates not yet supported_ + - [x] [did:pkh](https://w3c-ccg.github.io/did-method-pkh/) _Resolution only_ + - [x] [did:peer](https://identity.foundation/peer-did-method-spec/) _Resolution only_ +- [x] [Verifiable Credential Schema](https://w3c-ccg.github.io/vc-json-schemas/v2/index.html) Management +- [x] [Verifiable Credential](https://www.w3.org/TR/vc-data-model) Issuance & Verification + - [x] Signing and verification with [JWTs](https://w3c.github.io/vc-jwt/) + - [ ] Signing and verification with [Data Integrity Proofs](https://w3c.github.io/vc-data-integrity/) +- [x] Applying for Verifiable Credentials using [Credential Manifest](https://identity.foundation/credential-manifest/) +- [x] Requesting, Receiving, and the Validation of Verifiable Claims + using [Presentation Exchange](https://identity.foundation/presentation-exchange/) +- [x] Status of Verifiable Credentials using the [Status List 2021](https://w3c-ccg.github.io/vc-status-list-2021/) +- [x] [DID Well Known Configuration](https://identity.foundation/.well-known/resources/did-configuration/) documents +- [ ] Creating and managing Trust documents + using [Trust Establishment](https://identity.foundation/trust-establishment/) \ No newline at end of file diff --git a/doc/VERSIONING.md b/doc/service/versioning.md similarity index 100% rename from doc/VERSIONING.md rename to doc/service/versioning.md diff --git a/doc/VISION.md b/doc/service/vision.md similarity index 100% rename from doc/VISION.md rename to doc/service/vision.md diff --git a/doc/WEBHOOK.md b/doc/service/webhook.md similarity index 100% rename from doc/WEBHOOK.md rename to doc/service/webhook.md diff --git a/sip/README.md b/doc/sip/README.md similarity index 92% rename from sip/README.md rename to doc/sip/README.md index 6f9bbb22e..672324d66 100644 --- a/sip/README.md +++ b/doc/sip/README.md @@ -4,7 +4,7 @@ All feature proposal documents for the SSI Service follow a common format and are known as SSI Improvement Proposals or SIPs. Features must list a DRI (Directly Responsible Individual) and follow the [template here](sips/sip_template.md). -Discussion is encouraged on GitHub issues and pull requests and optionally on our forums. +Discussion is encouraged on GitHub issues and pull requests. ## SIP Status @@ -27,8 +27,9 @@ the [template](sips/sip_template.md), add it to the table below, and open up a p # SIPs + | SIP | Description | DRI | Status | Date of Status | -|------------------------------|---------------------------------------------------|--------------------------------------------------|----------|--------------------| +| ---------------------------- | ------------------------------------------------- | ------------------------------------------------ | -------- | ------------------ | | [SIP-1](sips/sip1/README.md) | SIP Purpose and Guidelines | [Gabe Cohen](https://github.com/decentralgabe) | Accepted | August 24, 2022 | | [SIP-2](sips/sip2/README.md) | Credential Issuance Flow | [Neal Roessler](https://github.com/nitro-neal) | Accepted | September 13, 2022 | | [SIP-3](sips/sip3/README.md) | Key Access: Signing & Verification | [Gabe Cohen](https://github.com/decentralgabe) | Accepted | September 20, 2022 | diff --git a/sip/sip_flow.png b/doc/sip/sip_flow.png similarity index 100% rename from sip/sip_flow.png rename to doc/sip/sip_flow.png diff --git a/sip/sips/sip1/README.md b/doc/sip/sips/sip1/README.md similarity index 100% rename from sip/sips/sip1/README.md rename to doc/sip/sips/sip1/README.md diff --git a/sip/sips/sip2/README.md b/doc/sip/sips/sip2/README.md similarity index 100% rename from sip/sips/sip2/README.md rename to doc/sip/sips/sip2/README.md diff --git a/sip/sips/sip2/assets/application.png b/doc/sip/sips/sip2/assets/application.png similarity index 100% rename from sip/sips/sip2/assets/application.png rename to doc/sip/sips/sip2/assets/application.png diff --git a/sip/sips/sip2/assets/manifest.png b/doc/sip/sips/sip2/assets/manifest.png similarity index 100% rename from sip/sips/sip2/assets/manifest.png rename to doc/sip/sips/sip2/assets/manifest.png diff --git a/sip/sips/sip2/assets/response.png b/doc/sip/sips/sip2/assets/response.png similarity index 100% rename from sip/sips/sip2/assets/response.png rename to doc/sip/sips/sip2/assets/response.png diff --git a/sip/sips/sip2/assets/toplevel.png b/doc/sip/sips/sip2/assets/toplevel.png similarity index 100% rename from sip/sips/sip2/assets/toplevel.png rename to doc/sip/sips/sip2/assets/toplevel.png diff --git a/sip/sips/sip3/README.md b/doc/sip/sips/sip3/README.md similarity index 100% rename from sip/sips/sip3/README.md rename to doc/sip/sips/sip3/README.md diff --git a/sip/sips/sip3/assets/key-access.png b/doc/sip/sips/sip3/assets/key-access.png similarity index 100% rename from sip/sips/sip3/assets/key-access.png rename to doc/sip/sips/sip3/assets/key-access.png diff --git a/sip/sips/sip4/README.md b/doc/sip/sips/sip4/README.md similarity index 100% rename from sip/sips/sip4/README.md rename to doc/sip/sips/sip4/README.md diff --git a/sip/sips/sip4/assets/diagram.png b/doc/sip/sips/sip4/assets/diagram.png similarity index 100% rename from sip/sips/sip4/assets/diagram.png rename to doc/sip/sips/sip4/assets/diagram.png diff --git a/sip/sips/sip4/assets/endpoints.png b/doc/sip/sips/sip4/assets/endpoints.png similarity index 100% rename from sip/sips/sip4/assets/endpoints.png rename to doc/sip/sips/sip4/assets/endpoints.png diff --git a/sip/sips/sip4/assets/manifest.png b/doc/sip/sips/sip4/assets/manifest.png similarity index 100% rename from sip/sips/sip4/assets/manifest.png rename to doc/sip/sips/sip4/assets/manifest.png diff --git a/sip/sips/sip5/README.md b/doc/sip/sips/sip5/README.md similarity index 100% rename from sip/sips/sip5/README.md rename to doc/sip/sips/sip5/README.md diff --git a/sip/sips/sip6/README.md b/doc/sip/sips/sip6/README.md similarity index 100% rename from sip/sips/sip6/README.md rename to doc/sip/sips/sip6/README.md diff --git a/sip/sips/sip7/README.md b/doc/sip/sips/sip7/README.md similarity index 100% rename from sip/sips/sip7/README.md rename to doc/sip/sips/sip7/README.md diff --git a/sip/sips/sip8/README.md b/doc/sip/sips/sip8/README.md similarity index 100% rename from sip/sips/sip8/README.md rename to doc/sip/sips/sip8/README.md diff --git a/sip/sips/sip8/assets/applicationsubmission.png b/doc/sip/sips/sip8/assets/applicationsubmission.png similarity index 100% rename from sip/sips/sip8/assets/applicationsubmission.png rename to doc/sip/sips/sip8/assets/applicationsubmission.png diff --git a/sip/sips/sip9/README.md b/doc/sip/sips/sip9/README.md similarity index 100% rename from sip/sips/sip9/README.md rename to doc/sip/sips/sip9/README.md diff --git a/sip/sips/sip_template.md b/doc/sip/sips/sip_template.md similarity index 100% rename from sip/sips/sip_template.md rename to doc/sip/sips/sip_template.md diff --git a/doc/swagger.yaml b/doc/swagger.yaml index 2bec50baf..9b2d81608 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -2198,12 +2198,12 @@ info: email: tbd-developer@squareup.com name: TBD url: https://github.com/TBD54566975/ssi-service/issues - description: '{{.Desc}}' + description: 'The Self Sovereign Identity Service: Managing DIDs, Verifiable Credentials, + and more!' license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html title: SSI Service API - version: '{{.SVN}}' paths: /health: get: diff --git a/pkg/server/server.go b/pkg/server/server.go index 2187b36af..adc0cc2d4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -3,14 +3,10 @@ package server import ( - "bytes" - "context" "os" - "text/template" sdkutil "github.com/TBD54566975/ssi-sdk/util" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" swaggerfiles "github.com/swaggo/files" ginswagger "github.com/swaggo/gin-swagger" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" @@ -72,23 +68,8 @@ func NewSSIServer(shutdown chan os.Signal, cfg config.SSIServiceConfig) (*SSISer // service-level routers engine.GET(HealthPrefix, router.Health) engine.GET(ReadinessPrefix, router.Readiness(ssi.GetServices())) - - tmpFile, err := writeSwaggerFile(cfg) - if err != nil { - logrus.WithError(err).Warnf("unable to write swagger file, skipping handler") - } else { - httpServer.RegisterPreShutdownHook(func(_ context.Context) error { - logrus.Infof("removing temp file %q", tmpFile.Name()) - err := os.Remove(tmpFile.Name()) - if err != nil { - logrus.WithError(err).Warnf("unable to delete %q during shutdown", tmpFile.Name()) - } - return nil - }) - - engine.StaticFile("swagger.yaml", tmpFile.Name()) - engine.GET(SwaggerPrefix, ginswagger.WrapHandler(swaggerfiles.Handler, ginswagger.URL("/swagger.yaml"))) - } + engine.StaticFile("swagger.yaml", "./doc/swagger.yaml") + engine.GET(SwaggerPrefix, ginswagger.WrapHandler(swaggerfiles.Handler, ginswagger.URL("/swagger.yaml"))) // register all v1 routers v1 := engine.Group(V1Prefix) @@ -130,30 +111,6 @@ func NewSSIServer(shutdown chan os.Signal, cfg config.SSIServiceConfig) (*SSISer }, nil } -func writeSwaggerFile(cfg config.SSIServiceConfig) (*os.File, error) { - t, err := template.ParseFiles("./doc/swagger.yaml") - if err != nil { - return nil, err - } - - var b bytes.Buffer - if err = t.Execute(&b, cfg); err != nil { - return nil, err - } - tmpFile, err := os.CreateTemp("", "swagger.yaml") - if err != nil { - return nil, err - } - - if _, err = tmpFile.Write(b.Bytes()); err != nil { - return nil, err - } - if err := tmpFile.Close(); err != nil { - return nil, err - } - return tmpFile, nil -} - // setUpEngine creates the gin engine and sets up the middleware based on config func setUpEngine(cfg config.ServerConfig, shutdown chan os.Signal) *gin.Engine { gin.ForceConsoleColor() From dc805c4ec497b92cebba6827d86f828481d2bc8e Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Fri, 28 Jul 2023 14:16:26 -0700 Subject: [PATCH 04/20] Tutorials for schemas and credentials (#610) --- doc/howto/credential.md | 123 ++++++++++++++++++++++++++++++++++ doc/howto/did.md | 13 ++-- doc/howto/schema.md | 123 ++++++++++++++++++++++++++++++++++ doc/swagger.yaml | 2 +- pkg/server/router/did.go | 3 +- pkg/service/schema/service.go | 2 - 6 files changed, 253 insertions(+), 13 deletions(-) diff --git a/doc/howto/credential.md b/doc/howto/credential.md index e69de29bb..53564c6f2 100644 --- a/doc/howto/credential.md +++ b/doc/howto/credential.md @@ -0,0 +1,123 @@ +# How To: Create a Credential + +## Background + +A [Verifiable Credential (VC)](https://www.w3.org/TR/vc-data-model/) is a standard format to package a set of claims that an _issuer_ makes about a _subject_. The Verifiable Credentials Data Model, a W3C standard, introduces a number of concepts, most notable among them, the [three party model](https://www.w3.org/TR/vc-data-model/#ecosystem-overview) of **issuers**, **holders**, and **verifiers**. The model is a novel way of empowering entities to have tamper-evident representations of their data which acts as a mechanism to present the data to any third party (a verifier) without necessitating contact between the verifier and issuer. With the three party model entities are given more [control, transparency, privacy, and utility](https://www.lifewithalacrity.com/2016/04/the-path-to-self-soverereign-identity.html) for data that is rightfully theirs. + +VCs are defined by a data model, which does not provide guidance on transmitting or sharing credentials between parties (protocols). The data model also does not provide guidance on _securing_ the credential (which puts the verifiable in verifiable credential). There are two prominent options here: [Data Integrity](https://www.w3.org/TR/vc-data-integrity/) and [JOSE/COSE](https://www.w3.org/TR/vc-jose-cose/) both of which we have demonstrated support for. The data model has a number of required (core) properties, and multiple means of extension to meet many use cases and functional needs. + +## VCs in the SSI Service + +VCs are a core component of SSI (Self Sovereign Identity) systems and work hand-in-hand with [DIDs](did.md). In our system, DIDs are used to represent the _issuer_ as well as the _subject_ of a credential. To define which content is in a VC we make use of [JSON schemas](schema.md). + +The SSI Service is transport-agnostic and does not mandate the usage of a single mechanism to deliver credentials to an intended holder. We have begun integration with both [Web5](https://github.com/TBD54566975/dwn-sdk-js#readme) and [OpenID Connect](https://openid.net/sg/openid4vc/) transportation mechanisms but leave the door open to any number of possibile options. + +At present, the service supports issuing credentials using the [v1.1 data model as a JWT](https://www.w3.org/TR/vc-data-model/#json-web-token). There is support for verifying credentials that make use of select [Data Integrity cryptographic suites](https://w3c.github.io/vc-data-integrity/) though use is discouraged due to complexity and potential security risks. Support for [v2.0](https://w3c.github.io/vc-data-model/) of the data model is planned and coming soon! + +Out of the box we have support for exposing two [credential statuses](status.md) using the [Verifiable Credentials Status List](https://w3c.github.io/vc-status-list-2021/) specification: suspension and revocation. + +## Creating a Verifiable Credential + +Creating a credential using the SSI Service currently requires four pieces of data: an `issuer` DID, a `verificationMethodId` which must be contained within the issuer's DID Document, a `subject` DID (the DID who the claims are about), and `data` which is an arbitrary JSON object for the claims that you wish to be in the credential (in the `credentialSubject` property). + +There are additional optional properties that let you specify `evidence`, an `expiry`, and whether you want to make the credential `revocable` or `suspendable`. Let's keep things simple and issue our first credential which will attest to a person's first and last name. + +### 1. Create an issuer DID + +We need two pieces of issuer information to create the credential: their DID and a verification method identifier. To get both, let's create a `did:key` to act as the issuer, with the following `PUT` command to `/v1/dids/key`. + +```bash +curl -X PUT localhost:3000/v1/dids/key -d '{"keyType": "Ed25519"}' +``` + +We get back a response with the `id` as `did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD`. Since we're using DID key we know that there is only a single key and its' `verificationMethodId` is the DID suffix: `did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD#z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD`. This value could be retrieved by resolving the DID or by making a `GET` request to `/v1/dids/key/did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD` as well. + +### 2. Create a person schema + +Because we want to include information about the subject in the credential, let's first create a schema to define the shape of the credential's data with required `firstName` and `lastName` values. While this step is optional, it's a good practice to have a schema that describes the shape of the data. + +Once we have our schema, we'll submit it to the service with a `PUT` request to `v1/schemas` as follows: + +```bash +curl -X PUT localhost:3000/v1/schemas -d '{ + "name": "Person Credential", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + } + }, + "required": ["firstName", "lastName"] + } + } + } +}' +``` + +After submission we get back an identifier to refer to the schema as `aed6f4f0-5ed7-4d7a-a3df-56430e1b2a88`. + +### 3. Create a credential + +Separately, we've figured out that the subject we're creating the credential for has the DID `did:key:z6MkmNnvnfzW3nLiePweN3niGLnvp2BjKx3NM186vJ2yRg2z`. Now we have all the set up done we're ready to create our credential. + +Construct a `PUT` request to `/v1/credentials` as follows: + +```bash +curl -X PUT localhost:3000/v1/credentials -d '{ + "issuer": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "verificationMethodId": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD#z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "subject": "did:key:z6MkmNnvnfzW3nLiePweN3niGLnvp2BjKx3NM186vJ2yRg2z", + "schemaId": "aed6f4f0-5ed7-4d7a-a3df-56430e1b2a88", + "data": { + "firstName": "Satoshi", + "lastName": "Nakamoto" + } +}' +``` + +Upon success we'll see a response such as: + +```json +{ + "id": "46bc3d25-6aaf-4f50-99ed-61c4b35f6411", + "fullyQualifiedVerificationMethodId": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD#z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "credential": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "id": "http://localhost:3000/v1/credentials/46bc3d25-6aaf-4f50-99ed-61c4b35f6411", + "type": ["VerifiableCredential"], + "issuer": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "issuanceDate": "2023-07-28T12:45:15-07:00", + "credentialSubject": { + "id": "did:key:z6MkmNnvnfzW3nLiePweN3niGLnvp2BjKx3NM186vJ2yRg2z", + "firstName": "Satoshi", + "lastName": "Nakamoto" + }, + "credentialSchema": { + "id": "aed6f4f0-5ed7-4d7a-a3df-56430e1b2a88", + "type": "JsonSchema2023" + } + }, + "credentialJwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEI3o2TWttMVRtUldSUEs2bjIxUW5jVVpuazF0ZFlramU4OTZtWUN6aE1mUTY3YXNzRCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTA1NzM1MTUsImlzcyI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEIiwianRpIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL3YxL2NyZWRlbnRpYWxzLzQ2YmMzZDI1LTZhYWYtNGY1MC05OWVkLTYxYzRiMzVmNjQxMSIsIm5iZiI6MTY5MDU3MzUxNSwibm9uY2UiOiIzMGMwNDYxZi1jMWUxLTQwNDctYWUwYS01NjgzMjdkMzY4YTYiLCJzdWIiOiJkaWQ6a2V5Ono2TWttTm52bmZ6VzNuTGllUHdlTjNuaUdMbnZwMkJqS3gzTk0xODZ2SjJ5UmcyeiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiU2F0b3NoaSIsImxhc3ROYW1lIjoiTmFrYW1vdG8ifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImFlZDZmNGYwLTVlZDctNGQ3YS1hM2RmLTU2NDMwZTFiMmE4OCIsInR5cGUiOiJKc29uU2NoZW1hMjAyMyJ9fX0.xwqpDuO6PDeEqYr6DflbeR6mhuwvVg0uR43i-7Zhy2DdaH1e3Jt4DuiMy09tZQ2jAXki0rjMNgLt7dPpzOl8BA" +} +``` + +In the `credential` property we see an unsecured, but readable, version of the VC. The VC is signed and packaged as a JWT in the `credentialJwt` property. If you're interested, you can decode the JWT using a tool such as [jwt.io](https://jwt.io/). If you were to 'issue' or transmit the credential to a _holder_ you would just send this JWT value. + +## Getting Credentials + +Once you've created multiple credentials, you can view all credentials by making a `GET` request to `/v1/credentials`. This endpoint also supports three query parameters: `issuer`, `schema`, and `subject` which can be used mutually exclusively. + +You can get a single credential by making a `GET` request to `/v1/credentials/{id}`. + +## Other Credential Operations + +To learn about verifying credentials [read more here](verification.md). You can also learn more about [credential status here](status.md). + diff --git a/doc/howto/did.md b/doc/howto/did.md index 414134c25..d3b6fbf30 100644 --- a/doc/howto/did.md +++ b/doc/howto/did.md @@ -33,7 +33,7 @@ Upon a successful request you should see a response such as: } ``` -## Creating A DID +## Creating a DID You can create a DID by sending a `PUT` request to the `/v1/dids/{method}` endpoint. The request body needs two pieces of information: a method and a key type. The method must be supported by the service, and the key type must be supported by the method. You can find out more specifics about what each method supports [by looking at the SDK](https://github.com/TBD54566975/ssi-sdk/tree/main/did). Certain methods may support additional properties in an optional `options` fields. @@ -41,12 +41,10 @@ For now let's keep things simple and create a new `did:key` with the key type [` **Create DID Key Request** -`PUT` to `/v1/dids/key` +Make a `PUT` request to `/v1/dids/key`. A sample CURL command is as follows: -```json -{ - "keyType": "Ed25519" -} +```bash +curl -X PUT localhost:3000/v1/dids/key -d '{"keyType": "Ed25519"}' ``` If successful, you should see a response such as... @@ -117,6 +115,3 @@ You can get a specific DID's document by making a `GET` request to the method's ## DIDs Outside the Service The [universal resolver](https://github.com/decentralized-identity/universal-resolver) is a project at the [Decentralized Identity Foundation](https://identity.foundation/) aiming to enable the resolution of _any_ DID Document. The service, when run with [Docker Compose, runs a select number of these drivers (and more can be configured). It's possible to leverage the resolution of DIDs not supported by the service by making `GET` requests to `/v1/dids/resolver/{did}`. - - - diff --git a/doc/howto/schema.md b/doc/howto/schema.md index e69de29bb..45999ce78 100644 --- a/doc/howto/schema.md +++ b/doc/howto/schema.md @@ -0,0 +1,123 @@ +# How To: Create a Schema + +## Background + +When creating [Verifiable Credentials](https://www.w3.org/TR/vc-data-model) it's useful to have a mechanism to define the shape the data in the credential takes, in a consistent manner. The VC Data Model uses an open world data model, and with it, provides a mechanism to "extend" the core terminology to add any term with a technology known as [JSON-LD](https://json-ld.org/). JSON-LD is responsible for the `@context` property visible in VCs, DIDs, and other documents in the SSI space. However, JSON-LD is focused on _semantics_, answering the question "do we have a shared understanding of what this thing is?" more specifically, for a name credential, does your concept of "name" match mine. Though the core data model is a JSON-LD data model, processing VCs as JSON-LD is not a requirement. The SSI Service chooses to take a simpler approach and [process VCs as pure JSON](https://www.w3.org/TR/vc-data-model/#json). + +When constructing and processing VCs as pure JSON it is useful to have a mechanism to define the data and add some light validation onto the shape that data takes. [JSON Schema](https://json-schema.org/) is a widely used, and widely supported toolset that enables such functionalty: the ability to define a schema, which provides a set of properties (both required and optional), and some light validation on top of those properties. The VC Data Model has [a section on data schemas](https://www.w3.org/TR/vc-data-model/#data-schemas) that enables this functionality. + +## Intro to JSON Schema with Verifiable Credentials + +Making use of the `credentialSchema` property [defined in the VC Data Model](https://www.w3.org/TR/vc-data-model/#data-schemas) TBD and other collaborators in the W3C are working on [a new specification](https://w3c.github.io/vc-json-schema/) which enables a standards-compliant path to using JSON Schema with Verifiable Credentials. The VC JSON Schema specification defines two options for using JSON Schemas: the first, a plan JSON Schema that can apply to _any set of properties_ in a VC, and the second, a Verifiable Credential that wraps a JSON Schema. + +In some cases it is useful to package a JSON Schema as a Verifiable Credential to retain information about authorship (who created the schema), when it was created, and enable other features the VC Data Model offers, such as the ability to suspend the usage of a schema with [a status](https://www.w3.org/TR/vc-data-model/#status). + +An example email JSON Schema using [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html) is provided below: + +```json +{ + "$id": "https://example.com/schemas/email.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "name": "Email Address", + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] +} +``` + +We can see that the schema defines a property `emailAddress` of JSON type `string`, and it is required. This means that any piece of JSON we apply this schema to will pass if a valid `emailAddress` property is present and fail otherwise. + +Now that we have a valid JSON Schema, we'll need to transform it so it's useful in being applied to a Verifiable Credential, not just any arbitrary JSON. We know that we want the presence of an `emailAddress` in the `credentialSubject` property of a VC and adjust the schema accordingly: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "name": "Email Credential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } +} +``` + +Now our schema, applied to a Verifiable Credential, will guarantee that the `credentialSubject` property contains a valid `emailAddress` property. + +## Creating a Schema + +The service exposes a set of APIs for managing schemas. To create a schema you have two options: signed or not. As mentioned earlier, the signed version of a schema is packaged as a Verifiable Credential. To create a signed schema you'll need to pass in two additional properties – the issuer DID and the ID of the verification method to use to sign the schema. We'll keep things simple for now and create an unsigned schema. + +After forming a valid JSON Schema, generate a `PUT` request to `/v1/schemas` as follows: + +```bash +curl -X PUT localhost:3000/v1/schemas -d '{ + "name": "Email Credential", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } +}' +``` + +Upon success you'll see a response which includes the schema you passed in, with a service-generated identifier for the schema. You'll also notice a type `JsonSchema2023`, which is defined by the [VC JSON Schema specification](https://w3c.github.io/vc-json-schema/#jsonschema2023): + +```json +{ + "id": "ebeebf7b-d452-4832-b8d3-0042ec80e108", + "type": "JsonSchema2023", + "schema": { + "$id": "http://localhost:3000/v1/schemas/ebeebf7b-d452-4832-b8d3-0042ec80e108", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "name": "Email Credential", + "properties": { + "credentialSubject": { + "properties": { + "emailAddress": { + "format": "email", + "type": "string" + } + }, + "required": [ + "emailAddress" + ], + "type": "object" + } + }, + "type": "object" + } +} +``` + +Now you're ready to use the schema in [creating a credential](credential.md). + +## Getting Schemas + +Once you've created multiple schemas, you can view them all by make a `GET` request to the `v1/schemas` endpoint. Future enhancements may enable filtering based on name, author, or other properties. + +You can get a specific schema by make a `GET` request to the `v1/schemas/{schemaId}` endpoint. + diff --git a/doc/swagger.yaml b/doc/swagger.yaml index 9b2d81608..c4f4282b9 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -2788,7 +2788,7 @@ paths: type: string summary: Batch Create DIDs tags: - - CredentialAPI + - DecentralizedIdentityAPI /v1/dids/resolver/{id}: get: consumes: diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index d07386ce9..352bc9955 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -11,6 +11,7 @@ import ( "github.com/gin-gonic/gin" "github.com/goccy/go-json" "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/framework" "github.com/tbd54566975/ssi-service/pkg/server/pagination" @@ -408,7 +409,7 @@ func NewBatchDIDRouter(svc *did.BatchService) *BatchDIDRouter { // @Summary Batch Create DIDs // @Description Create a batch of verifiable credentials. The operation is atomic, meaning that all requests will // @Description succeed or fail. This is currently only supported for the DID method named `did:key`. -// @Tags CredentialAPI +// @Tags DecentralizedIdentityAPI // @Accept json // @Produce json // @Param method path string true "Method. Only `key` is supported." diff --git a/pkg/service/schema/service.go b/pkg/service/schema/service.go index 4ab277158..57b562991 100644 --- a/pkg/service/schema/service.go +++ b/pkg/service/schema/service.go @@ -164,8 +164,6 @@ func (s Service) createCredentialSchema(ctx context.Context, jsonSchema schema.J // set subject value as the schema subject := credential.CredentialSubject(jsonSchema) - // TODO(gabe) remove this after https://github.com/TBD54566975/ssi-sdk/pull/404 is merged - subject[credential.VerifiableCredentialIDProperty] = schemaURI if err := builder.SetCredentialSubject(subject); err != nil { return nil, sdkutil.LoggingErrorMsgf(err, "could not set subject: %+v", subject) } From b43f45c7c1d4163692ca4aa74649fb92d7789803 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Fri, 28 Jul 2023 17:48:46 -0400 Subject: [PATCH 05/20] Adding how-to for DID Configuration (#611) * Adding how-to for DID Configuration * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Update doc/howto/wellknown.md Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> * Undo stuff that was done --------- Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> --- doc/README.md | 2 +- doc/howto/wellknown.md | 104 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 doc/howto/wellknown.md diff --git a/doc/README.md b/doc/README.md index 8c4c21403..834ca3c91 100644 --- a/doc/README.md +++ b/doc/README.md @@ -52,6 +52,6 @@ the SSI Service to get up to speed with its functionality. | [Revoke/Suspend a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/status.md) | Get started with credential status functionality | | [[TODO] Requesting and Verifying Credentials with Presentation Exchange](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Presentation Exchange functionality | | [[TODO] Accepting Applications for and Issuing Credentials using Credential Manifest](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Credential Manifest functionality | -| [[TODO] Creating a Well Known File for your DID](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with DID Well Known functionality | +| [Link your DID with a Website](./howto/wellknown.md) | Get started with DID Well Known functionality | diff --git a/doc/howto/wellknown.md b/doc/howto/wellknown.md new file mode 100644 index 000000000..d2e130dcd --- /dev/null +++ b/doc/howto/wellknown.md @@ -0,0 +1,104 @@ +# How To: Link your DID with a Website + +## Background + +A [DID Configuration Resource](https://identity.foundation/.well-known/resources/did-configuration/) provides proof of a bi-directional relationship between the controller of a web domain and a DID via cryptographically verifiable signature, associated with a DID's key material. + +## Steps + +You can use a DID Configuration Resource to advertise that the same entity which controls a given website also controls a DID. The SSI Service does all the heavy lifting to make it easy to create such a resource, linking DIDs that created within the service to a website you control. The steps for doing so are outlined below. + +### Prerequisites + +* A DID was created with SSI Service. See [How To: Create A DID](./did.md) +* You control an origin (e.g. like https://www.tbd.website). +* You are able to host files in a path within that origin.(e.g. you can host the file returned by https://www.tbd.website/.well-known/did-configuration.json) + +For the purposes of our example, let's assume that the did created was `did:key:z6MkkZDjunoN4gyPMx5TSy7Mfzw22D2RZQZUcx46bii53Ex3`. + +### 1. Create a DIDConfiguration + +Make a `PUT` request to `/v1/did-configurations`: + +```json +{ + "expirationDate": "2051-10-05T14:48:00.000Z", + "issuanceDate": "2021-10-05T14:48:00.000Z", + "issuerDid": "did:key:z6MkmM43K3x5xAgzkLRW9r6HCv5c4QKfD2wjfi6tiW3CuzjZ", + "origin": "https://www.tbd.website", + "verificationMethodId": "did:key:z6MkmM43K3x5xAgzkLRW9r6HCv5c4QKfD2wjfi6tiW3CuzjZ#z6MkmM43K3x5xAgzkLRW9r6HCv5c4QKfD2wjfi6tiW3CuzjZ" +} +``` + +Or if you like CURLing: + +```shell +curl -X PUT 'localhost:3000/v1/did-configurations' -d '{ + "expirationDate": "2051-10-05T14:48:00.000Z", + "issuanceDate": "2021-10-05T14:48:00.000Z", + "issuerDid": "did:key:z6MkmM43K3x5xAgzkLRW9r6HCv5c4QKfD2wjfi6tiW3CuzjZ", + "origin": "https://www.tbd.website", + "verificationMethodId": "did:key:z6MkmM43K3x5xAgzkLRW9r6HCv5c4QKfD2wjfi6tiW3CuzjZ#z6MkmM43K3x5xAgzkLRW9r6HCv5c4QKfD2wjfi6tiW3CuzjZ" +}' +``` + +Upon success you will see a response such as... + +```json +{ + "wellKnownLocation": "https://www.tbd.website/.well-known/did-configuration.json", + "didConfiguration": { + "@context": "https://identity.foundation/.well-known/did-configuration/v1", + "linked_dids": [ + "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa21NNDNLM3g1eEFnemtMUlc5cjZIQ3Y1YzRRS2ZEMndqZmk2dGlXM0N1empaI3o2TWttTTQzSzN4NXhBZ3prTFJXOXI2SEN2NWM0UUtmRDJ3amZpNnRpVzNDdXpqWiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI1ODAxMzAwODAsImlhdCI6MTYzMzQ0NTI4MCwiaXNzIjoiZGlkOmtleTp6Nk1rbU00M0szeDV4QWd6a0xSVzlyNkhDdjVjNFFLZkQyd2pmaTZ0aVczQ3V6aloiLCJuYmYiOjE2MzM0NDUyODAsIm5vbmNlIjoiNzljN2UzMTgtMDEzMS00ODQ4LWJmOTMtODNiZGI1MmQ2YjZmIiwic3ViIjoiZGlkOmtleTp6Nk1rbU00M0szeDV4QWd6a0xSVzlyNkhDdjVjNFFLZkQyd2pmaTZ0aVczQ3V6aloiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJvcmlnaW4iOiJodHRwczovL3d3dy50YmQud2Vic2l0ZSJ9LCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.9oeN5rGzCaAMttOic47LOxIsjqpgH2DojGzsLiENy0UOKPdX66GJaEUEZSllWoGYOLqBnr6VWMnFgSk381jcAQ" + ] + } +} +``` + +This contains two properties: `wellKnownLocation` which describes where you should be hosting the content and `didConfiguration` which is content that you will host. + +### 2. Host the created DID Configuration + +This next step is for you to do outside of the service. You have to ensure that the value of `wellKnownLocation` resolves to a JSON file. The contents of the file should be the value of `didConfiguration`. In our example, we would have to make sure that the URL `https://www.tbd.website/.well-known/did-configuration.json` returns the JSON object described below. + +```json +{ + "@context": "https://identity.foundation/.well-known/did-configuration/v1", + "linked_dids": [ + "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa21NNDNLM3g1eEFnemtMUlc5cjZIQ3Y1YzRRS2ZEMndqZmk2dGlXM0N1empaI3o2TWttTTQzSzN4NXhBZ3prTFJXOXI2SEN2NWM0UUtmRDJ3amZpNnRpVzNDdXpqWiIsInR5cCI6IkpXVCJ9.eyJleHAiOjI1ODAxMzAwODAsImlhdCI6MTYzMzQ0NTI4MCwiaXNzIjoiZGlkOmtleTp6Nk1rbU00M0szeDV4QWd6a0xSVzlyNkhDdjVjNFFLZkQyd2pmaTZ0aVczQ3V6aloiLCJuYmYiOjE2MzM0NDUyODAsIm5vbmNlIjoiNzljN2UzMTgtMDEzMS00ODQ4LWJmOTMtODNiZGI1MmQ2YjZmIiwic3ViIjoiZGlkOmtleTp6Nk1rbU00M0szeDV4QWd6a0xSVzlyNkhDdjVjNFFLZkQyd2pmaTZ0aVczQ3V6aloiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJvcmlnaW4iOiJodHRwczovL3d3dy50YmQud2Vic2l0ZSJ9LCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.9oeN5rGzCaAMttOic47LOxIsjqpgH2DojGzsLiENy0UOKPdX66GJaEUEZSllWoGYOLqBnr6VWMnFgSk381jcAQ" + ] +} +``` + +### 3. Verify the DID Configuration + +Once you've done the steps above, you can also use SSI Service to verify that the DID configuration is correct! + +Make a `PUT` request to `/v1/did-configurations/verification`: + +```json +{ + "origin": "https://www.tbd.website" +} +``` + +... or using CURL + +```shell +curl -X PUT 'localhost:3000/v1/did-configurations/verification' -d '{ + "origin": "https://www.tbd.website" +}' +``` + +The result will look similar to the response below: + +```json +{ + "verified": false, + "didConfiguration": "{\n \"@context\":\"https://identity.foundation/.well-known/did-configuration/v1\",\n \"linked_dids\":[\n \"eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2g0QTZRVE5DQUZpNE5aZm5XdDhxTlNHWERHbk5YaHhYV2V3Y3BCcnpTMTl2IiwidHlwIjoiSldUIn0.eyJleHAiOjI1ODAxMzAwODAsImlzcyI6ImRpZDprZXk6ejZNa2g0QTZRVE5DQUZpNE5aZm5XdDhxTlNHWERHbk5YaHhYV2V3Y3BCcnpTMTl2IiwibmJmIjoxNjc2NTcyMDkzLCJzdWIiOiJkaWQ6a2V5Ono2TWtoNEE2UVROQ0FGaTROWmZuV3Q4cU5TR1hER25OWGh4WFdld2NwQnJ6UzE5diIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJEb21haW5MaW5rYWdlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6a2V5Ono2TWtoNEE2UVROQ0FGaTROWmZuV3Q4cU5TR1hER25OWGh4WFdld2NwQnJ6UzE5diIsImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDItMTZUMTI6Mjg6MTMtMDY6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwNTEtMTAtMDVUMTQ6NDg6MDAuMDAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1raDRBNlFUTkNBRmk0Tlpmbld0OHFOU0dYREduTlhoeFhXZXdjcEJyelMxOXYiLCJvcmlnaW4iOiJodHRwczovL3d3dy50YmQud2Vic2l0ZS8ifX19.szn9o_JhCLqYMH_SNtwFaJWViueg-pvrZW4G88cegh2Airh9ziQ7fYvSY4Hts2FlF6at8fMfAzrsnhJ-Fb0_Dw\",\n \"eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6dGJkLndlYnNpdGUiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjI1ODAxMzAwODAsImlzcyI6ImRpZDp3ZWI6dGJkLndlYnNpdGUiLCJuYmYiOjE2NzY1NzIwOTUsInN1YiI6ImRpZDp3ZWI6dGJkLndlYnNpdGUiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOndlYjp0YmQud2Vic2l0ZSIsImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDItMTZUMTI6Mjg6MTUtMDY6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwNTEtMTAtMDVUMTQ6NDg6MDAuMDAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOndlYjp0YmQud2Vic2l0ZSIsIm9yaWdpbiI6Imh0dHBzOi8vd3d3LnRiZC53ZWJzaXRlLyJ9fX0.bweamOE6q-K1jQ64cfqk-vhhuugSpLvcit3Q6REBM2z0CpvvTX4SttHF533oUIDovtOSqmAAOOUFCbrTJYQfDw\"\n ]\n }", + "reason": "verifying JWT credential: error getting key to verify credential<>: did has no verification methods with kid: did:key:z6Mkh4A6QTNCAFi4NZfnWt8qNSGXDGnNXhxXWewcpBrzS19v" +} +``` + +In this case, `verified` is `false`, so the `reason` property is populated, explaining where verification went wrong. Note that the `didConfiguration` value is a string. It represents the exact response that was received from the origin. \ No newline at end of file From 2958e96154adacd5ed54df0911940b960a7a0971 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Mon, 31 Jul 2023 10:29:38 -0400 Subject: [PATCH 06/20] Use the correct verifier for DI VCs (#613) Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> --- internal/credential/verification.go | 11 +- pkg/server/server_did_configuration_test.go | 388 +++++++++++++++++++- pkg/server/server_did_test.go | 2 +- pkg/service/well-known/did_configuration.go | 14 +- 4 files changed, 399 insertions(+), 16 deletions(-) diff --git a/internal/credential/verification.go b/internal/credential/verification.go index 27ce166de..536b12e95 100644 --- a/internal/credential/verification.go +++ b/internal/credential/verification.go @@ -8,6 +8,8 @@ import ( "github.com/TBD54566975/ssi-sdk/credential/integrity" "github.com/TBD54566975/ssi-sdk/credential/validation" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/crypto/jwx" + "github.com/TBD54566975/ssi-sdk/cryptosuite/jws2020" "github.com/TBD54566975/ssi-sdk/did/resolution" sdkutil "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" @@ -99,14 +101,19 @@ func (v Validator) VerifyDataIntegrityCredential(ctx context.Context, credential } // construct a signature validator from the verification information - verifier, err := keyaccess.NewDataIntegrityKeyAccess(issuer, verificationMethod, pubKey) + publicKeyJWK, err := jwx.PublicKeyToPublicKeyJWK(verificationMethod, pubKey) + if err != nil { + return sdkutil.LoggingErrorMsgf(err, "could not convert private key to JWK: %s", verificationMethod) + } + verifier, err := jws2020.NewJSONWebKeyVerifier(issuer, *publicKeyJWK) if err != nil { errMsg := fmt.Sprintf("could not create validator for kid %s", verificationMethod) return sdkutil.LoggingErrorMsg(err, errMsg) } + cryptoSuite := jws2020.GetJSONWebSignature2020Suite() // verify the signature on the credential - if err = verifier.Verify(&credential); err != nil { + if err = cryptoSuite.Verify(verifier, &credential); err != nil { return sdkutil.LoggingErrorMsg(err, "could not verify the credential's signature") } diff --git a/pkg/server/server_did_configuration_test.go b/pkg/server/server_did_configuration_test.go index e0d0afe62..e7f7be5af 100644 --- a/pkg/server/server_did_configuration_test.go +++ b/pkg/server/server_did_configuration_test.go @@ -20,6 +20,340 @@ import ( "gopkg.in/h2non/gock.v1" ) +const w3cCredentialContext = `{ + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "credentialSchema": { + "@id": "cred:credentialSchema", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + + "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018" + } + }, + "credentialStatus": {"@id": "cred:credentialStatus", "@type": "@id"}, + "credentialSubject": {"@id": "cred:credentialSubject", "@type": "@id"}, + "evidence": {"@id": "cred:evidence", "@type": "@id"}, + "expirationDate": {"@id": "cred:expirationDate", "@type": "xsd:dateTime"}, + "holder": {"@id": "cred:holder", "@type": "@id"}, + "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, + "issuer": {"@id": "cred:issuer", "@type": "@id"}, + "issuanceDate": {"@id": "cred:issuanceDate", "@type": "xsd:dateTime"}, + "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, + "refreshService": { + "@id": "cred:refreshService", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + + "ManualRefreshService2018": "cred:ManualRefreshService2018" + } + }, + "termsOfUse": {"@id": "cred:termsOfUse", "@type": "@id"}, + "validFrom": {"@id": "cred:validFrom", "@type": "xsd:dateTime"}, + "validUntil": {"@id": "cred:validUntil", "@type": "xsd:dateTime"} + } + }, + + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + + "holder": {"@id": "cred:holder", "@type": "@id"}, + "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, + "verifiableCredential": {"@id": "cred:verifiableCredential", "@type": "@id", "@container": "@graph"} + } + }, + + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "EcdsaSecp256r1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "RsaSignature2018": { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@version": 1.1, + "@protected": true, + + "challenge": "sec:challenge", + "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"}, + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + + "id": "@id", + "type": "@type", + + "sec": "https://w3id.org/security#", + + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"} + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"} + } + }, + + "proof": {"@id": "https://w3id.org/security#proof", "@type": "@id", "@container": "@graph"} + } +}` + +const wellKnownDIDContext = `{ + "@context": [ + { + "@version": 1.1, + "@protected": true, + "LinkedDomains": "https://identity.foundation/.well-known/resources/did-configuration/#LinkedDomains", + "DomainLinkageCredential": "https://identity.foundation/.well-known/resources/did-configuration/#DomainLinkageCredential", + "origin": "https://identity.foundation/.well-known/resources/did-configuration/#origin", + "linked_dids": "https://identity.foundation/.well-known/resources/did-configuration/#linked_dids" + } + ] +}` + +const vcJWS2020Context = `{ + "@context": { + "privateKeyJwk": { + "@id": "https://w3id.org/security#privateKeyJwk", + "@type": "@json" + }, + "JsonWebKey2020": { + "@id": "https://w3id.org/security#JsonWebKey2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + } + } + }, + "JsonWebSignature2020": { + "@id": "https://w3id.org/security#JsonWebSignature2020", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +}` + func TestDIDConfigurationAPI(t *testing.T) { t.Run("Create DID Configuration", func(t *testing.T) { for _, test := range testutil.TestDatabases { @@ -79,22 +413,58 @@ func TestDIDConfigurationAPI(t *testing.T) { didConfigurationService := setupDIDConfigurationRouter(t, keyStoreService, didService.GetResolver(), schemaService) - t.Run("passes for TBD", func(t *testing.T) { + t.Run("passes for complex did configuration resource", func(t *testing.T) { defer gock.Off() + client := didConfigurationService.Service.HTTPClient + gock.InterceptClient(client) + defer gock.RestoreClient(client) - gock.InterceptClient(didConfigurationService.Service.HTTPClient) - gock.New("https://www.tbd.website"). + // mock all the contexts needed for json-ld canonicalization + gock.New("https://www.w3.org"). + Get("/2018/credentials/v1"). + Reply(200). + BodyString(w3cCredentialContext) + gock.New("https://identity.foundation").Get("/.well-known/did-configuration/v1").Reply(200).BodyString(wellKnownDIDContext) + gock.New("https://www.w3.org"). + Get("/2018/credentials/v1"). + Reply(200). + BodyString(w3cCredentialContext) + gock.New("https://identity.foundation").Get("/.well-known/did-configuration/v1").Reply(200).BodyString(wellKnownDIDContext) + gock.New("https://w3id.org").Get("/security/suites/jws-2020/v1").Reply(200).BodyString(vcJWS2020Context) + + gock.New("https://identity.foundation"). Get("/.well-known/did-configuration.json"). Reply(200). BodyString(`{ - "@context": "https://identity.foundation/.well-known/did-configuration/v1", - "linked_dids": [ - "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2hDcWNza2haU0V4NEZHbU5XNXNNcVVoZzJyZnc2YnMyWkNCZXBzTkZjQ3Y1I3o2TWtoQ3Fjc2toWlNFeDRGR21OVzVzTXFVaGcycmZ3NmJzMlpDQmVwc05GY0N2NSIsInR5cCI6IkpXVCJ9.eyJleHAiOjI1ODAxMzAwODAsImlhdCI6MTYzMzQ0NTI4MCwiaXNzIjoiZGlkOmtleTp6Nk1raENxY3NraFpTRXg0RkdtTlc1c01xVWhnMnJmdzZiczJaQ0JlcHNORmNDdjUiLCJuYmYiOjE2MzM0NDUyODAsIm5vbmNlIjoiOTU5MTczMDktYWJmMC00OWI0LTkzYTktMjQyYTg4NmFhYTBmIiwic3ViIjoiZGlkOmtleTp6Nk1raENxY3NraFpTRXg0RkdtTlc1c01xVWhnMnJmdzZiczJaQ0JlcHNORmNDdjUiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJvcmlnaW4iOiJodHRwczovL3d3dy50YmQud2Vic2l0ZSJ9LCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.OV56DeG2bp4Hd-kulCUgZCk4PgY51sEzw4REZeUfiS4Cqa9LnbK0fkiJ2jPAZoBzTSDq75Hxc2qX5Ey0JVCoBw" - ] - }`) + "@context": "https://identity.foundation/.well-known/did-configuration/v1", + "linked_dids": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://identity.foundation/.well-known/did-configuration/v1" + ], + "issuer": "did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM", + "issuanceDate": "2020-12-04T14:08:28-06:00", + "expirationDate": "2025-12-04T14:08:28-06:00", + "type": ["VerifiableCredential", "DomainLinkageCredential"], + "credentialSubject": { + "id": "did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM", + "origin": "https://identity.foundation" + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2020-12-04T20:08:28.540Z", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..D0eDhglCMEjxDV9f_SNxsuU-r3ZB9GR4vaM9TYbyV7yzs1WfdUyYO8rFZdedHbwQafYy8YOpJ1iJlkSmB4JaDQ", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM#z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM" + } + }, + "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNI3o2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSJ9.eyJleHAiOjE3NjQ4NzkxMzksImlzcyI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwibmJmIjoxNjA3MTEyNzM5LCJzdWIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rb1RIc2dOTnJieThKekNOUTFpUkx5VzVRUTZSOFh1dTZBQThpZ0dyTVZQVU0iLCJvcmlnaW4iOiJpZGVudGl0eS5mb3VuZGF0aW9uIn0sImV4cGlyYXRpb25EYXRlIjoiMjAyNS0xMi0wNFQxNDoxMjoxOS0wNjowMCIsImlzc3VhbmNlRGF0ZSI6IjIwMjAtMTItMDRUMTQ6MTI6MTktMDY6MDAiLCJpc3N1ZXIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJEb21haW5MaW5rYWdlQ3JlZGVudGlhbCJdfX0.aUFNReA4R5rcX_oYm3sPXqWtso_gjPHnWZsB6pWcGv6m3K8-4JIAvFov3ZTM8HxPOrOL17Qf4vBFdY9oK0HeCQ" + ] +}`) request := wellknown.VerifyDIDConfigurationRequest{ - Origin: "https://www.tbd.website/", + Origin: "https://identity.foundation", } value := newRequestValue(t, request) req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/did-configurations/verification", value) diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go index b2815aaa6..bfc15eeea 100644 --- a/pkg/server/server_did_test.go +++ b/pkg/server/server_did_test.go @@ -194,11 +194,11 @@ func TestDIDAPI(t *testing.T) { // reset recorder between calls w = httptest.NewRecorder() + defer gock.Off() gock.New(testIONResolverURL). Post("/operations"). Reply(200). BodyString(string(BasicDIDResolution)) - defer gock.Off() // with body, good key type, no options createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} diff --git a/pkg/service/well-known/did_configuration.go b/pkg/service/well-known/did_configuration.go index d5c13a397..555b815c7 100644 --- a/pkg/service/well-known/did_configuration.go +++ b/pkg/service/well-known/did_configuration.go @@ -67,12 +67,12 @@ func (s DIDConfigurationService) VerifyDIDConfiguration(ctx context.Context, req } httpResponse, err := s.HTTPClient.Do(httpReq) - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(httpResponse.Body) if err != nil { return nil, errors.Wrap(err, "performing http request") } + defer func() { + _ = httpResponse.Body.Close() + }() if !util.Is2xxResponse(httpResponse.StatusCode) { return nil, errors.Errorf("expected 2xx code, got %d", httpResponse.StatusCode) @@ -112,7 +112,8 @@ func (s DIDConfigurationService) VerifyDIDConfiguration(ctx context.Context, req // 3. The credentialSubject.origin property MUST be present, and its value MUST match the origin the resource was requested from. credentialSubjectOrigin := domainLinkageCredential.Credential.CredentialSubject["origin"].(string) - if !strings.HasPrefix(httpReq.URL.String(), credentialSubjectOrigin) { + requestedURL := httpReq.URL.String() + if !originMatches(requestedURL, credentialSubjectOrigin) { response.Reason = fmt.Sprintf("The credentialSubject.origin property MUST be present, and its value MUST match the origin the resource was requested from") return &response, nil } @@ -130,6 +131,11 @@ func (s DIDConfigurationService) VerifyDIDConfiguration(ctx context.Context, req response.Verified = true return &response, nil } + +func originMatches(requestedURL string, credentialSubjectOrigin string) bool { + return strings.Contains(requestedURL, credentialSubjectOrigin) +} + func (s DIDConfigurationService) CreateDIDConfiguration(ctx context.Context, req *CreateDIDConfigurationRequest) (*CreateDIDConfigurationResponse, error) { builder := credential.NewVerifiableCredentialBuilder() if err := builder.SetIssuer(req.IssuerDID); err != nil { From ddf663a922e89794ce3355e08674312533b09c48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 14:40:27 +0000 Subject: [PATCH 07/20] Bump go.einride.tech/aip from 0.60.0 to 0.61.0 (#618) --- go.mod | 6 +++--- go.sum | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index d09fe3170..616de97f2 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 - go.einride.tech/aip v0.60.0 + go.einride.tech/aip v0.61.0 go.etcd.io/bbolt v1.3.7 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.42.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 @@ -170,8 +170,8 @@ require ( golang.org/x/text v0.11.0 // indirect golang.org/x/tools v0.9.3 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230725213213-b022f6e96895 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 // indirect google.golang.org/grpc v1.56.2 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect diff --git a/go.sum b/go.sum index cb1362cfe..cd11d1603 100644 --- a/go.sum +++ b/go.sum @@ -500,8 +500,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.einride.tech/aip v0.60.0 h1:h6bgabZ5BCfAptbGex8jbh3VvPBRLa6xq+pQ1CAjHYw= -go.einride.tech/aip v0.60.0/go.mod h1:SdLbSbgSU60Xkb4TMkmsZEQPHeEWx0ikBoq5QnqZvdg= +go.einride.tech/aip v0.61.0 h1:H7r59BtQDcj8kGNa0Dytw88so1iWrzp6mSOEQgcIJWI= +go.einride.tech/aip v0.61.0/go.mod h1:YVrCQRL7SCB5Mv7i2ZF1R6vkLPh844RQBCLrrLcefaU= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -854,11 +854,11 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= -google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU= -google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto v0.0.0-20230725213213-b022f6e96895 h1:f4HtRHVw5oEuUSMwhzcRW+w4X9++1iU+MZ9cRAHbWxk= +google.golang.org/genproto/googleapis/api v0.0.0-20230725213213-b022f6e96895 h1:9rcwSXpqHEULy96NKetvTJMCLnvnod0LcF8A/ULEBxE= +google.golang.org/genproto/googleapis/api v0.0.0-20230725213213-b022f6e96895/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 h1:co8AMhI481nhd3WBfW2mq5msyQHNBcGn7G9GCEqz45k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -921,7 +921,7 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 9fb4ef58d0463982f020348d283abb9aa0137225 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:13:17 +0000 Subject: [PATCH 08/20] Bump google.golang.org/api from 0.132.0 to 0.134.0 (#617) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 616de97f2..dd702fae4 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( go.opentelemetry.io/otel/trace v1.16.0 golang.org/x/crypto v0.11.0 golang.org/x/term v0.10.0 - google.golang.org/api v0.132.0 + google.golang.org/api v0.134.0 gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/h2non/gock.v1 v1.1.2 ) diff --git a/go.sum b/go.sum index cd11d1603..f6974d4dc 100644 --- a/go.sum +++ b/go.sum @@ -807,8 +807,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.132.0 h1:8t2/+qZ26kAOGSmOiHwVycqVaDg7q3JDILrNi/Z6rvc= -google.golang.org/api v0.132.0/go.mod h1:AeTBC6GpJnJSRJjktDcPX0QwtS8pGYZOV6MSuSCusw0= +google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw= +google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From de454c74d454e75a0269e9cd2e06ddbae50e085e Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Mon, 31 Jul 2023 14:49:48 -0400 Subject: [PATCH 09/20] Update ssi-sdk version (#614) * Update ssi-sdk version * spec * prep for update of sdk * Deps n swag --------- Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> --- doc/swagger.yaml | 30 ++++++++++++++++++- go.mod | 4 +-- go.sum | 8 ++--- .../didweb_resolver_integration_test.go | 9 +++--- pkg/server/router/did_test.go | 8 ++--- pkg/server/server_did_test.go | 8 ++--- pkg/service/did/service.go | 2 +- pkg/service/did/web.go | 7 +++-- 8 files changed, 51 insertions(+), 25 deletions(-) diff --git a/doc/swagger.yaml b/doc/swagger.yaml index c4f4282b9..a93a630d0 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -2128,20 +2128,36 @@ definitions: resolution.DocumentMetadata: properties: canonicalId: + description: See `canonicalId` in https://www.w3.org/TR/did-core/#did-document-metadata type: string created: + description: See `created` in https://www.w3.org/TR/did-core/#did-document-metadata type: string deactivated: + description: See `deactivated` in https://www.w3.org/TR/did-core/#did-document-metadata type: boolean equivalentId: - type: string + description: See `equivalentId` in https://www.w3.org/TR/did-core/#did-document-metadata + items: + type: string + type: array + method: + allOf: + - $ref: '#/definitions/resolution.Method' + description: |- + Optional information that is specific to the DID Method of the DID Document resolved. Populated only + for sidetree based did methods (e.g. ION), as described in https://identity.foundation/sidetree/spec/#did-resolver-output nextUpdate: + description: See `nextUpdate` in https://www.w3.org/TR/did-core/#did-document-metadata type: string nextVersionId: + description: See `nextVersionId` in https://www.w3.org/TR/did-core/#did-document-metadata type: string updated: + description: See `updated` in https://www.w3.org/TR/did-core/#did-document-metadata type: string versionId: + description: See `versionId` in https://www.w3.org/TR/did-core/#did-document-metadata type: string type: object resolution.Error: @@ -2162,6 +2178,18 @@ definitions: error: $ref: '#/definitions/resolution.Error' type: object + resolution.Method: + properties: + published: + description: The `method` property in https://identity.foundation/sidetree/spec/#did-resolver-output + type: boolean + recoveryCommitment: + description: The `recoveryCommitment` property in https://identity.foundation/sidetree/spec/#did-resolver-output + type: string + updateCommitment: + description: The `updateCommitment` property in https://identity.foundation/sidetree/spec/#did-resolver-output + type: string + type: object schema.JSONSchema: additionalProperties: {} type: object diff --git a/go.mod b/go.mod index dd702fae4..a10a782e8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/BurntSushi/toml v1.3.2 - github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230711190054-bce640c9bf25 + github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230731175253-d5c302a1d9b9 github.com/alicebob/miniredis/v2 v2.30.4 github.com/ardanlabs/conf v1.5.0 github.com/benbjohnson/clock v1.3.5 @@ -143,7 +143,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.2.0 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.9.5 // indirect diff --git a/go.sum b/go.sum index f6974d4dc..50cd64ddf 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230711190054-bce640c9bf25 h1:wW+49kQxN/BYcMkbDjQA9mkrUC9cYUV6HOFLP+JIx+E= -github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230711190054-bce640c9bf25/go.mod h1:lup1EqGAT730/c7dRF9Q8OvgsjWJewq4K2dFAyBV1vk= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230731175253-d5c302a1d9b9 h1:Ig2o+eOTFTaa9agWiz+Vz/7N4zoTJ2Na9PiRaYaAbXY= +github.com/TBD54566975/ssi-sdk v0.0.4-alpha.0.20230731175253-d5c302a1d9b9/go.mod h1:mVKRjfdpgmCxPwnfQluXGkgzsFyrPsjrCvHXCJ41avQ= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= @@ -437,8 +437,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= diff --git a/integration/didweb_resolver_integration_test.go b/integration/didweb_resolver_integration_test.go index e5b8fed67..823a40d80 100644 --- a/integration/didweb_resolver_integration_test.go +++ b/integration/didweb_resolver_integration_test.go @@ -12,7 +12,7 @@ func TestResolveDIDWebIntegration(t *testing.T) { } // A .well-known file exists at https://tbd.website/.well-known/did.json - didWebOutput, err := put(endpoint+version+"dids/web", `{ "keyType":"Ed25519", "options": {"didWebId":"did:web:tbd.website"}}`) + didWebOutput, err := put(endpoint+version+"dids/web", `{ "keyType":"Ed25519", "options": {"didWebId":"did:web:i-made-up-this.website"}}`) assert.NoError(t, err) did, err := getJSONElement(didWebOutput, "$.did.id") @@ -20,12 +20,11 @@ func TestResolveDIDWebIntegration(t *testing.T) { resolvedOutput, err := ResolveDID(did) assert.NoError(t, err) - didError, err := getJSONElement(resolvedOutput, "$.didResolutionMetadata.Error") - assert.NoError(t, err) - assert.Equal(t, "", didError) + _, err = getJSONElement(resolvedOutput, "$.didResolutionMetadata.Error") + assert.ErrorContains(t, err, "key error: Error not found in object") didDocumentID, err := getJSONElement(resolvedOutput, "$.didDocument.id") assert.NoError(t, err) - assert.Equal(t, "did:web:tbd.website", didDocumentID) + assert.Equal(t, "did:web:i-made-up-this.website", didDocumentID) } diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 97d25c89d..6a2c0d23b 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -199,13 +199,12 @@ func TestDIDRouter(t *testing.T) { createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", Options: createOpts}) assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not generate key for did:web") + assert.Contains(tt, err.Error(), "key type not supported") gock.Off() gock.New("https://example.com"). Get("/.well-known/did.json"). - Reply(200). - BodyString("") + Reply(404) // good key type createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) assert.NoError(tt, err) @@ -225,8 +224,7 @@ func TestDIDRouter(t *testing.T) { gock.Off() gock.New("https://tbd.website"). Get("/.well-known/did.json"). - Reply(200). - BodyString("") + Reply(404) // create a second DID createOpts = did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"} createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go index bfc15eeea..7cfce51c2 100644 --- a/pkg/server/server_did_test.go +++ b/pkg/server/server_did_test.go @@ -160,7 +160,7 @@ func TestDIDAPI(t *testing.T) { gock.New("https://example.com"). Get("/.well-known/did.json"). Reply(200). - BodyString(`{"didDocument": {"id": "did:web:example.com"}}`) + BodyString(`Something here that's not a DID'`) defer gock.Off() c = newRequestContextWithParams(w, req, params) @@ -279,8 +279,7 @@ func TestDIDAPI(t *testing.T) { gock.New("https://example.com"). Get("/.well-known/did.json"). - Reply(200). - BodyString(`{"didDocument": {"id": "did:web:example.com"}}`) + Reply(404) defer gock.Off() c := newRequestContextWithParams(w, req, params) @@ -299,8 +298,7 @@ func TestDIDAPI(t *testing.T) { req2 := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/web", requestReader2) gock.New("https://example.com"). Get("/.well-known/did.json"). - Reply(200). - BodyString(`{"didDocument": {"id": "did:web:example.com"}}`) + Reply(404) defer gock.Off() // Make sure it can't make another did:web of the same DIDWebID diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go index 44af368e1..ccd029573 100644 --- a/pkg/service/did/service.go +++ b/pkg/service/did/service.go @@ -144,7 +144,7 @@ func (s *Service) ResolveDID(request ResolveDIDRequest) (*ResolveDIDResponse, er return &ResolveDIDResponse{ ResolutionMetadata: &resolved.Metadata, DIDDocument: &resolved.Document, - DIDDocumentMetadata: &resolved.DocumentMetadata, + DIDDocumentMetadata: resolved.DocumentMetadata, }, nil } diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go index 158b207e8..381f36bf7 100644 --- a/pkg/service/did/web.go +++ b/pkg/service/did/web.go @@ -49,6 +49,9 @@ func (h *webHandler) GetMethod() did.Method { func (h *webHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) { logrus.Debugf("creating DID: %+v", request) + if !crypto.IsSupportedKeyType(request.KeyType) { + return nil, errors.Errorf("key type <%s> not supported", request.KeyType) + } // process options if request.Options == nil { return nil, errors.New("options cannot be empty") @@ -64,8 +67,8 @@ func (h *webHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* didWeb := web.DIDWeb(opts.DIDWebID) err := didWeb.Validate(ctx) - if err != nil { - return nil, errors.Wrap(err, "could not validate if did:web exists externally") + if err == nil { + return nil, fmt.Errorf("%s exists externally", didWeb.String()) } exists, err := h.storage.DIDExists(ctx, opts.DIDWebID) From d9327f3ec9be131f3822d91e3f67b1e49df2aa11 Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:19:36 -0700 Subject: [PATCH 10/20] How tos on credential status and verification (#616) * doc on verification * update cred status * add note on custodial keys in credentials * Update doc/howto/status.md Co-authored-by: Andres Uribe --------- Co-authored-by: Andres Uribe --- doc/howto/credential.md | 5 +- doc/howto/status.md | 212 ++++++++++++++++++++++++++++++++++++++ doc/howto/verification.md | 62 +++++++++++ 3 files changed, 276 insertions(+), 3 deletions(-) diff --git a/doc/howto/credential.md b/doc/howto/credential.md index 53564c6f2..b1cca530f 100644 --- a/doc/howto/credential.md +++ b/doc/howto/credential.md @@ -18,7 +18,7 @@ Out of the box we have support for exposing two [credential statuses](status.md) ## Creating a Verifiable Credential -Creating a credential using the SSI Service currently requires four pieces of data: an `issuer` DID, a `verificationMethodId` which must be contained within the issuer's DID Document, a `subject` DID (the DID who the claims are about), and `data` which is an arbitrary JSON object for the claims that you wish to be in the credential (in the `credentialSubject` property). +Creating a credential using the SSI Service currently requires four pieces of data: an `issuer` DID, a `verificationMethodId` which must be contained within the issuer's DID Document, a `subject` DID (the DID who the claims are about), and `data` which is an arbitrary JSON object for the claims that you wish to be in the credential (in the `credentialSubject` property). As noted in the [How To on DIDs](did.md), all DIDs managed by the SSI Service are _fully custodial_. This means that all keys referenced in those DIDs are stored by the SSI Service, which is necessary for the service to author any content on behalf of those DIDs. The cryptographic keys associated with the DID are encrypted at rest using a KEK (Key Encryption Key) which can optionally be used with a cloud HSM. There are additional optional properties that let you specify `evidence`, an `expiry`, and whether you want to make the credential `revocable` or `suspendable`. Let's keep things simple and issue our first credential which will attest to a person's first and last name. @@ -83,7 +83,7 @@ curl -X PUT localhost:3000/v1/credentials -d '{ }' ``` -Upon success we'll see a response such as: +Upon success we see a response such as: ```json { @@ -120,4 +120,3 @@ You can get a single credential by making a `GET` request to `/v1/credentials/{i ## Other Credential Operations To learn about verifying credentials [read more here](verification.md). You can also learn more about [credential status here](status.md). - diff --git a/doc/howto/status.md b/doc/howto/status.md index e69de29bb..1f72058af 100644 --- a/doc/howto/status.md +++ b/doc/howto/status.md @@ -0,0 +1,212 @@ +# How To: Revoke/Suspend a Credential + +## Background + +Though [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) are designed to give the holder a large degree of freedom in using their data, credential issuers are able to retain some control over the data they attest to after issuance. One of the mechanisms by which they retain this control is through the usage of credential status. Credential status can be implemented through any valid JSON-LD type, to specify any status such as whether a credential is suspended or revoked. The most prominently used type is through the [Status List](https://w3c.github.io/vc-status-list-2021/) type, a work item in the [VC Working Group](https://www.w3.org/groups/wg/vc). + +To make use of credential status, issuers must follow the rules outlined in the [Status List specification](https://w3c.github.io/vc-status-list-2021/#statuslist2021credential) to build a status list credential, and then include the requisite values in the `credentialStatus` property of any Verifiable Credential they issue according to the [Status List Entry](https://w3c.github.io/vc-status-list-2021/#statuslist2021entry) portion of the specification. + +## How does the Status List work? + +The Status List specification is designed to provide issuers a mechanism to express the status of a given credential, verifiers a mechanism to check the status of a given credential, and holders a set of privacy guarantees about status checks for credentials they hold. The way this works is by issuers creating a new credential that represents credential status. In our implementation credential status credentials are unique for each pair. The construction of this status credential uses a [bitstring](https://w3c.github.io/vc-status-list-2021/#security-considerations) which can provide _herd privacy_ for credential holders — in simpler terms this means that many credentials can be represented in a single bitstring, so it is not clear which credential/holder a verifier is requesting information about — this is great for holder privacy! + +Then, for each new credential an issuer creates for a given schema, a new credential status credential is created or an existing credential status credential is used. Each credential an issuer creates now contains a reference to the status list credential contained in the credential's `credentialStatus` property, which can be used by verifiers to check the status of the credential. + +**Example Credential Status Credential** + +```json +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "https://example.com/credentials/status/3", + "type": ["VerifiableCredential", "StatusList2021Credential"], + "issuer": "did:example:12345", + "issued": "2021-04-05T14:27:40Z", + "credentialSubject": { + "id": "https://example.com/status/3#list", + "type": "StatusList2021", + "statusPurpose": "revocation", + "encodedList": "H4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA" + }, + "proof": { ... } +} +``` + +**Example Credential with Credential Status** + +```json +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "https://example.com/credentials/23894672394", + "type": ["VerifiableCredential"], + "issuer": "did:example:12345", + "issued": "2021-04-05T14:27:42Z", + "credentialStatus": { + "id": "https://example.com/credentials/status/3#94567", + "type": "StatusList2021Entry", + "statusPurpose": "revocation", + "statusListIndex": "94567", + "statusListCredential": "https://example.com/credentials/status/3" + }, + "credentialSubject": { + "id": "did:example:6789", + "type": "Person" + }, + "proof": { ... } +} +``` + +In the first example above we can see a _status list credential_ which is used for many credentials issued by the issuer identified by the DID `did:example:12345`. In the second example above we can see a credential that `did:example:12345` issued to `did:example:6789`. The second example also shows a reference to the above status list credential in the given `credentialStatus` block. We see that the credential has a `statusListIndex` of `94567` which is needed by any verifier of the holder's credential to check its status. The verification process would be as follows: + +1. Holder `did:example:6789` presents their credential to a verifier. +2. Verifier makes a request to resolve the credential status credential identified by `https://example.com/credentials/status/3`. +3. Upon resolution the verifier checks the value of the bit string at index `94567`. +4. If present, the credential has the associated status (revoked), if absent, the credential does not have the associated status (not revoked). + +## Status in the SSI Service + +By now you should be familiar with [creating a credential](credential.md). Notably, that upon forming a request to create a credential there are a number of possible request values, two of which are `revocable` and `suspendable`. These options are exposed to give issuers the ability to specify status for credentials they create in the service. If either (or both) of the status values are set in the credential creation request then an associated status list credential will be created (if it does not yet exist) and a `credentialStatus` entry will be added to the newly created credential. + +We will assume you've followed the aforementioned guide on creating a credential, so you already have an issuer DID and person schema. Let's jump right into creating a revocable credential: + +### 1. Create a revocable credential + +Create a `PUT` request to `/v1/credentials` making sure the request body has the value `revocable` set to `true`. + +```json +curl -X PUT localhost:3000/v1/credentials -d '{ + "issuer": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "verificationMethodId": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD#z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "subject": "did:key:z6MkmNnvnfzW3nLiePweN3niGLnvp2BjKx3NM186vJ2yRg2z", + "schemaId": "aed6f4f0-5ed7-4d7a-a3df-56430e1b2a88", + "data": { + "firstName": "Satoshi", + "lastName": "Nakamoto" + }, + "revocable": true +}' +``` + +Upon success we see a response such as: + +```json +{ + "id": "8f9d58b2-c978-4317-96bd-35949ce76121", + "fullyQualifiedVerificationMethodId": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD#z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "credential": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "id": "http://localhost:3000/v1/credentials/8f9d58b2-c978-4317-96bd-35949ce76121", + "type": ["VerifiableCredential"], + "issuer": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "issuanceDate": "2023-07-31T11:18:26-07:00", + "credentialStatus": { + "id": "http://localhost:3000/v1/credentials/8f9d58b2-c978-4317-96bd-35949ce76121/status", + "statusListCredential": "http://localhost:3000/v1/credentials/status/b7a8bd19-f20d-4132-ac2e-137ff4d1511a", + "statusListIndex": "106493", + "statusPurpose": "revocation", + "type": "StatusList2021Entry" + }, + "credentialSubject": { + "firstName": "Satoshi", + "id": "did:key:z6MkmNnvnfzW3nLiePweN3niGLnvp2BjKx3NM186vJ2yRg2z", + "lastName": "Nakamoto" + }, + "credentialSchema": { + "id": "aed6f4f0-5ed7-4d7a-a3df-56430e1b2a88", + "type": "JsonSchema2023" + } + }, + "credentialJwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEI3o2TWttMVRtUldSUEs2bjIxUW5jVVpuazF0ZFlramU4OTZtWUN6aE1mUTY3YXNzRCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTA4Mjc1MDYsImlzcyI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEIiwianRpIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL3YxL2NyZWRlbnRpYWxzLzhmOWQ1OGIyLWM5NzgtNDMxNy05NmJkLTM1OTQ5Y2U3NjEyMSIsIm5iZiI6MTY5MDgyNzUwNiwibm9uY2UiOiI0ZGQyYzg1YS02NTFjLTQ3MDAtOTZhZC1hM2VlNTU1YTFmZTMiLCJzdWIiOiJkaWQ6a2V5Ono2TWttTm52bmZ6VzNuTGllUHdlTjNuaUdMbnZwMkJqS3gzTk0xODZ2SjJ5UmcyeiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN0YXR1cyI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC92MS9jcmVkZW50aWFscy84ZjlkNThiMi1jOTc4LTQzMTctOTZiZC0zNTk0OWNlNzYxMjEvc3RhdHVzIiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvdjEvY3JlZGVudGlhbHMvc3RhdHVzL2I3YThiZDE5LWYyMGQtNDEzMi1hYzJlLTEzN2ZmNGQxNTExYSIsInN0YXR1c0xpc3RJbmRleCI6IjEwNjQ5MyIsInN0YXR1c1B1cnBvc2UiOiJyZXZvY2F0aW9uIiwidHlwZSI6IlN0YXR1c0xpc3QyMDIxRW50cnkifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiU2F0b3NoaSIsImxhc3ROYW1lIjoiTmFrYW1vdG8ifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImFlZDZmNGYwLTVlZDctNGQ3YS1hM2RmLTU2NDMwZTFiMmE4OCIsInR5cGUiOiJKc29uU2NoZW1hMjAyMyJ9fX0.7mkFcjRXkFcVTB888NO1Ty85yTJz8dEdt8dHViE7iuQZvXwED9bfIMMSHU9mmqkokZtSldnaKcoPwO0WCuVwAQ" +} +``` + +Notably we see the `credentialStatus` entry in the credential we've created, with id `http://localhost:3000/v1/credentials/8f9d58b2-c978-4317-96bd-35949ce76121/status` and the status list credential that has been created, with id `http://localhost:3000/v1/credentials/status/b7a8bd19-f20d-4132-ac2e-137ff4d1511a`. + +### 2. Get a status list credential + +Next, let's get the crednetial's associated status list credential. We make a request to `/v1/credentials/status/{id}` to get the status list credential. + +```bash +curl http://localhost:3000/v1/credentials/status/b7a8bd19-f20d-4132-ac2e-137ff4d1511a +``` + +Upon success we see a response such as: + +```json +{ + "id": "b7a8bd19-f20d-4132-ac2e-137ff4d1511a", + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "http://localhost:3000/v1/credentials/status/b7a8bd19-f20d-4132-ac2e-137ff4d1511a", + "type": [ + "VerifiableCredential", + "StatusList2021Credential" + ], + "issuer": "did:key:z6Mkm1TmRWRPK6n21QncUZnk1tdYkje896mYCzhMfQ67assD", + "issuanceDate": "2023-07-31T18:18:26Z", + "credentialSubject": { + "encodedList": "H4sIAAAAAAAA/2IAAweGUTAKRsEoGAWjYBSMPAAIAAD//9BoYmEICAAA", + "id": "http://localhost:3000/v1/credentials/status/b7a8bd19-f20d-4132-ac2e-137ff4d1511a", + "statusPurpose": "revocation", + "type": "StatusList2021" + } + }, + "credentialJwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEI3o2TWttMVRtUldSUEs2bjIxUW5jVVpuazF0ZFlramU4OTZtWUN6aE1mUTY3YXNzRCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTA4Mjc1MDYsImlzcyI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEIiwianRpIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL3YxL2NyZWRlbnRpYWxzL3N0YXR1cy9iN2E4YmQxOS1mMjBkLTQxMzItYWMyZS0xMzdmZjRkMTUxMWEiLCJuYmYiOjE2OTA4Mjc1MDYsIm5vbmNlIjoiNzZmMjI5MzEtMjU5Mi00MDY1LTllODktMTM3ZGRkOTEyNGY5Iiwic3ViIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL3YxL2NyZWRlbnRpYWxzL3N0YXR1cy9iN2E4YmQxOS1mMjBkLTQxMzItYWMyZS0xMzdmZjRkMTUxMWEiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMvc3RhdHVzLWxpc3QvMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiU3RhdHVzTGlzdDIwMjFDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImVuY29kZWRMaXN0IjoiSDRzSUFBQUFBQUFBLzJJQUF3ZUdVVEFLUnNFb0dBV2pZQlNNUEFBSUFBRC8vOUJvWW1FSUNBQUEiLCJzdGF0dXNQdXJwb3NlIjoicmV2b2NhdGlvbiIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMSJ9fX0.eZMzdNib_QrfSni7i74vXR73X6knIsOIeTNm32j26mQsQlk9em5fKLN0dqlVK9o0v_zDZI2UzvaE2p5PRH3BDA" +} +``` + +With this status list credential we're able to check the status for the credential we created, which is identified by its id `8f9d58b2-c978-4317-96bd-35949ce76121` and status list index `106493`. To check the status you have a few options: + +1. Run the [verification algorithm](https://w3c.github.io/vc-status-list-2021/#validate-algorithm) yourself using the specification. +2. Use the [utility in the SSI SDK](https://github.com/TBD54566975/ssi-sdk/blob/d5c302a1d9b9d04c1636a0c8dfda015f61bb0f6b/credential/status/statuslist2021.go#L254) to check the status. +3. Use the SSI Service's endpoint for status validation. + +### 3. Verify a credential's status + +Let's go with option 3 since it's simplest. The service has an endpoint which you can make `GET` requests to at `/v1/credentials/{id}/status` to check the status for any credential. + +Making a request for our credential's id, `8f9d58b2-c978-4317-96bd-35949ce76121`, we make a request: + +```bash +curl localhost:3000/v1/credentials/8f9d58b2-c978-4317-96bd-35949ce76121/status +``` + +Upon success we see a response such as: + +```json +{ + "revoked": false, + "suspended": false +} +``` + +We can see that the credential is neither revoked nor suspended, as expected. + +### 4. Revoke a credential + +As the creator of the credential we're able to change the status of the credential we've created. To do so, we make a request to the Update Credential Status endpoint which is a `PUT` request to `/v1/credentials/{id}/status`. At present, the endpoint accepts boolean values for the status(es) the credential supports. Let's update the credential's status to revoked: + +```bash +curl -X PUT localhost:3000/v1/credentials/8f9d58b2-c978-4317-96bd-35949ce76121/status -d '{ "revoked": true }' +``` + +Upon success we see a response such as: + +```json +{ + "revoked": true, + "suspended": false +} +``` + +Making a request as we did in step 3 should now show the same response. The credential is now revoked. + +**Note:** It is possible to reverse the status of a credential. To do so, make the same request mentioned above, but setting the value of `revoked` to `false`. diff --git a/doc/howto/verification.md b/doc/howto/verification.md index e69de29bb..92e98f75d 100644 --- a/doc/howto/verification.md +++ b/doc/howto/verification.md @@ -0,0 +1,62 @@ +# How To: Verify a Credential + +## Background + +Determining the validity of a [Verifiable Credential](https://www.w3.org/TR/vc-data-model/) can be a complex topic. The data model itself [has guidance on validity checks](https://www.w3.org/TR/vc-data-model/#validity-checks), and a [separate section on validation](https://www.w3.org/TR/vc-data-model/#validation), which provide useful information for implementers to consider. + +There are many factors that can go into determining whether a credential is valid or not; ultimately it is up to each verifier to define their own criteria for what constitutes a valid credential from a broader set of possibilities which may include: + +- Signature verification (i.e. is the credential signed by the issuer and has the signature been tampered with?) +- Credential status (i.e. not revoked or suspended) +- Credential validity (i.e. the credential is not expired) +- Credential issuer (i.e. is the issuer trusted for the claims they're attesting to?) +- Credential evidence (i.e. does the credential contain evidence that supports the claims being made?) +- Credential schema (i.e. does the credential conform to a schema defined in the `credentialSchema` property?) +- And more... + +## Verification or Validation? + +Both terms get thrown around and it can be confusing to determine what each means! Usually, when talking about digital signatures we talk about _signing_ and _verifying_ so _verification_ is a necessary part it making sure a given digitial signature is valid. Validation can be a more thorough process, containing any of the aforementioned validation steps, of which verification is a crucial step. Verification can be used in another sense too, since the name of the technology is "Verifiable Credential." Can a credential be verified without it being valid? Maybe, if by verified you mean the signature checks out. Can it be valid without being verified? Probably not. + +For the sake of simplicity let's say that a verifiable credential undergoes a _verification process_, within which, there are a number of validity checks. After passing all validity checks the credential is both valid and verified ✅. + +## Verifying a Credential + +As a part of the service's credential API we expose an endpoint `/v1/credentials/verification` that can be used as a stateless utility to verify any credential. At present, the endpoint performs the following verification process: + +* Make sure the credential is complaint with the VC Data Model +* Make sure the credential is not expired +* Make sure the signature of the credential is valid (currently supports both JWT and some Linked Data credentials) +* If the credential has a schema, makes sure its data complies with the schema (note: the schema must be hosted within the service) + +In the future this endpoint can (and should!) be expanded to support status checks and external schema resolution, among other optional checks. + +Building upon the credential we created in the [How To: Create a Credential](credential.md) guide, we'll take the credential we created, which is a JWT, and verify it. + +We make a `PUT` request to the endpoint `/v1/credentials/verification` as follows: + +``` +curl -X PUT localhost:3000/v1/credentials/verification -d '{ + "credentialJwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEI3o2TWttMVRtUldSUEs2bjIxUW5jVVpuazF0ZFlramU4OTZtWUN6aE1mUTY3YXNzRCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTA1NzM1MTUsImlzcyI6ImRpZDprZXk6ejZNa20xVG1SV1JQSzZuMjFRbmNVWm5rMXRkWWtqZTg5Nm1ZQ3poTWZRNjdhc3NEIiwianRpIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL3YxL2NyZWRlbnRpYWxzLzQ2YmMzZDI1LTZhYWYtNGY1MC05OWVkLTYxYzRiMzVmNjQxMSIsIm5iZiI6MTY5MDU3MzUxNSwibm9uY2UiOiIzMGMwNDYxZi1jMWUxLTQwNDctYWUwYS01NjgzMjdkMzY4YTYiLCJzdWIiOiJkaWQ6a2V5Ono2TWttTm52bmZ6VzNuTGllUHdlTjNuaUdMbnZwMkJqS3gzTk0xODZ2SjJ5UmcyeiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiU2F0b3NoaSIsImxhc3ROYW1lIjoiTmFrYW1vdG8ifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImFlZDZmNGYwLTVlZDctNGQ3YS1hM2RmLTU2NDMwZTFiMmE4OCIsInR5cGUiOiJKc29uU2NoZW1hMjAyMyJ9fX0.xwqpDuO6PDeEqYr6DflbeR6mhuwvVg0uR43i-7Zhy2DdaH1e3Jt4DuiMy09tZQ2jAXki0rjMNgLt7dPpzOl8BA" +}' +``` + +Upon success we see a response such as: + +```json +{ + "verified": true +} +``` + +## Other Types of Verification + +### Verifiable Presentations + +The example we've gone through above verifies a credential from an _issuer_. But what about verifying the _presentation_ of a credential, or set of credentials, from a _holder_ to a _verifier_? To do this, a holder must construct what's called a [Verifiable Presentation](https://www.w3.org/TR/vc-data-model/#presentations-0), an object which is also defined by the VC Data Model, which allows a _holder_ of a verifiable credential to create an authenticated wrapper around a set of credentials it wishes to present to a _verifier_. The service's verification API does [not yet](https://github.com/TBD54566975/ssi-service/issues/615) support this functionality. + +### Presentation Exchange + +What about applying more complex logic to the verification process? Like checking if a credential was issued from a known set of issuers? Or requesting two of one type of credential and three of another? Or checking that certain credential fields are present and have expected values? With [Presentation Exchange](https://identity.foundation/presentation-exchange/), a specification created in the [Decentralized Identity Foundation](https://identity.foundation/) this arbitarily-complex style of verification is made possible. + +The SSI Service supports Presentation Exchange. Its usage will be covered in a separate how to guide. From ab19193a10233c030574504c125562646a5a08f1 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Tue, 1 Aug 2023 11:43:16 -0400 Subject: [PATCH 11/20] How-to on creating a did web (#620) * How-to on creating a did web * typo * Apply suggestions from code review Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> --------- Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> --- doc/README.md | 13 ++-- doc/howto/didweb.md | 163 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 doc/howto/didweb.md diff --git a/doc/README.md b/doc/README.md index 834ca3c91..17aca8ff5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -32,7 +32,7 @@ which DID methods to enable, and which port to listen on. Read the docs below fo ## API Documentation -API documentation is generated using [Swagger](https://swagger.io/). The most recent API docs file [can be found here](doc/swagger.yaml), which can be pasted into the [Swagger Editor](https://editor.swagger.io/) for interaction. +API documentation is generated using [Swagger](https://swagger.io/). The most recent API docs file [can be found here](./swagger.yaml), which can be pasted into the [Swagger Editor](https://editor.swagger.io/) for interaction. When running the service you can find API documentation at: `http://localhost:8080/swagger/index.html` @@ -45,13 +45,14 @@ the SSI Service to get up to speed with its functionality. | Resource | Description | |----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| [Creating a DID](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/credential.md) | Get started with DID functionality | -| [Creating a Schema](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/schema.md) | Get started with schema functionality | -| [Issuing a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/credential.md) | Get started with credential issuance functionality | -| [Verify a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/verification.md) | Get started with credential verification functionality | -| [Revoke/Suspend a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/status.md) | Get started with credential status functionality | +| [Creating a DID](./howto/did.md) | Get started with DID functionality | +| [Creating a Schema](./howto/schema.md) | Get started with schema functionality | +| [Issuing a Credential](./howto/credential.md) | Get started with credential issuance functionality | +| [Verify a Credential](./howto/verification.md) | Get started with credential verification functionality | +| [Revoke/Suspend a Credential](./howto/status.md) | Get started with credential status functionality | | [[TODO] Requesting and Verifying Credentials with Presentation Exchange](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Presentation Exchange functionality | | [[TODO] Accepting Applications for and Issuing Credentials using Credential Manifest](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Credential Manifest functionality | | [Link your DID with a Website](./howto/wellknown.md) | Get started with DID Well Known functionality | +| [Creating a DID Web Identifier](./howto/didweb.md) | Get started with did:web | diff --git a/doc/howto/didweb.md b/doc/howto/didweb.md new file mode 100644 index 000000000..be49628ac --- /dev/null +++ b/doc/howto/didweb.md @@ -0,0 +1,163 @@ +# How To: Create a did:web + +## Background + +The [did:web Method Specification)](https://w3c-ccg.github.io/did-method-web/) describes a DID method that uses an existing web domain to host and establish trust for a DID Document. + +It relies on the controller of an existing domain to host a custom file with the contents of the DID Document they want to expose. The SSI Service facilitates creation of a `did:web`, which you then must update on the domain you control. + +## Steps + +### Prerequisites + +* You control an existing domain (e.g. like https://www.tbd.website). +* You are able to host files in a path within that origin (e.g. you can host the file returned by https://www.tbd.website/.well-known/did.json). + +## 1. Create a `did:web` DID + +Make a `PUT` request to `/v1/dids/web`, with a request body as follows: + +```json +{ + "keyType": "Ed25519", + "options": { + "didWebId": "did:web:tbd.website" + } +} +``` + +Or if you like CURLing: + +```shell +curl -X PUT 'localhost:3000/v1/dids/web' -d '{ + "keyType": "Ed25519", + "options": { + "didWebId": "did:web:tbd.website" + } +}' +``` + +Upon success, the contents of the response should look as follows... + +```json +{ + "did": { + "@context": "https://www.w3.org/ns/did/v1", + "id": "did:web:tbd.website", + "verificationMethod": [ + { + "id": "did:web:tbd.website", + "type": "JsonWebKey2020", + "controller": "did:web:tbd.website#owner", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "TuAM4Ro4q5_cFMarCHmOm-1c7NaxBxvoEe7-x7K7xhw", + "alg": "EdDSA", + "kid": "did:web:tbd.website#owner" + } + } + ], + "authentication": [ + [ + "did:web:tbd.website#owner" + ] + ], + "assertionMethod": [ + [ + "did:web:tbd.website#owner" + ] + ] + } +} +``` + +This response is an object containing a `did` property, whose value is a DID Document. This value is what needs to be hosted on your domain. + +### 2. Host the created DID Document + +This next step is for you to do outside of the service. You have to ensure that the URL `/.well-known/did.json` resolves to the content of the value of the `did` property from the response. In our example, we would have to make sure that the URL `https://tbd.website/.well-known/did.json` returns the JSON object described below: + +```json +{ + "@context": "https://www.w3.org/ns/did/v1", + "id": "did:web:tbd.website", + "verificationMethod": [ + { + "id": "did:web:tbd.website", + "type": "JsonWebKey2020", + "controller": "did:web:tbd.website#owner", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "TuAM4Ro4q5_cFMarCHmOm-1c7NaxBxvoEe7-x7K7xhw", + "alg": "EdDSA", + "kid": "did:web:tbd.website#owner" + } + } + ], + "authentication": [ + [ + "did:web:tbd.website#owner" + ] + ], + "assertionMethod": [ + [ + "did:web:tbd.website#owner" + ] + ] +} +``` + +### 3. Verify the `did:web` hosted + +This last step ensures that SSI Service considers the created `did:web` to be valid. + +Make a `GET` request to `v1/dids/resolver/` + +Using CURL: + +```shell +curl 'localhost:3000/v1/dids/resolver/did:web:tbd.website' +``` + +Upon success you will see a response such as... + +```json +{ + "didResolutionMetadata": { + "ContentType": "application/json" + }, + "didDocument": { + "@context": "https://www.w3.org/ns/did/v1", + "id": "did:web:tbd.website", + "verificationMethod": [ + { + "id": "did:web:tbd.website", + "type": "JsonWebKey2020", + "controller": "did:web:tbd.website#owner", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "TuAM4Ro4q5_cFMarCHmOm-1c7NaxBxvoEe7-x7K7xhw", + "alg": "EdDSA", + "kid": "did:web:tbd.website#owner" + } + } + ], + "authentication": [ + [ + "did:web:tbd.website#owner" + ] + ], + "assertionMethod": [ + [ + "did:web:tbd.website#owner" + ] + ] + }, + "didDocumentMetadata": {} +} +``` + +In this case the JSON object contains a [DID Resolution Result](https://www.w3.org/TR/did-core/#did-resolution), in which the `didDocument` property has the value that was created in step 1. \ No newline at end of file From 02d6f8f826ddecbffa238e65f6fce32b7ff53e31 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Wed, 2 Aug 2023 01:56:36 +1000 Subject: [PATCH 12/20] ability to set a default bearer token (#556) * ability to set a default bearer token * linting fixes * moving docs to service section --------- Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> --- doc/README.md | 3 + doc/service/authorization.md | 20 +++++ pkg/server/middleware/Authentication.go | 33 +++++--- pkg/server/middleware/Authentication_test.go | 81 ++++++++++++++++++++ pkg/server/server.go | 1 + 5 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 doc/service/authorization.md create mode 100644 pkg/server/middleware/Authentication_test.go diff --git a/doc/README.md b/doc/README.md index 17aca8ff5..3b3f2d4cc 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,6 +13,8 @@ resource for developers and users of the SSI Service. | [Versioning](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/versioning.md) | Describes versioning practices for the service | | [Webhooks](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/webhook.md) | Describes how to use webhooks in the service | | [Features](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/features.md) | Features currently supported by the service | +| [Authorization](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/authorization.md) | How to setup token authentication and extend for authorization | + ## Service Improvement Proposals (SIPs) @@ -29,6 +31,7 @@ which DID methods to enable, and which port to listen on. Read the docs below fo | [TOML Config Files](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/toml.md) | Describes how to use TOML config files | | [Using a Cloud Key Management Service](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/kms.md) | Describes how to configure a KMS | | [Storage](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/storage.md) | Describes alternatives for storage by the service | +| [Authentication](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/authorization.md) | Describes how to setup out of the box token authentication | ## API Documentation diff --git a/doc/service/authorization.md b/doc/service/authorization.md new file mode 100644 index 000000000..26b07939a --- /dev/null +++ b/doc/service/authorization.md @@ -0,0 +1,20 @@ +# Authentication + +Out of the box if you set the AUTH_TOKEN to a sha256 token value, then all api calls will require a bearer token that hashes to that. If AUTH_TOKEN is not set then no authentication is required. + +Generate a token by hashing the super secure token of `hunter2`: +```sh +export AUTH_TOKEN=$(echo -n "hunter2" | shasum -a 256) +``` + +Then use `hunter2` as a Bearer token: + +```sh +export TOKEN=hunter2 +curl -H "Authorization: Bearer $TOKEN" .... +``` + +# Extending Authentication and Authorization for production environments + +The ssi server uses the Gin framework from Golang, which allows various kinds of middleware. Look in `pkg/middleware/Authentication.go` and `pkg/middleware/Authorization.go` for details on how you can wire up authentication and authorization for your use case. One such option is the https://github.com/zalando/gin-oauth2 framework. + diff --git a/pkg/server/middleware/Authentication.go b/pkg/server/middleware/Authentication.go index 48ea70aed..c9015764c 100644 --- a/pkg/server/middleware/Authentication.go +++ b/pkg/server/middleware/Authentication.go @@ -1,7 +1,10 @@ package middleware import ( + "crypto/sha256" + "encoding/hex" "net/http" + "os" "github.com/gin-gonic/gin" ) @@ -17,25 +20,37 @@ func setUpEngine(cfg config.ServerConfig, shutdown chan os.Signal) *gin.Engine { gin.Logger(), middleware.Errors(shutdown), middleware.AuthMiddleware(), - middleware.AuthorizationMiddleware(), } - - */ func AuthMiddleware() gin.HandlerFunc { + authToken := os.Getenv("AUTH_TOKEN") + return func(c *gin.Context) { token := c.GetHeader("Authorization") - // This is a dummy check here. You should do your actual JWT token verification. - if token == "IF YOU SET IT TO THIS VALUE IT WILL FAIL" { + + // If AUTH_TOKEN is not set, skip the authentication + if authToken == "" { + c.Next() + return + } + + // Remove "Bearer " from the token + if len(token) > 7 && token[:7] == "Bearer " { + token = token[7:] + } + + // Generate SHA256 hash of the token from the header + hash := sha256.Sum256([]byte(token)) + hashedToken := hex.EncodeToString(hash[:]) + + // Check if the hashed token from the header matches the AUTH token + if hashedToken != authToken { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization is required"}) c.Abort() return } - // Assuming that the token is valid and we got the user info from the JWT token. - // You should replace it with actual user info. - user := "user" - c.Set("user", user) + c.Next() } } diff --git a/pkg/server/middleware/Authentication_test.go b/pkg/server/middleware/Authentication_test.go new file mode 100644 index 000000000..b32b86f95 --- /dev/null +++ b/pkg/server/middleware/Authentication_test.go @@ -0,0 +1,81 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestAuthMiddleware(t *testing.T) { + // Set the AUTH_TOKEN environment variable for testing + t.Setenv("AUTH_TOKEN", "f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7") // sha256 hash of "hunter2" + + // Create a new gin engine + r := gin.Default() + + // Add the AuthMiddleware to the gin engine + r.Use(AuthMiddleware()) + + // Add a test route + r.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + + // Create a request with the correct Authorization header + req, _ := http.NewRequest(http.MethodGet, "/test", nil) + req.Header.Add("Authorization", "Bearer hunter2") + + // Create a response recorder + w := httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(w, req) + + // Assert that the status code is 200 OK + assert.Equal(t, http.StatusOK, w.Code) + + // Create a request with an incorrect Authorization header + req, _ = http.NewRequest(http.MethodGet, "/test", nil) + req.Header.Add("Authorization", "Bearer nonsense") + + // Reset the response recorder + w = httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(w, req) + + // Assert that the status code is 401 Unauthorized + assert.Equal(t, http.StatusUnauthorized, w.Code) +} + +func TestNoAuthMiddleware(t *testing.T) { + + t.Setenv("AUTH_TOKEN", "") // no auth token so things just work + + // Create a new gin engine + r := gin.Default() + + // Add the AuthMiddleware to the gin engine + r.Use(AuthMiddleware()) + + // Add a test route + r.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "OK") + }) + + // Create a request with the correct Authorization header + req, _ := http.NewRequest(http.MethodGet, "/test", nil) + + // Create a response recorder + w := httptest.NewRecorder() + + // Serve the request + r.ServeHTTP(w, req) + + // Assert that the status code is 200 OK + assert.Equal(t, http.StatusOK, w.Code) + +} diff --git a/pkg/server/server.go b/pkg/server/server.go index adc0cc2d4..bff03db26 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -118,6 +118,7 @@ func setUpEngine(cfg config.ServerConfig, shutdown chan os.Signal) *gin.Engine { gin.Recovery(), gin.Logger(), middleware.Errors(shutdown), + middleware.AuthMiddleware(), } if cfg.JagerEnabled { middlewares = append(middlewares, otelgin.Middleware(config.ServiceName)) From 5440beeb22eca82c214232ca5ba51eda54a8fc78 Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Wed, 2 Aug 2023 10:21:09 -0700 Subject: [PATCH 13/20] config moved to serviceinfo singleton (#625) * config moved to serviceinfo singleton * fix integration * schemas path * merge * fix port * edit * add version to swagger * rename * rename to avoid git weirdness * remove ptr * remove access * comment on thread safety * all use the same service endpoint --- cmd/authserver/main.go | 2 +- cmd/ssiservice/main.go | 7 +- config/config.go | 236 +++--------------- config/config.toml.example | 21 -- config/dev.toml | 24 -- config/info.go | 70 ++++++ config/prod.toml | 21 -- config/test.toml | 22 -- doc/README.md | 19 +- doc/config/auth.md | 20 ++ doc/service/authorization.md | 20 -- doc/swagger.yaml | 1 + integration/common.go | 6 + {pkg => internal}/encryption/encryption.go | 5 +- .../encryption/encryption_test.go | 3 +- pkg/authorizationserver/oauth2.go | 4 +- pkg/authorizationserver/oauth2_test.go | 2 +- .../{Authentication.go => authn.go} | 0 .../{Authentication_test.go => authn_test.go} | 5 +- .../middleware/{Authorization.go => authz.go} | 0 pkg/server/router/credential_test.go | 30 +-- pkg/server/router/keystore_test.go | 7 +- pkg/server/router/presentation_test.go | 3 +- pkg/server/router/router_test.go | 3 +- pkg/server/router/schema_test.go | 10 +- pkg/server/router/testutils_test.go | 24 +- pkg/server/server.go | 61 +++-- pkg/server/server_issuance_test.go | 4 +- pkg/server/server_presentation_test.go | 3 +- pkg/server/server_test.go | 31 +-- pkg/service/credential/service.go | 6 +- pkg/service/credential/status.go | 4 +- pkg/service/did/batch.go | 87 +++++++ pkg/service/did/ion_test.go | 9 +- pkg/service/did/service.go | 77 +----- pkg/service/issuance/service.go | 5 +- pkg/service/keystore/service.go | 5 +- pkg/service/keystore/service_test.go | 10 +- pkg/service/keystore/storage.go | 3 +- pkg/service/manifest/service.go | 10 +- pkg/service/presentation/service.go | 9 +- pkg/service/schema/service.go | 10 +- pkg/service/service.go | 24 +- pkg/storage/db_test.go | 3 +- pkg/storage/encrypt.go | 3 +- 45 files changed, 367 insertions(+), 562 deletions(-) create mode 100644 config/info.go create mode 100644 doc/config/auth.md delete mode 100644 doc/service/authorization.md rename {pkg => internal}/encryption/encryption.go (98%) rename {pkg => internal}/encryption/encryption_test.go (99%) rename pkg/server/middleware/{Authentication.go => authn.go} (100%) rename pkg/server/middleware/{Authentication_test.go => authn_test.go} (96%) rename pkg/server/middleware/{Authorization.go => authz.go} (100%) create mode 100644 pkg/service/did/batch.go diff --git a/cmd/authserver/main.go b/cmd/authserver/main.go index 3c4a30d61..fbd6a94ce 100644 --- a/cmd/authserver/main.go +++ b/cmd/authserver/main.go @@ -130,7 +130,7 @@ func newTracerProvider(cfg authorizationserver.AuthConfig) (*sdktrace.TracerProv sdktrace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(config.ServiceName), - semconv.ServiceVersionKey.String(cfg.Version.SVN), + semconv.ServiceVersionKey.String(config.ServiceVersion), )), ) return tp, nil diff --git a/cmd/ssiservice/main.go b/cmd/ssiservice/main.go index 268635d6e..451612be3 100644 --- a/cmd/ssiservice/main.go +++ b/cmd/ssiservice/main.go @@ -31,6 +31,7 @@ import ( // // @title SSI Service API // @description The Self Sovereign Identity Service: Managing DIDs, Verifiable Credentials, and more! +// @version 0.0.3 // @contact.name TBD // @contact.url https://github.com/TBD54566975/ssi-service/issues // @contact.email tbd-developer@squareup.com @@ -88,9 +89,9 @@ func run() error { } } - expvar.NewString("build").Set(cfg.Version.SVN) + expvar.NewString("build").Set(config.ServiceVersion) - logrus.Infof("main: Started : Service initializing : env [%s] : version %q", cfg.Server.Environment, cfg.Version.SVN) + logrus.Infof("main: Started : Service initializing : env [%s] : version %q", cfg.Server.Environment, config.ServiceVersion) defer logrus.Info("main: Completed") out, err := conf.String(cfg) @@ -171,7 +172,7 @@ func newTracerProvider(cfg *config.SSIServiceConfig) (*sdktrace.TracerProvider, sdktrace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(config.ServiceName), - semconv.ServiceVersionKey.String(cfg.Version.SVN), + semconv.ServiceVersionKey.String(config.ServiceVersion), )), ) otel.SetTracerProvider(tp) diff --git a/config/config.go b/config/config.go index 370256be4..337684a01 100644 --- a/config/config.go +++ b/config/config.go @@ -21,11 +21,8 @@ const ( DefaultConfigPath = "config/dev.toml" DefaultEnvPath = "config/.env" Filename = "dev.toml" - ServiceName = "ssi-service" Extension = ".toml" - DefaultServiceEndpoint = "http://localhost:8080" - EnvironmentDev Environment = "dev" EnvironmentTest Environment = "test" EnvironmentProd Environment = "prod" @@ -44,7 +41,6 @@ func (e EnvironmentVariable) String() string { } type SSIServiceConfig struct { - conf.Version Server ServerConfig `toml:"server"` Services ServicesConfig `toml:"services"` } @@ -53,7 +49,7 @@ type SSIServiceConfig struct { type ServerConfig struct { Environment Environment `toml:"env" conf:"default:dev"` APIHost string `toml:"api_host" conf:"default:0.0.0.0:3000"` - JagerHost string `toml:"jager_host" conf:"http://jaeger:14268/api/traces"` + JagerHost string `toml:"jager_host" conf:"default:http://jaeger:14268/api/traces"` JagerEnabled bool `toml:"jager_enabled" conf:"default:false"` ReadTimeout time.Duration `toml:"read_timeout" conf:"default:5s"` WriteTimeout time.Duration `toml:"write_timeout" conf:"default:5s"` @@ -69,42 +65,27 @@ type ServicesConfig struct { // at present, it is assumed that a single storage provider works for all services // in the future it may make sense to have per-service storage providers (e.g. mysql for one service, // mongo for another) - StorageProvider string `toml:"storage"` + StorageProvider string `toml:"storage" conf:"default:bolt"` StorageOptions []storage.Option `toml:"storage_option"` - ServiceEndpoint string `toml:"service_endpoint"` + ServiceEndpoint string `toml:"service_endpoint" conf:"default:http://localhost:8080"` // Application level encryption configuration. Defines how values are encrypted before they are stored in the // configured KV store. AppLevelEncryptionConfiguration EncryptionConfig `toml:"storage_encryption,omitempty"` // Embed all service-specific configs here. The order matters: from which should be instantiated first, to last - KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"` - DIDConfig DIDServiceConfig `toml:"did,omitempty"` - SchemaConfig SchemaServiceConfig `toml:"schema,omitempty"` - CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"` - OperationConfig OperationServiceConfig `toml:"operation,omitempty"` - PresentationConfig PresentationServiceConfig `toml:"presentation,omitempty"` - ManifestConfig ManifestServiceConfig `toml:"manifest,omitempty"` - IssuanceServiceConfig IssuanceServiceConfig `toml:"issuance,omitempty"` - WebhookConfig WebhookServiceConfig `toml:"webhook,omitempty"` -} - -// BaseServiceConfig represents configurable properties for a specific component of the SSI Service -// Can be wrapped and extended for any specific service config -type BaseServiceConfig struct { - Name string `toml:"name"` - ServiceEndpoint string `toml:"service_endpoint"` + KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"` + DIDConfig DIDServiceConfig `toml:"did,omitempty"` + CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"` + WebhookConfig WebhookServiceConfig `toml:"webhook,omitempty"` } type KeyStoreServiceConfig struct { - *BaseServiceConfig - - // Configuration describing the encryption of the private keys that are under ssi-service's custody. EncryptionConfig } type EncryptionConfig struct { - DisableEncryption bool `toml:"disable_encryption"` + DisableEncryption bool `toml:"disable_encryption" conf:"default:false"` // The URI for a master key. We use tink for envelope encryption as described in https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-with-tink // When left empty and DisableEncryption is off, then a random key is generated and used. This random key is persisted unencrypted in the @@ -131,7 +112,8 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool { if k == nil { return true } - return reflect.DeepEqual(k, &KeyStoreServiceConfig{}) + // this returns false since reflection will fail on the EncryptionConfig struct + return false } func (k *KeyStoreServiceConfig) GetMasterKeyURI() string { @@ -147,9 +129,8 @@ func (k *KeyStoreServiceConfig) EncryptionEnabled() bool { } type DIDServiceConfig struct { - *BaseServiceConfig - Methods []string `toml:"methods"` - LocalResolutionMethods []string `toml:"local_resolution_methods"` + Methods []string `toml:"methods" conf:"default:key;web"` + LocalResolutionMethods []string `toml:"local_resolution_methods" conf:"default:key;peer;web;jwk;pkh"` UniversalResolverURL string `toml:"universal_resolver_url"` UniversalResolverMethods []string `toml:"universal_resolver_methods"` IONResolverURL string `toml:"ion_resolver_url"` @@ -164,19 +145,7 @@ func (d *DIDServiceConfig) IsEmpty() bool { return reflect.DeepEqual(d, &DIDServiceConfig{}) } -type SchemaServiceConfig struct { - *BaseServiceConfig -} - -func (s *SchemaServiceConfig) IsEmpty() bool { - if s == nil { - return true - } - return reflect.DeepEqual(s, &SchemaServiceConfig{}) -} - type CredentialServiceConfig struct { - *BaseServiceConfig // BatchCreateMaxItems set's the maximum amount that can be. BatchCreateMaxItems int `toml:"batch_create_max_items" conf:"default:100"` @@ -190,53 +159,8 @@ func (c *CredentialServiceConfig) IsEmpty() bool { return reflect.DeepEqual(c, &CredentialServiceConfig{}) } -type OperationServiceConfig struct { - *BaseServiceConfig -} - -func (o *OperationServiceConfig) IsEmpty() bool { - if o == nil { - return true - } - return reflect.DeepEqual(o, &OperationServiceConfig{}) -} - -type PresentationServiceConfig struct { - *BaseServiceConfig -} - -func (p *PresentationServiceConfig) IsEmpty() bool { - if p == nil { - return true - } - return reflect.DeepEqual(p, &PresentationServiceConfig{}) -} - -type ManifestServiceConfig struct { - *BaseServiceConfig -} - -func (m *ManifestServiceConfig) IsEmpty() bool { - if m == nil { - return true - } - return reflect.DeepEqual(m, &ManifestServiceConfig{}) -} - -type IssuanceServiceConfig struct { - *BaseServiceConfig -} - -func (s *IssuanceServiceConfig) IsEmpty() bool { - if s == nil { - return true - } - return reflect.DeepEqual(s, &IssuanceServiceConfig{}) -} - type WebhookServiceConfig struct { - *BaseServiceConfig - WebhookTimeout string `toml:"webhook_timeout"` + WebhookTimeout string `toml:"webhook_timeout" conf:"default:10s"` } func (p *WebhookServiceConfig) IsEmpty() bool { @@ -252,33 +176,31 @@ func LoadConfig(path string, fs fs.FS) (*SSIServiceConfig, error) { if fs == nil { fs = os.DirFS(".") } - loadDefaultConfig, err := checkValidConfigPath(path) + useDefaultConfig, err := checkValidConfigPath(path) if err != nil { return nil, errors.Wrap(err, "validate config path") } // create the config object - var config SSIServiceConfig - if err = parseAndApplyDefaults(config); err != nil { + config := new(SSIServiceConfig) + if err = parseConfig(config); err != nil { return nil, errors.Wrap(err, "parse and apply defaults") } - if loadDefaultConfig { - defaultServicesConfig := getDefaultServicesConfig() - config.Services = defaultServicesConfig - } else if err = loadTOMLConfig(path, &config, fs); err != nil { - return nil, errors.Wrap(err, "load toml config") + if !useDefaultConfig { + if err = loadTOMLConfig(path, config, fs); err != nil { + return nil, errors.Wrap(err, "load toml config") + } } - if err = applyEnvVariables(&config); err != nil { + if err = applyEnvVariables(config); err != nil { return nil, errors.Wrap(err, "apply env variables") } - if err = validateConfig(&config); err != nil { + if err = validateConfig(config); err != nil { return nil, errors.Wrap(err, "validating config values") } - - return &config, nil + return config, nil } func validateConfig(s *SSIServiceConfig) error { @@ -287,7 +209,7 @@ func validateConfig(s *SSIServiceConfig) error { return errors.New("prod environment cannot disable key encryption") } if s.Services.AppLevelEncryptionConfiguration.DisableEncryption { - logrus.Warn("prod environment detected without app level encryption. This is strongly discouraged.") + logrus.Warn("Prod environment detected without app level encryption. This is strongly discouraged.") } } return nil @@ -305,138 +227,42 @@ func checkValidConfigPath(path string) (bool, error) { return defaultConfig, nil } -func parseAndApplyDefaults(config SSIServiceConfig) error { +func parseConfig(cfg *SSIServiceConfig) error { // parse and apply defaults - err := conf.Parse(os.Args[1:], ServiceName, &config) + err := conf.Parse(os.Args[1:], ServiceName, cfg) if err == nil { return nil } switch { case errors.Is(err, conf.ErrHelpWanted): - usage, err := conf.Usage(ServiceName, &config) + usage, err := conf.Usage(ServiceName, &cfg) if err != nil { return errors.Wrap(err, "parsing config") } - logrus.Println(usage) + logrus.Info(usage) return nil case errors.Is(err, conf.ErrVersionWanted): - version, err := conf.VersionString(ServiceName, &config) + version, err := conf.VersionString(ServiceName, &cfg) if err != nil { return errors.Wrap(err, "generating config version") } - logrus.Println(version) + logrus.Info(version) return nil } return errors.Wrap(err, "parsing config") } -// TODO(gabe) remove this from config in https://github.com/TBD54566975/ssi-service/issues/502 -func getDefaultServicesConfig() ServicesConfig { - return ServicesConfig{ - StorageProvider: "bolt", - ServiceEndpoint: DefaultServiceEndpoint, - KeyStoreConfig: KeyStoreServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "keystore", ServiceEndpoint: DefaultServiceEndpoint + "/v1/keys"}, - }, - DIDConfig: DIDServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "did", ServiceEndpoint: DefaultServiceEndpoint + "/v1/dids"}, - Methods: []string{"key", "web"}, - LocalResolutionMethods: []string{"key", "peer", "web", "jwk", "pkh"}, - }, - SchemaConfig: SchemaServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "schema", ServiceEndpoint: DefaultServiceEndpoint + "/v1/schemas"}, - }, - CredentialConfig: CredentialServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "credential", ServiceEndpoint: DefaultServiceEndpoint + "/v1/credentials"}, - }, - OperationConfig: OperationServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "operation", ServiceEndpoint: DefaultServiceEndpoint + "/v1/operations"}, - }, - PresentationConfig: PresentationServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "presentation", ServiceEndpoint: DefaultServiceEndpoint + "/v1/presentations"}, - }, - ManifestConfig: ManifestServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "manifest", ServiceEndpoint: DefaultServiceEndpoint + "/v1/manifests"}, - }, - IssuanceServiceConfig: IssuanceServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "issuance", ServiceEndpoint: DefaultServiceEndpoint + "/v1/issuancetemplates"}, - }, - WebhookConfig: WebhookServiceConfig{ - BaseServiceConfig: &BaseServiceConfig{Name: "webhook", ServiceEndpoint: DefaultServiceEndpoint + "/v1/webhooks"}, - WebhookTimeout: "10s", - }, - } -} - func loadTOMLConfig(path string, config *SSIServiceConfig, fs fs.FS) error { // load from TOML file file, err := fs.Open(path) if err != nil { return errors.Wrapf(err, "opening path %s", path) } - if _, err := toml.NewDecoder(file).Decode(&config); err != nil { + if _, err = toml.NewDecoder(file).Decode(&config); err != nil { return errors.Wrapf(err, "could not load config: %s", path) } - - // apply defaults - services := config.Services - endpoint := services.ServiceEndpoint + "/v1" - if services.KeyStoreConfig.IsEmpty() { - services.KeyStoreConfig = KeyStoreServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.KeyStoreConfig.ServiceEndpoint = endpoint + "/keys" - if services.DIDConfig.IsEmpty() { - services.DIDConfig = DIDServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.DIDConfig.ServiceEndpoint = endpoint + "/dids" - if services.SchemaConfig.IsEmpty() { - services.SchemaConfig = SchemaServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.SchemaConfig.ServiceEndpoint = endpoint + "/schemas" - if services.CredentialConfig.IsEmpty() { - services.CredentialConfig = CredentialServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.CredentialConfig.ServiceEndpoint = endpoint + "/credentials" - if services.OperationConfig.IsEmpty() { - services.OperationConfig = OperationServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.OperationConfig.ServiceEndpoint = endpoint + "/operations" - if services.PresentationConfig.IsEmpty() { - services.PresentationConfig = PresentationServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.PresentationConfig.ServiceEndpoint = endpoint + "/presentations" - if services.ManifestConfig.IsEmpty() { - services.ManifestConfig = ManifestServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.ManifestConfig.ServiceEndpoint = endpoint + "/manifests" - if services.IssuanceServiceConfig.IsEmpty() { - services.IssuanceServiceConfig = IssuanceServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.IssuanceServiceConfig.ServiceEndpoint = endpoint + "/issuancetemplates" - if services.WebhookConfig.IsEmpty() { - services.WebhookConfig = WebhookServiceConfig{ - BaseServiceConfig: new(BaseServiceConfig), - } - } - services.WebhookConfig.ServiceEndpoint = endpoint + "/webhooks" return nil } diff --git a/config/config.toml.example b/config/config.toml.example index 835aa8460..604708a83 100644 --- a/config/config.toml.example +++ b/config/config.toml.example @@ -1,8 +1,3 @@ -title = "SSI Service Config" - -svn = "0.0.1" -desc = "Default configuration to be used while running the service as a single go process." - # http service configuration [server] env = "dev" # either 'dev' or 'prod' @@ -45,33 +40,17 @@ service_endpoint = "http://localhost:8080" # per-service configuration [services.keystore] -name = "keystore" password = "default-password" # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" # kms_credentials_path = "credentials.json" [services.did] -name = "did" methods = ["key", "web"] local_resolution_methods = ["key", "web", "pkh", "peer"] batch_create_max_items = 100 -[services.schema] -name = "schema" - [services.credential] -name = "credential" batch_create_max_items = 100 -[services.issuance] -name = "issuance" - -[services.manifest] -name = "manifest" - -[services.presentation] -name = "presentation" - [services.webhook] -name = "webhook" webhook_timeout = "10s" diff --git a/config/dev.toml b/config/dev.toml index 70e87d59b..a3841c907 100644 --- a/config/dev.toml +++ b/config/dev.toml @@ -1,8 +1,3 @@ -title = "SSI Service Config" - -svn = "0.0.1" -desc = "Default configuration to be used while running the service for testing as a single go process." - # http service configuration [server] env = "dev" # either 'dev', 'test', or 'prod' @@ -37,34 +32,15 @@ disable_encryption = true id = "boltdb-filepath-option" option = "bolt.db" -# per-service configuration -[services.keystore] -name = "keystore" - [services.did] -name = "did" methods = ["key", "web"] local_resolution_methods = ["key", "web", "pkh", "peer"] universal_resolver_url = "https://dev.uniresolver.io/" universal_resolver_methods = ["ion"] batch_create_max_items = 100 -[services.schema] -name = "schema" - [services.credential] -name = "credential" batch_create_max_items = 100 -[services.issuance] -name = "issuance" - -[services.manifest] -name = "manifest" - -[services.presentation] -name = "presentation" - [services.webhook] -name = "webhook" webhook_timeout = "10s" diff --git a/config/info.go b/config/info.go new file mode 100644 index 000000000..264ddd93e --- /dev/null +++ b/config/info.go @@ -0,0 +1,70 @@ +package config + +import ( + "strings" + + "github.com/tbd54566975/ssi-service/pkg/service/framework" +) + +const ( + ServiceName = "ssi-service" + ServiceVersion = "0.0.3" + APIVersion = "v1" +) + +var ( + si = &serviceInfo{ + name: ServiceName, + description: "The Self Sovereign Identity Service is a RESTful web service that facilitates all things relating" + + " to DIDs, VCs, and related standards-based interactions.", + version: ServiceVersion, + apiVersion: APIVersion, + servicePaths: make(map[framework.Type]string), + } +) + +// serviceInfo is intended to be a singleton object for static service info. +// WARNING: it is **NOT** currently thread safe. +type serviceInfo struct { + name string + description string + version string + apiBase string + apiVersion string + servicePaths map[framework.Type]string +} + +func Name() string { + return si.name +} + +func Description() string { + return si.description +} + +func (si *serviceInfo) Version() string { + return si.version +} + +func SetAPIBase(url string) { + if strings.LastIndexAny(url, "/") == len(url)-1 { + url = url[:len(url)-1] + } + si.apiBase = url +} + +func GetAPIBase() string { + return si.apiBase +} + +func SetServicePath(service framework.Type, path string) { + // normalize path + if strings.IndexAny(path, "/") == 0 { + path = path[1:] + } + si.servicePaths[service] = strings.Join([]string{si.apiBase, APIVersion, path}, "/") +} + +func GetServicePath(service framework.Type) string { + return si.servicePaths[service] +} diff --git a/config/prod.toml b/config/prod.toml index dc11c4413..b93ec37d6 100644 --- a/config/prod.toml +++ b/config/prod.toml @@ -1,8 +1,3 @@ -title = "SSI Service Config" - -svn = "0.0.1" -desc = "Configuration to be used while running the service in a production environment." - # http service configuration [server] env = "prod" # either 'dev', 'test', or 'prod' @@ -42,13 +37,11 @@ option = "password" # per-service configuration [services.keystore] -name = "keystore" disable_encryption = false # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" # kms_credentials_path = "credentials.json" [services.did] -name = "did" methods = ["key", "web", "ion"] local_resolution_methods = ["key", "web", "pkh", "peer"] universal_resolver_url = "http://uni-resolver-web:8080" @@ -56,22 +49,8 @@ universal_resolver_methods = ["ion"] ion_resolver_url = "https://ion.tbddev.org" batch_create_max_items = 100 -[services.schema] -name = "schema" - [services.credential] -name = "credential" batch_create_max_items = 100 -[services.issuance] -name = "issuance" - -[services.manifest] -name = "manifest" - -[services.presentation] -name = "presentation" - [services.webhook] -name = "webhook" webhook_timeout = "10s" diff --git a/config/test.toml b/config/test.toml index 02399cc05..919891ea4 100644 --- a/config/test.toml +++ b/config/test.toml @@ -1,8 +1,3 @@ -title = "SSI Service Config" - -svn = "0.0.1" -desc = "Configuration to be used while running the service for testing with docker compose." - # http service configuration [server] env = "test" # either 'dev', 'test' or 'prod' @@ -43,15 +38,12 @@ option = "redis:6379" id = "storage-password-option" option = "password" -# per-service configuration [services.keystore] -name = "keystore" # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" # kms_credentials_path = "credentials.json" disable_encryption = false [services.did] -name = "did" methods = ["key", "web", "ion"] local_resolution_methods = ["key", "web", "pkh", "peer"] universal_resolver_url = "http://uni-resolver-web:8080" @@ -59,22 +51,8 @@ universal_resolver_methods = ["ion"] ion_resolver_url = "https://ion.tbddev.org" batch_create_max_items = 100 -[services.schema] -name = "schema" - [services.credential] -name = "credential" batch_create_max_items = 100 -[services.issuance] -name = "issuance" - -[services.manifest] -name = "manifest" - -[services.presentation] -name = "presentation" - [services.webhook] -name = "webhook" webhook_timeout = "10s" diff --git a/doc/README.md b/doc/README.md index 3b3f2d4cc..53c4076fc 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,7 +13,6 @@ resource for developers and users of the SSI Service. | [Versioning](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/versioning.md) | Describes versioning practices for the service | | [Webhooks](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/webhook.md) | Describes how to use webhooks in the service | | [Features](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/features.md) | Features currently supported by the service | -| [Authorization](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/authorization.md) | How to setup token authentication and extend for authorization | ## Service Improvement Proposals (SIPs) @@ -30,8 +29,8 @@ which DID methods to enable, and which port to listen on. Read the docs below fo |------------------------------------------------------------------------------------------------------------|----------------------------------------| | [TOML Config Files](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/toml.md) | Describes how to use TOML config files | | [Using a Cloud Key Management Service](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/kms.md) | Describes how to configure a KMS | -| [Storage](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/storage.md) | Describes alternatives for storage by the service | -| [Authentication](https://github.com/TBD54566975/ssi-service/blob/main/doc/service/authorization.md) | Describes how to setup out of the box token authentication | +| [Storage](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/storage.md) | Describes alternatives for storage by the service | +| [Authorization](https://github.com/TBD54566975/ssi-service/blob/main/doc/config/auth.md) | How to setup token authentication and extend for authorization | ## API Documentation @@ -44,18 +43,18 @@ When running the service you can find API documentation at: `http://localhost:80 ## How To's How to documentation is focused on explaining usage of the SSI Service. It is intended to be a resource for users of -the SSI Service to get up to speed with its functionality. +the service to get up to speed with its functionality. | Resource | Description | |----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| [Creating a DID](./howto/did.md) | Get started with DID functionality | -| [Creating a Schema](./howto/schema.md) | Get started with schema functionality | -| [Issuing a Credential](./howto/credential.md) | Get started with credential issuance functionality | -| [Verify a Credential](./howto/verification.md) | Get started with credential verification functionality | -| [Revoke/Suspend a Credential](./howto/status.md) | Get started with credential status functionality | +| [Creating a DID](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/credential.md) | Get started with DID functionality | +| [Creating a Schema](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/schema.md) | Get started with schema functionality | +| [Issuing a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/credential.md) | Get started with credential issuance functionality | +| [Verify a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/verification.md) | Get started with credential verification functionality | +| [Revoke/Suspend a Credential](https://github.com/TBD54566975/ssi-service/blob/main/doc/howto/status.md) | Get started with credential status functionality | | [[TODO] Requesting and Verifying Credentials with Presentation Exchange](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Presentation Exchange functionality | | [[TODO] Accepting Applications for and Issuing Credentials using Credential Manifest](https://github.com/TBD54566975/ssi-service/issues/606) | Get started with Credential Manifest functionality | | [Link your DID with a Website](./howto/wellknown.md) | Get started with DID Well Known functionality | -| [Creating a DID Web Identifier](./howto/didweb.md) | Get started with did:web | +| [Creating a DID Web Identifier](./howto/didweb.md) | Get started with did:web | diff --git a/doc/config/auth.md b/doc/config/auth.md new file mode 100644 index 000000000..9f26aa836 --- /dev/null +++ b/doc/config/auth.md @@ -0,0 +1,20 @@ +# Authentication + +Out of the box if you set the `AUTH_TOKEN` to a sha256 token value, then all API calls will require a bearer token that hashes to that. If `AUTH_TOKEN` is not set then no authentication is required. + +Generate a token by hashing the super secure token of `hunter2`: +```sh +export AUTH_TOKEN=$(echo -n "hunter2" | shasum -a 256) +``` + +Then use `hunter2` as a Bearer token: + +```sh +export TOKEN=hunter2 +curl -H "Authorization: Bearer $TOKEN" .... +``` + +# Extending Authentication and Authorization for production environments + +The server uses the []Gin framework](https://github.com/gin-gonic/gin), which allows various kinds of middleware. Look in `pkg/middleware/Authentication.go` and `pkg/middleware/Authorization.go` for details on how you can wire up authentication and authorization for your use case. One such option is the https://github.com/zalando/gin-oauth2 framework. + diff --git a/doc/service/authorization.md b/doc/service/authorization.md deleted file mode 100644 index 26b07939a..000000000 --- a/doc/service/authorization.md +++ /dev/null @@ -1,20 +0,0 @@ -# Authentication - -Out of the box if you set the AUTH_TOKEN to a sha256 token value, then all api calls will require a bearer token that hashes to that. If AUTH_TOKEN is not set then no authentication is required. - -Generate a token by hashing the super secure token of `hunter2`: -```sh -export AUTH_TOKEN=$(echo -n "hunter2" | shasum -a 256) -``` - -Then use `hunter2` as a Bearer token: - -```sh -export TOKEN=hunter2 -curl -H "Authorization: Bearer $TOKEN" .... -``` - -# Extending Authentication and Authorization for production environments - -The ssi server uses the Gin framework from Golang, which allows various kinds of middleware. Look in `pkg/middleware/Authentication.go` and `pkg/middleware/Authorization.go` for details on how you can wire up authentication and authorization for your use case. One such option is the https://github.com/zalando/gin-oauth2 framework. - diff --git a/doc/swagger.yaml b/doc/swagger.yaml index a93a630d0..f54ff4b02 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -2232,6 +2232,7 @@ info: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html title: SSI Service API + version: 0.0.3 paths: /health: get: diff --git a/integration/common.go b/integration/common.go index aac65e371..09628ce52 100644 --- a/integration/common.go +++ b/integration/common.go @@ -20,10 +20,12 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/config" credmodel "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" + "github.com/tbd54566975/ssi-service/pkg/service/framework" ) const ( @@ -45,6 +47,10 @@ func init() { DisableQuote: true, ForceColors: true, }) + + config.SetAPIBase(endpoint) + config.SetServicePath(framework.Credential, "/credentials") + config.SetServicePath(framework.Schema, "/schemas") } type didConfigurationResourceParams struct { diff --git a/pkg/encryption/encryption.go b/internal/encryption/encryption.go similarity index 98% rename from pkg/encryption/encryption.go rename to internal/encryption/encryption.go index 64a0feba1..60a8fd124 100644 --- a/pkg/encryption/encryption.go +++ b/internal/encryption/encryption.go @@ -12,8 +12,9 @@ import ( "github.com/google/tink/go/keyset" "github.com/google/tink/go/tink" "github.com/pkg/errors" - "github.com/tbd54566975/ssi-service/internal/util" "google.golang.org/api/option" + + "github.com/tbd54566975/ssi-service/internal/util" ) // Encrypter the interface for any encrypter implementation. @@ -24,7 +25,7 @@ type Encrypter interface { // Decrypter is the interface for any decrypter. May be AEAD or Hybrid. type Decrypter interface { // Decrypt decrypts ciphertext. The second parameter may be treated as associated data for AEAD (as abstracted in - // https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfofor HPKE (https://www.rfc-editor.org/rfc/rfc9180.html) + // https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfo for HPKE (https://www.rfc-editor.org/rfc/rfc9180.html) Decrypt(ctx context.Context, ciphertext, contextInfo []byte) ([]byte, error) } diff --git a/pkg/encryption/encryption_test.go b/internal/encryption/encryption_test.go similarity index 99% rename from pkg/encryption/encryption_test.go rename to internal/encryption/encryption_test.go index ad1eddbe4..9e22d7419 100644 --- a/pkg/encryption/encryption_test.go +++ b/internal/encryption/encryption_test.go @@ -9,8 +9,9 @@ import ( "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/stretchr/testify/assert" - "github.com/tbd54566975/ssi-service/internal/util" "golang.org/x/crypto/chacha20poly1305" + + "github.com/tbd54566975/ssi-service/internal/util" ) func createServiceKey() (key string, err error) { diff --git a/pkg/authorizationserver/oauth2.go b/pkg/authorizationserver/oauth2.go index 98e1f546c..973c792e8 100644 --- a/pkg/authorizationserver/oauth2.go +++ b/pkg/authorizationserver/oauth2.go @@ -7,7 +7,6 @@ import ( "time" "github.com/TBD54566975/ssi-sdk/oidc/issuance" - "github.com/ardanlabs/conf" "github.com/gin-gonic/gin" "github.com/goccy/go-json" "github.com/ory/fosite" @@ -125,14 +124,13 @@ func loadIssuerMetadata(config *AuthConfig) (*issuance.IssuerMetadata, error) { } var im issuance.IssuerMetadata - if err := json.Unmarshal(jsonData, &im); err != nil { + if err = json.Unmarshal(jsonData, &im); err != nil { return nil, err } return &im, nil } type AuthConfig struct { - conf.Version Server config.ServerConfig CredentialIssuerFile string `toml:"credential_issuer_file" conf:"default:config/credential_issuer_metadata.example.json"` } diff --git a/pkg/authorizationserver/oauth2_test.go b/pkg/authorizationserver/oauth2_test.go index 560ddfc2e..9b0f94ed1 100644 --- a/pkg/authorizationserver/oauth2_test.go +++ b/pkg/authorizationserver/oauth2_test.go @@ -25,7 +25,7 @@ var ( func TestMain(m *testing.M) { store = storage.NewMemoryStore() - // Create an httptest server with the metadataHandler + // Create a httptest server with the metadataHandler authServer, err := NewServer(make(chan os.Signal, 1), &AuthConfig{ CredentialIssuerFile: "../../config/credential_issuer_metadata.example.json", }, store) diff --git a/pkg/server/middleware/Authentication.go b/pkg/server/middleware/authn.go similarity index 100% rename from pkg/server/middleware/Authentication.go rename to pkg/server/middleware/authn.go diff --git a/pkg/server/middleware/Authentication_test.go b/pkg/server/middleware/authn_test.go similarity index 96% rename from pkg/server/middleware/Authentication_test.go rename to pkg/server/middleware/authn_test.go index b32b86f95..e7172583f 100644 --- a/pkg/server/middleware/Authentication_test.go +++ b/pkg/server/middleware/authn_test.go @@ -52,8 +52,8 @@ func TestAuthMiddleware(t *testing.T) { } func TestNoAuthMiddleware(t *testing.T) { - - t.Setenv("AUTH_TOKEN", "") // no auth token so things just work + // no auth token so things just work + t.Setenv("AUTH_TOKEN", "") // Create a new gin engine r := gin.Default() @@ -77,5 +77,4 @@ func TestNoAuthMiddleware(t *testing.T) { // Assert that the status code is 200 OK assert.Equal(t, http.StatusOK, w.Code) - } diff --git a/pkg/server/middleware/Authorization.go b/pkg/server/middleware/authz.go similarity index 100% rename from pkg/server/middleware/Authorization.go rename to pkg/server/middleware/authz.go diff --git a/pkg/server/router/credential_test.go b/pkg/server/router/credential_test.go index 4b3e5dfd2..a9f87a815 100644 --- a/pkg/server/router/credential_test.go +++ b/pkg/server/router/credential_test.go @@ -14,6 +14,7 @@ import ( "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -25,7 +26,6 @@ import ( ) func TestCredentialRouter(t *testing.T) { - for _, test := range testutil.TestDatabases { t.Run(test.Name, func(t *testing.T) { @@ -47,7 +47,7 @@ func TestCredentialRouter(t *testing.T) { s := test.ServiceStorage(tt) assert.NotEmpty(tt, s) - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) @@ -205,7 +205,7 @@ func TestCredentialRouter(t *testing.T) { assert.NotEmpty(tt, s) // Initialize services - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) @@ -246,7 +246,7 @@ func TestCredentialRouter(t *testing.T) { err = keyStoreService.RevokeKey(context.Background(), keystore.RevokeKeyRequest{ID: keyID}) assert.NoError(tt, err) - // Create a crendential with the revoked key, it fails + // Create a credential with the revoked key, it fails subject = "did:test:43" createdCred, err = credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{ Issuer: didID, @@ -267,7 +267,7 @@ func TestCredentialRouter(t *testing.T) { s := test.ServiceStorage(tt) assert.NotEmpty(tt, s) - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "v1/credentials"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) @@ -376,7 +376,7 @@ func TestCredentialRouter(t *testing.T) { s := test.ServiceStorage(tt) assert.NotEmpty(tt, s) - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "/v1/credentials"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) @@ -480,7 +480,7 @@ func TestCredentialRouter(t *testing.T) { s := test.ServiceStorage(tt) assert.NotEmpty(tt, s) - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) @@ -543,7 +543,7 @@ func TestCredentialRouter(t *testing.T) { assert.NoError(tt, err) assert.Contains(tt, statusEntry.ID, fmt.Sprintf("%s/status", createdCred.Credential.ID)) - assert.Contains(tt, statusEntry.StatusListCredential, "http://localhost:1234/v1/credentials/status") + assert.Contains(tt, statusEntry.StatusListCredential, "https://ssi-service.com/v1/credentials/status") assert.NotEmpty(tt, statusEntry.StatusListIndex) credStatus, err := credService.GetCredentialStatus(context.Background(), credential.GetCredentialStatusRequest{ID: createdCred.ID}) @@ -600,7 +600,7 @@ func TestCredentialRouter(t *testing.T) { s := test.ServiceStorage(tt) assert.NotEmpty(tt, s) - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) @@ -663,7 +663,7 @@ func TestCredentialRouter(t *testing.T) { assert.NoError(tt, err) assert.Contains(tt, statusEntry.ID, fmt.Sprintf("%s/status", createdCred.Credential.ID)) - assert.Contains(tt, statusEntry.StatusListCredential, "http://localhost:1234/v1/credentials/status") + assert.Contains(tt, statusEntry.StatusListCredential, "https://ssi-service.com/v1/credentials/status") assert.NotEmpty(tt, statusEntry.StatusListIndex) credStatus, err := credService.GetCredentialStatus(context.Background(), credential.GetCredentialStatusRequest{ID: createdCred.ID}) @@ -720,7 +720,7 @@ func TestCredentialRouter(t *testing.T) { s := test.ServiceStorage(tt) assert.NotEmpty(tt, s) - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) @@ -828,7 +828,7 @@ func TestCredentialRouter(t *testing.T) { assert.NoError(tt, err) assert.Contains(tt, statusEntry.ID, fmt.Sprintf("%s/status", createdCred.Credential.ID)) - assert.Contains(tt, statusEntry.StatusListCredential, "http://localhost:1234/v1/credentials/status") + assert.Contains(tt, statusEntry.StatusListCredential, "https://ssi-service.com/v1/credentials/status") assert.NotEmpty(tt, statusEntry.StatusListIndex) credStatus, err := credService.GetCredentialStatus(context.Background(), credential.GetCredentialStatusRequest{ID: createdCred.ID}) @@ -884,7 +884,7 @@ func TestCredentialRouter(t *testing.T) { assert.NoError(tt, err) assert.Contains(tt, statusEntry.ID, fmt.Sprintf("%s/status", createdCred.Credential.ID)) - assert.Contains(tt, statusEntry.StatusListCredential, "http://localhost:1234/v1/credentials/status") + assert.Contains(tt, statusEntry.StatusListCredential, "https://ssi-service.com/v1/credentials/status") assert.NotEmpty(tt, statusEntry.StatusListIndex) credStatus, err := credService.GetCredentialStatus(context.Background(), credential.GetCredentialStatusRequest{ID: createdCred.ID}) @@ -962,7 +962,7 @@ func TestCredentialRouter(t *testing.T) { assert.NoError(tt, err) assert.Contains(tt, statusEntry.ID, fmt.Sprintf("%s/status", createdCred.Credential.ID)) - assert.Contains(tt, statusEntry.StatusListCredential, "http://localhost:1234/v1/credentials/status") + assert.Contains(tt, statusEntry.StatusListCredential, "https://ssi-service.com/v1/credentials/status") assert.NotEmpty(tt, statusEntry.StatusListIndex) credStatus, err := credService.GetCredentialStatus(context.Background(), credential.GetCredentialStatusRequest{ID: createdCred.ID}) @@ -1169,7 +1169,7 @@ func idFromURI(cred string) string { func createCredServicePrereqs(tt *testing.T, s storage.ServiceStorage) (issuer, verificationMethodID, schemaID string, credSvc credential.Service) { require.NotEmpty(tt, s) - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "http://localhost:1234/v1/credentials"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} keyStoreService := testKeyStoreService(tt, s) didService := testDIDService(tt, s, keyStoreService) schemaService := testSchemaService(tt, s, keyStoreService, didService) diff --git a/pkg/server/router/keystore_test.go b/pkg/server/router/keystore_test.go index 6db8efbdb..df1e11633 100644 --- a/pkg/server/router/keystore_test.go +++ b/pkg/server/router/keystore_test.go @@ -7,6 +7,7 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/pkg/testutil" "github.com/tbd54566975/ssi-service/config" @@ -36,10 +37,8 @@ func TestKeyStoreRouter(t *testing.T) { db := test.ServiceStorage(tt) assert.NotEmpty(tt, db) - serviceConfig := config.KeyStoreServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "keystore"}, - } - keyStoreService, err := keystore.NewKeyStoreService(serviceConfig, db) + serviceConfig := new(config.KeyStoreServiceConfig) + keyStoreService, err := keystore.NewKeyStoreService(*serviceConfig, db) assert.NoError(tt, err) assert.NotEmpty(tt, keyStoreService) diff --git a/pkg/server/router/presentation_test.go b/pkg/server/router/presentation_test.go index 0c248759d..6a5dc840f 100644 --- a/pkg/server/router/presentation_test.go +++ b/pkg/server/router/presentation_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -66,7 +65,7 @@ func TestPresentationDefinitionService(t *testing.T) { ka, err := keyaccess.NewJWKKeyAccessVerifier(authorDID.DID.ID, authorDID.DID.ID, pubKey) require.NoError(t, err) - service, err := presentation.NewPresentationService(config.PresentationServiceConfig{}, s, didService.GetResolver(), schemaService, keyStoreService) + service, err := presentation.NewPresentationService(s, didService.GetResolver(), schemaService, keyStoreService) require.NoError(t, err) t.Run("Create returns the created definition", func(t *testing.T) { diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index cb02e6e79..86604827d 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -2,6 +2,7 @@ package router import ( didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/framework" ) @@ -23,8 +24,6 @@ func (s *testService) Config() config.ServicesConfig { StorageProvider: "bolt", KeyStoreConfig: config.KeyStoreServiceConfig{}, DIDConfig: config.DIDServiceConfig{Methods: []string{string(didsdk.KeyMethod)}}, - SchemaConfig: config.SchemaServiceConfig{}, CredentialConfig: config.CredentialServiceConfig{}, - ManifestConfig: config.ManifestServiceConfig{}, } } diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go index de957dbd9..9ec338463 100644 --- a/pkg/server/router/schema_test.go +++ b/pkg/server/router/schema_test.go @@ -9,7 +9,6 @@ import ( didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/stretchr/testify/assert" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/keystore" @@ -40,10 +39,9 @@ func TestSchemaRouter(t *testing.T) { db := test.ServiceStorage(tt) assert.NotEmpty(tt, db) - serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} keyStoreService := testKeyStoreService(tt, db) didService := testDIDService(tt, db, keyStoreService) - schemaService, err := schema.NewSchemaService(serviceConfig, db, keyStoreService, didService.GetResolver()) + schemaService, err := schema.NewSchemaService(db, keyStoreService, didService.GetResolver()) assert.NoError(tt, err) assert.NotEmpty(tt, schemaService) @@ -120,10 +118,9 @@ func TestSchemaSigning(t *testing.T) { db := test.ServiceStorage(tt) assert.NotEmpty(tt, db) - serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} keyStoreService := testKeyStoreService(tt, db) didService := testDIDService(tt, db, keyStoreService) - schemaService, err := schema.NewSchemaService(serviceConfig, db, keyStoreService, didService.GetResolver()) + schemaService, err := schema.NewSchemaService(db, keyStoreService, didService.GetResolver()) assert.NoError(tt, err) assert.NotEmpty(tt, schemaService) @@ -145,10 +142,9 @@ func TestSchemaSigning(t *testing.T) { db := test.ServiceStorage(tt) assert.NotEmpty(tt, db) - serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} keyStoreService := testKeyStoreService(tt, db) didService := testDIDService(tt, db, keyStoreService) - schemaService, err := schema.NewSchemaService(serviceConfig, db, keyStoreService, didService.GetResolver()) + schemaService, err := schema.NewSchemaService(db, keyStoreService, didService.GetResolver()) assert.NoError(tt, err) assert.NotEmpty(tt, schemaService) diff --git a/pkg/server/router/testutils_test.go b/pkg/server/router/testutils_test.go index f33ae878a..71d5b8b89 100644 --- a/pkg/server/router/testutils_test.go +++ b/pkg/server/router/testutils_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/presentation" "github.com/tbd54566975/ssi-service/pkg/testutil" @@ -18,8 +20,14 @@ import ( "github.com/tbd54566975/ssi-service/pkg/storage" ) +const ( + testServerURL = "https://ssi-service.com" +) + func TestMain(t *testing.M) { testutil.EnableSchemaCaching() + config.SetAPIBase(testServerURL) + config.SetServicePath(framework.Credential, "/credentials") os.Exit(t.Run()) } @@ -34,9 +42,6 @@ func testKeyStoreService(t *testing.T, db storage.ServiceStorage) *keystore.Serv func testDIDService(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Service) *did.Service { serviceConfig := config.DIDServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{ - Name: "did", - }, Methods: []string{"key"}, LocalResolutionMethods: []string{"key"}, } @@ -48,16 +53,15 @@ func testDIDService(t *testing.T, db storage.ServiceStorage, keyStore *keystore. } func testSchemaService(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Service, did *did.Service) *schema.Service { - serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} // create a schema service - schemaService, err := schema.NewSchemaService(serviceConfig, db, keyStore, did.GetResolver()) + schemaService, err := schema.NewSchemaService(db, keyStore, did.GetResolver()) require.NoError(t, err) require.NotEmpty(t, schemaService) return schemaService } func testCredentialService(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Service, did *did.Service, schema *schema.Service) *credential.Service { - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 100} // create a credential service credentialService, err := credential.NewCredentialService(serviceConfig, db, keyStore, did.GetResolver(), schema) require.NoError(t, err) @@ -66,19 +70,15 @@ func testCredentialService(t *testing.T, db storage.ServiceStorage, keyStore *ke } func testPresentationDefinitionService(t *testing.T, db storage.ServiceStorage, didService *did.Service, schemaService *schema.Service, keyStoreService *keystore.Service) *presentation.Service { - cfg := config.PresentationServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{ - Name: "presentation", - }} - svc, err := presentation.NewPresentationService(cfg, db, didService.GetResolver(), schemaService, keyStoreService) + svc, err := presentation.NewPresentationService(db, didService.GetResolver(), schemaService, keyStoreService) require.NoError(t, err) require.NotEmpty(t, svc) return svc } func testManifestService(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Service, did *did.Service, credential *credential.Service, presentationSvc *presentation.Service) *manifest.Service { - serviceConfig := config.ManifestServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "manifest"}} // create a manifest service - manifestService, err := manifest.NewManifestService(serviceConfig, db, keyStore, did.GetResolver(), credential, presentationSvc) + manifestService, err := manifest.NewManifestService(db, keyStore, did.GetResolver(), credential, presentationSvc) require.NoError(t, err) require.NotEmpty(t, manifestService) return manifestService diff --git a/pkg/server/server.go b/pkg/server/server.go index bff03db26..dbc760428 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -21,8 +21,6 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/webhook" ) -// gin-swagger middleware - const ( HealthPrefix = "/health" ReadinessPrefix = "/readiness" @@ -65,6 +63,9 @@ func NewSSIServer(shutdown chan os.Signal, cfg config.SSIServiceConfig) (*SSISer return nil, sdkutil.LoggingErrorMsg(err, "unable to instantiate ssi service") } + // make sure to set the api base in our service info + config.SetAPIBase(cfg.Services.ServiceEndpoint) + // service-level routers engine.GET(HealthPrefix, router.Health) engine.GET(ReadinessPrefix, router.Readiness(ssi.GetServices())) @@ -118,7 +119,6 @@ func setUpEngine(cfg config.ServerConfig, shutdown chan os.Signal) *gin.Engine { gin.Recovery(), gin.Logger(), middleware.Errors(shutdown), - middleware.AuthMiddleware(), } if cfg.JagerEnabled { middlewares = append(middlewares, otelgin.Middleware(config.ServiceName)) @@ -141,6 +141,22 @@ func setUpEngine(cfg config.ServerConfig, shutdown chan os.Signal) *gin.Engine { return engine } +// KeyStoreAPI registers all HTTP handlers for the Key Store Service +func KeyStoreAPI(rg *gin.RouterGroup, service svcframework.Service) (err error) { + keyStoreRouter, err := router.NewKeyStoreRouter(service) + if err != nil { + return sdkutil.LoggingErrorMsg(err, "creating key store router") + } + + // make sure the keystore service is configured to use the correct path + config.SetServicePath(svcframework.KeyStore, KeyStorePrefix) + keyStoreAPI := rg.Group(KeyStorePrefix) + keyStoreAPI.PUT("", keyStoreRouter.StoreKey) + keyStoreAPI.GET("/:id", keyStoreRouter.GetKeyDetails) + keyStoreAPI.DELETE("/:id", keyStoreRouter.RevokeKey) + return +} + // DecentralizedIdentityAPI registers all HTTP handlers for the DID Service func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, did *didsvc.BatchService, webhookService *webhook.Service) (err error) { didRouter, err := router.NewDIDRouter(service) @@ -149,6 +165,8 @@ func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, did } batchDIDRouter := router.NewBatchDIDRouter(did) + // make sure the DID service is configured to use the correct path + config.SetServicePath(svcframework.DID, DIDsPrefix) didAPI := rg.Group(DIDsPrefix) didAPI.GET("", didRouter.ListDIDMethods) didAPI.PUT("/:method", middleware.Webhook(webhookService, webhook.DID, webhook.Create), didRouter.CreateDIDByMethod) @@ -167,6 +185,8 @@ func SchemaAPI(rg *gin.RouterGroup, service svcframework.Service, webhookService return sdkutil.LoggingErrorMsg(err, "creating schema router") } + // make sure the schema service is configured to use the correct path + config.SetServicePath(svcframework.Schema, SchemasPrefix) schemaAPI := rg.Group(SchemasPrefix) schemaAPI.PUT("", middleware.Webhook(webhookService, webhook.Schema, webhook.Create), schemaRouter.CreateSchema) schemaAPI.GET("/:id", schemaRouter.GetSchema) @@ -182,6 +202,9 @@ func CredentialAPI(rg *gin.RouterGroup, service svcframework.Service, webhookSer return sdkutil.LoggingErrorMsg(err, "creating credential router") } + // make sure the credential service is configured to use the correct path + config.SetServicePath(svcframework.Credential, CredentialsPrefix) + // Credentials credentialAPI := rg.Group(CredentialsPrefix) credentialAPI.PUT("", middleware.Webhook(webhookService, webhook.Credential, webhook.Create), credRouter.CreateCredential) @@ -205,6 +228,9 @@ func PresentationAPI(rg *gin.RouterGroup, service svcframework.Service, webhookS return sdkutil.LoggingErrorMsg(err, "creating credential router") } + // make sure the presentation service is configured to use the correct path + config.SetServicePath(svcframework.Presentation, PresentationsPrefix) + presDefAPI := rg.Group(PresentationsPrefix + DefinitionsPrefix) presDefAPI.PUT("", presRouter.CreateDefinition) presDefAPI.GET("/:id", presRouter.GetDefinition) @@ -225,20 +251,6 @@ func PresentationAPI(rg *gin.RouterGroup, service svcframework.Service, webhookS return } -// KeyStoreAPI registers all HTTP handlers for the Key Store Service -func KeyStoreAPI(rg *gin.RouterGroup, service svcframework.Service) (err error) { - keyStoreRouter, err := router.NewKeyStoreRouter(service) - if err != nil { - return sdkutil.LoggingErrorMsg(err, "creating key store router") - } - - keyStoreAPI := rg.Group(KeyStorePrefix) - keyStoreAPI.PUT("", keyStoreRouter.StoreKey) - keyStoreAPI.GET("/:id", keyStoreRouter.GetKeyDetails) - keyStoreAPI.DELETE("/:id", keyStoreRouter.RevokeKey) - return -} - // OperationAPI registers all HTTP handlers for the Operations Service func OperationAPI(rg *gin.RouterGroup, service svcframework.Service) (err error) { operationRouter, err := router.NewOperationRouter(service) @@ -246,6 +258,9 @@ func OperationAPI(rg *gin.RouterGroup, service svcframework.Service) (err error) return sdkutil.LoggingErrorMsg(err, "creating operation router") } + // make sure the operation service is configured to use the correct path + config.SetServicePath(svcframework.Operation, OperationPrefix) + operationAPI := rg.Group(OperationPrefix) operationAPI.GET("", operationRouter.ListOperations) // In this case, it's used so that the operation id matches `presentations/submissions/{submission_id}` for the DIDWebID @@ -262,6 +277,9 @@ func ManifestAPI(rg *gin.RouterGroup, service svcframework.Service, webhookServi return sdkutil.LoggingErrorMsg(err, "creating manifest router") } + // make sure the manifest service is configured to use the correct path + config.SetServicePath(svcframework.Manifest, ManifestsPrefix) + manifestAPI := rg.Group(ManifestsPrefix) manifestAPI.PUT("", middleware.Webhook(webhookService, webhook.Manifest, webhook.Create), manifestRouter.CreateManifest) manifestAPI.GET("", manifestRouter.ListManifests) @@ -295,6 +313,9 @@ func IssuanceAPI(rg *gin.RouterGroup, service svcframework.Service) error { return sdkutil.LoggingErrorMsg(err, "creating issuing router") } + // make sure the issuance service is configured to use the correct path + config.SetServicePath(svcframework.Issuance, IssuanceTemplatePrefix) + issuanceAPI := rg.Group(IssuanceTemplatePrefix) issuanceAPI.PUT("", issuanceRouter.CreateIssuanceTemplate) issuanceAPI.GET("", issuanceRouter.ListIssuanceTemplates) @@ -310,6 +331,9 @@ func WebhookAPI(rg *gin.RouterGroup, service svcframework.Service) (err error) { return sdkutil.LoggingErrorMsg(err, "creating webhook router") } + // make sure the webhook service is configured to use the correct path + config.SetServicePath(svcframework.Webhook, WebhookPrefix) + webhookAPI := rg.Group(WebhookPrefix) webhookAPI.PUT("", webhookRouter.CreateWebhook) webhookAPI.GET("", webhookRouter.ListWebhooks) @@ -328,6 +352,9 @@ func DIDConfigurationAPI(rg *gin.RouterGroup, service svcframework.Service) erro return sdkutil.LoggingErrorMsg(err, "creating webhook router") } + // make sure the did configuration service is configured to use the correct path + config.SetServicePath(svcframework.DIDConfiguration, DIDConfigurationsPrefix) + webhookAPI := rg.Group(DIDConfigurationsPrefix) webhookAPI.PUT("", didConfigurationsRouter.CreateDIDConfiguration) webhookAPI.PUT(VerificationPath, didConfigurationsRouter.VerifyDIDConfiguration) diff --git a/pkg/server/server_issuance_test.go b/pkg/server/server_issuance_test.go index 27d1879af..377c60bea 100644 --- a/pkg/server/server_issuance_test.go +++ b/pkg/server/server_issuance_test.go @@ -16,7 +16,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -456,8 +455,7 @@ func setupAllThings(t *testing.T, s storage.ServiceStorage) (*did.CreateDIDRespo } func testIssuanceRouter(t *testing.T, s storage.ServiceStorage) *router.IssuanceRouter { - serviceConfig := config.IssuanceServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "test-issuance"}} - svc, err := issuance.NewIssuanceService(serviceConfig, s) + svc, err := issuance.NewIssuanceService(s) require.NoError(t, err) r, err := router.NewIssuanceRouter(svc) diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go index 459db3d01..d379365f3 100644 --- a/pkg/server/server_presentation_test.go +++ b/pkg/server/server_presentation_test.go @@ -25,7 +25,6 @@ import ( "github.com/tbd54566975/ssi-service/pkg/testutil" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" @@ -757,7 +756,7 @@ func setupPresentationRouter(t *testing.T, s storage.ServiceStorage) (*router.Pr didService, _ := testDIDService(t, s, keyStoreService, nil) schemaService := testSchemaService(t, s, keyStoreService, didService) - service, err := presentation.NewPresentationService(config.PresentationServiceConfig{}, s, didService.GetResolver(), schemaService, keyStoreService) + service, err := presentation.NewPresentationService(s, didService.GetResolver(), schemaService, keyStoreService) assert.NoError(t, err) pRouter, err := router.NewPresentationRouter(service) diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index dca596fad..429b09f5b 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -11,6 +11,7 @@ import ( "github.com/TBD54566975/ssi-sdk/credential/exchange" "github.com/gin-gonic/gin" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/service/issuance" "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" @@ -39,10 +40,12 @@ import ( const ( testIONResolverURL = "https://test-ion-resolver.com" + testServerURL = "https://ssi-service.com" ) func TestMain(t *testing.M) { testutil.EnableSchemaCaching() + config.SetAPIBase(testServerURL) os.Exit(t.Run()) } @@ -224,14 +227,12 @@ func testKeyStore(t *testing.T, bolt storage.ServiceStorage) (*router.KeyStoreRo } func testKeyStoreService(t *testing.T, db storage.ServiceStorage) (*keystore.Service, keystore.ServiceFactory) { - serviceConfig := config.KeyStoreServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "test-keystore"}, - } + serviceConfig := new(config.KeyStoreServiceConfig) // create a keystore service encrypter, decrypter, err := keystore.NewServiceEncryption(db, serviceConfig.EncryptionConfig, keystore.ServiceKeyEncryptionKey) require.NoError(t, err) - factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db, encrypter, decrypter) + factory := keystore.NewKeyStoreServiceFactory(*serviceConfig, db, encrypter, decrypter) keystoreService, err := factory(db) require.NoError(t, err) require.NotEmpty(t, keystoreService) @@ -239,11 +240,7 @@ func testKeyStoreService(t *testing.T, db storage.ServiceStorage) (*keystore.Ser } func testIssuanceService(t *testing.T, db storage.ServiceStorage) *issuance.Service { - cfg := config.IssuanceServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "test-issuing"}, - } - - s, err := issuance.NewIssuanceService(cfg, db) + s, err := issuance.NewIssuanceService(db) require.NoError(t, err) require.NotEmpty(t, s) return s @@ -254,7 +251,6 @@ func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystor methods = []string{"key"} } serviceConfig := config.DIDServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "test-did"}, Methods: methods, LocalResolutionMethods: []string{"key", "web", "peer", "pkh"}, IONResolverURL: testIONResolverURL, @@ -284,7 +280,7 @@ func testDIDRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore } func testSchemaService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, did *did.Service) *schema.Service { - schemaService, err := schema.NewSchemaService(config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "test-schema"}}, bolt, keyStore, did.GetResolver()) + schemaService, err := schema.NewSchemaService(bolt, keyStore, did.GetResolver()) require.NoError(t, err) require.NotEmpty(t, schemaService) return schemaService @@ -301,7 +297,7 @@ func testSchemaRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *keyst } func testCredentialService(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Service, did *did.Service, schema *schema.Service) *credential.Service { - serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential", ServiceEndpoint: "https://ssi-service.com/v1/credentials"}, BatchCreateMaxItems: 1000} + serviceConfig := config.CredentialServiceConfig{BatchCreateMaxItems: 1000} // create a credential service credentialService, err := credential.NewCredentialService(serviceConfig, db, keyStore, did.GetResolver(), schema) @@ -313,6 +309,9 @@ func testCredentialService(t *testing.T, db storage.ServiceStorage, keyStore *ke func testCredentialRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *keystore.Service, did *did.Service, schema *schema.Service) *router.CredentialRouter { credentialService := testCredentialService(t, bolt, keyStore, did, schema) + // set endpoint in service info + config.SetServicePath(svcframework.Credential, CredentialsPrefix) + // create router for service credentialRouter, err := router.NewCredentialRouter(credentialService) require.NoError(t, err) @@ -322,9 +321,8 @@ func testCredentialRouter(t *testing.T, bolt storage.ServiceStorage, keyStore *k } func testManifest(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Service, did *did.Service, credential *credential.Service) (*router.ManifestRouter, *manifest.Service) { - serviceConfig := config.ManifestServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "manifest"}} // create a manifest service - manifestService, err := manifest.NewManifestService(serviceConfig, db, keyStore, did.GetResolver(), credential, nil) + manifestService, err := manifest.NewManifestService(db, keyStore, did.GetResolver(), credential, nil) require.NoError(t, err) require.NotEmpty(t, manifestService) @@ -337,10 +335,7 @@ func testManifest(t *testing.T, db storage.ServiceStorage, keyStore *keystore.Se } func testWebhookService(t *testing.T, bolt storage.ServiceStorage) *webhook.Service { - serviceConfig := config.WebhookServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "webhook"}, - WebhookTimeout: "10s", - } + serviceConfig := config.WebhookServiceConfig{WebhookTimeout: "10s"} // create a webhook service webhookService, err := webhook.NewWebhookService(serviceConfig, bolt) diff --git a/pkg/service/credential/service.go b/pkg/service/credential/service.go index 4229c1bb3..7f8f02290 100644 --- a/pkg/service/credential/service.go +++ b/pkg/service/credential/service.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/config" credint "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" @@ -65,7 +66,8 @@ func (s Service) Config() config.CredentialServiceConfig { return s.config } -func NewCredentialService(config config.CredentialServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, didResolver resolution.Resolver, schema *schema.Service) (*Service, error) { +func NewCredentialService(config config.CredentialServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, + didResolver resolution.Resolver, schema *schema.Service) (*Service, error) { credentialStorage, err := NewCredentialStorage(s) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate storage for the credential service") @@ -142,7 +144,7 @@ func (s Service) createCredential(ctx context.Context, request CreateCredentialR builder := credential.NewVerifiableCredentialBuilder() credentialID := uuid.NewString() - credentialURI := s.Config().ServiceEndpoint + "/" + credentialID + credentialURI := config.GetServicePath(framework.Credential) + "/" + credentialID if err := builder.SetID(credentialURI); err != nil { return nil, sdkutil.LoggingErrorMsgf(err, "could not build credential when setting id: %s", credentialURI) } diff --git a/pkg/service/credential/status.go b/pkg/service/credential/status.go index eae39ff30..6037b4c2d 100644 --- a/pkg/service/credential/status.go +++ b/pkg/service/credential/status.go @@ -11,7 +11,9 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/config" credint "github.com/tbd54566975/ssi-service/internal/credential" + "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -67,7 +69,7 @@ func (s Service) createStatusListEntryForCredential(ctx context.Context, credID func (s Service) createStatusListCredential(ctx context.Context, tx storage.Tx, statusPurpose statussdk.StatusPurpose, issuerID, fullyQualifiedVerificationMethodID string, slcMetadata StatusListCredentialMetadata) (int, *credential.VerifiableCredential, error) { statusListID := uuid.NewString() - statusListURI := fmt.Sprintf("%s/status/%s", s.config.ServiceEndpoint, statusListID) + statusListURI := fmt.Sprintf("%s/status/%s", config.GetServicePath(framework.Credential), statusListID) generatedStatusListCredential, err := statussdk.GenerateStatusList2021Credential(statusListURI, issuerID, statusPurpose, []credential.VerifiableCredential{}) if err != nil { diff --git a/pkg/service/did/batch.go b/pkg/service/did/batch.go new file mode 100644 index 000000000..a9429dc29 --- /dev/null +++ b/pkg/service/did/batch.go @@ -0,0 +1,87 @@ +package did + +import ( + "context" + + didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/google/uuid" + "github.com/pkg/errors" + + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/pkg/service/keystore" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +type BatchService struct { + config config.DIDServiceConfig + storage *Storage + + keyStoreFactory keystore.ServiceFactory + didStorageFactory StorageFactory +} + +func NewBatchDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, factory keystore.ServiceFactory) (*BatchService, error) { + didStorage, err := NewDIDStorage(s) + if err != nil { + return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service") + } + + service := BatchService{ + config: config, + storage: didStorage, + keyStoreFactory: factory, + didStorageFactory: NewDIDStorageFactory(s), + } + return &service, nil +} + +func (s *BatchService) BatchCreateDIDs(ctx context.Context, batchReq BatchCreateDIDsRequest) (*BatchCreateDIDsResponse, error) { + watchKey := storage.WatchKey{ + Namespace: "temporary", + Key: "batch-create-dids-key-" + uuid.NewString(), + } + if err := s.storage.db.Write(ctx, watchKey.Namespace, watchKey.Key, []byte("starting")); err != nil { + return nil, err + } + returnValue, err := s.storage.db.Execute(ctx, func(ctx context.Context, tx storage.Tx) (any, error) { + batchResponse := BatchCreateDIDsResponse{ + DIDs: make([]didsdk.Document, 0, len(batchReq.Requests)), + } + keyStore, err := s.keyStoreFactory(tx) + if err != nil { + return nil, err + } + didStorage, err := s.didStorageFactory(tx) + if err != nil { + return nil, err + } + handler, err := NewKeyHandler(didStorage, keyStore) + if err != nil { + return nil, err + } + // watch some new key watchKey + // accumulate all writes + // execute all writes s.t. if one write fails, then watchKey is written from elsewhere + for _, request := range batchReq.Requests { + didResponse, err := handler.CreateDID(ctx, request) + if err != nil { + return nil, err + } + batchResponse.DIDs = append(batchResponse.DIDs, didResponse.DID) + } + return &batchResponse, nil + }, []storage.WatchKey{watchKey}) + if err != nil { + return nil, err + } + + batchResponse, ok := returnValue.(*BatchCreateDIDsResponse) + if !ok { + return nil, errors.New("problem casting to BatchCreateDIDsResponse") + } + return batchResponse, nil +} + +func (s *BatchService) Config() config.DIDServiceConfig { + return s.config +} diff --git a/pkg/service/did/ion_test.go b/pkg/service/did/ion_test.go index 7612d9275..1a7323112 100644 --- a/pkg/service/did/ion_test.go +++ b/pkg/service/did/ion_test.go @@ -13,11 +13,12 @@ import ( "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/h2non/gock.v1" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/keystore" "github.com/tbd54566975/ssi-service/pkg/storage" "github.com/tbd54566975/ssi-service/pkg/testutil" - "gopkg.in/h2non/gock.v1" ) //go:embed testdata/basic_did_resolution.json @@ -284,12 +285,10 @@ func TestIONHandler(t *testing.T) { } func testKeyStoreService(t *testing.T, db storage.ServiceStorage) *keystore.Service { - serviceConfig := config.KeyStoreServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{Name: "test-keystore"}, - } + serviceConfig := new(config.KeyStoreServiceConfig) // create a keystore service - keystoreService, err := keystore.NewKeyStoreService(serviceConfig, db) + keystoreService, err := keystore.NewKeyStoreService(*serviceConfig, db) require.NoError(t, err) require.NotEmpty(t, keystoreService) return keystoreService diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go index ccd029573..4108105a2 100644 --- a/pkg/service/did/service.go +++ b/pkg/service/did/service.go @@ -7,8 +7,8 @@ import ( didsdk "github.com/TBD54566975/ssi-sdk/did" didresolution "github.com/TBD54566975/ssi-sdk/did/resolution" sdkutil "github.com/TBD54566975/ssi-sdk/util" - "github.com/google/uuid" "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/did/resolution" "github.com/tbd54566975/ssi-service/pkg/service/framework" @@ -220,78 +220,3 @@ func (s *Service) getHandler(method didsdk.Method) (MethodHandler, error) { } return handler, nil } - -type BatchService struct { - config config.DIDServiceConfig - storage *Storage - - keyStoreFactory keystore.ServiceFactory - didStorageFactory StorageFactory -} - -func NewBatchDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, factory keystore.ServiceFactory) (*BatchService, error) { - didStorage, err := NewDIDStorage(s) - if err != nil { - return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service") - } - - service := BatchService{ - config: config, - storage: didStorage, - keyStoreFactory: factory, - didStorageFactory: NewDIDStorageFactory(s), - } - return &service, nil -} - -func (s *BatchService) BatchCreateDIDs(ctx context.Context, batchReq BatchCreateDIDsRequest) (*BatchCreateDIDsResponse, error) { - watchKey := storage.WatchKey{ - Namespace: "temporary", - Key: "batch-create-dids-key-" + uuid.NewString(), - } - err := s.storage.db.Write(ctx, watchKey.Namespace, watchKey.Key, []byte("starting")) - if err != nil { - return nil, err - } - returnValue, err := s.storage.db.Execute(ctx, func(ctx context.Context, tx storage.Tx) (any, error) { - batchResponse := BatchCreateDIDsResponse{ - DIDs: make([]didsdk.Document, 0, len(batchReq.Requests)), - } - keyStore, err := s.keyStoreFactory(tx) - if err != nil { - return nil, err - } - didStorage, err := s.didStorageFactory(tx) - if err != nil { - return nil, err - } - handler, err := NewKeyHandler(didStorage, keyStore) - if err != nil { - return nil, err - } - // watch some new key watchKey - // accumulate all writes - // execute all writes s.t. if one write fails, then watchKey is written from elsewhere - for _, request := range batchReq.Requests { - didResponse, err := handler.CreateDID(ctx, request) - if err != nil { - return nil, err - } - batchResponse.DIDs = append(batchResponse.DIDs, didResponse.DID) - } - return &batchResponse, nil - }, []storage.WatchKey{watchKey}) - if err != nil { - return nil, err - } - - batchResponse, ok := returnValue.(*BatchCreateDIDsResponse) - if !ok { - return nil, errors.New("problem casting to BatchCreateDIDsResponse") - } - return batchResponse, nil -} - -func (s *BatchService) Config() config.DIDServiceConfig { - return s.config -} diff --git a/pkg/service/issuance/service.go b/pkg/service/issuance/service.go index e7b32301e..f5d34bf00 100644 --- a/pkg/service/issuance/service.go +++ b/pkg/service/issuance/service.go @@ -6,7 +6,6 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/framework" manifeststg "github.com/tbd54566975/ssi-service/pkg/service/manifest/storage" "github.com/tbd54566975/ssi-service/pkg/service/schema" @@ -14,13 +13,12 @@ import ( ) type Service struct { - config config.IssuanceServiceConfig storage Storage manifestStorage manifeststg.Storage schemaStorage schema.Storage } -func NewIssuanceService(config config.IssuanceServiceConfig, s storage.ServiceStorage) (*Service, error) { +func NewIssuanceService(s storage.ServiceStorage) (*Service, error) { issuanceStorage, err := NewIssuanceStorage(s) if err != nil { return nil, errors.Wrap(err, "creating issuance storage") @@ -35,7 +33,6 @@ func NewIssuanceService(config config.IssuanceServiceConfig, s storage.ServiceSt } return &Service{ storage: *issuanceStorage, - config: config, manifestStorage: *manifestStorage, schemaStorage: *schemaStorage, }, nil diff --git a/pkg/service/keystore/service.go b/pkg/service/keystore/service.go index cb8c4b5c2..0e7ab1496 100644 --- a/pkg/service/keystore/service.go +++ b/pkg/service/keystore/service.go @@ -10,13 +10,14 @@ import ( "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/crypto/chacha20poly1305" + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/internal/encryption" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" - "github.com/tbd54566975/ssi-service/pkg/encryption" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/storage" - "golang.org/x/crypto/chacha20poly1305" ) type ServiceFactory func(storage.Tx) (*Service, error) diff --git a/pkg/service/keystore/service_test.go b/pkg/service/keystore/service_test.go index 7adf5eafe..31d040cf4 100644 --- a/pkg/service/keystore/service_test.go +++ b/pkg/service/keystore/service_test.go @@ -12,6 +12,7 @@ import ( "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -111,13 +112,8 @@ func createKeyStoreService(t *testing.T) (*Service, error) { _ = os.Remove(s.URI()) }) - keyStore, err := NewKeyStoreService( - config.KeyStoreServiceConfig{ - BaseServiceConfig: &config.BaseServiceConfig{ - Name: "test-keyStore", - }, - }, - s) + serviceConfig := new(config.KeyStoreServiceConfig) + keyStore, err := NewKeyStoreService(*serviceConfig, s) mockClock := clock.NewMock() mockClock.Set(time.Date(2023, 06, 23, 0, 0, 0, 0, time.UTC)) diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go index 097c893ef..393f8e0aa 100644 --- a/pkg/service/keystore/storage.go +++ b/pkg/service/keystore/storage.go @@ -11,7 +11,8 @@ import ( "github.com/goccy/go-json" "github.com/mr-tron/base58" "github.com/pkg/errors" - "github.com/tbd54566975/ssi-service/pkg/encryption" + + "github.com/tbd54566975/ssi-service/internal/encryption" "github.com/tbd54566975/ssi-service/pkg/storage" ) diff --git a/pkg/service/manifest/service.go b/pkg/service/manifest/service.go index cd9baf518..fc13cfcc4 100644 --- a/pkg/service/manifest/service.go +++ b/pkg/service/manifest/service.go @@ -14,7 +14,7 @@ import ( "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/tbd54566975/ssi-service/config" + credint "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/pkg/service/common" @@ -38,7 +38,6 @@ type Service struct { storage *manifeststg.Storage opsStorage *operation.Storage issuanceTemplateStorage *issuance.Storage - config config.ManifestServiceConfig // external dependencies keyStore *keystore.Service @@ -77,11 +76,7 @@ func (s Service) Status() framework.Status { return framework.Status{Status: framework.StatusReady} } -func (s Service) Config() config.ManifestServiceConfig { - return s.config -} - -func NewManifestService(config config.ManifestServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, didResolver resolution.Resolver, credential *credential.Service, presentationSvc *presentation.Service) (*Service, error) { +func NewManifestService(s storage.ServiceStorage, keyStore *keystore.Service, didResolver resolution.Resolver, credential *credential.Service, presentationSvc *presentation.Service) (*Service, error) { manifestStorage, err := manifeststg.NewManifestStorage(s) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate storage for the manifest service") @@ -99,7 +94,6 @@ func NewManifestService(config config.ManifestServiceConfig, s storage.ServiceSt storage: manifestStorage, opsStorage: opsStorage, issuanceTemplateStorage: issuanceStorage, - config: config, keyStore: keyStore, didResolver: didResolver, credential: credential, diff --git a/pkg/service/presentation/service.go b/pkg/service/presentation/service.go index b791f2123..30f182bb2 100644 --- a/pkg/service/presentation/service.go +++ b/pkg/service/presentation/service.go @@ -12,7 +12,6 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/credential" didint "github.com/tbd54566975/ssi-service/internal/did" "github.com/tbd54566975/ssi-service/internal/keyaccess" @@ -34,7 +33,6 @@ type Service struct { storage presentationstorage.Storage keystore *keystore.Service opsStorage *operation.Storage - config config.PresentationServiceConfig resolver resolution.Resolver schema *schema.Service verifier *credential.Validator @@ -59,11 +57,7 @@ func (s Service) Status() framework.Status { return framework.Status{Status: framework.StatusReady} } -func (s Service) Config() config.PresentationServiceConfig { - return s.config -} - -func NewPresentationService(config config.PresentationServiceConfig, s storage.ServiceStorage, +func NewPresentationService(s storage.ServiceStorage, resolver resolution.Resolver, schema *schema.Service, keystore *keystore.Service) (*Service, error) { presentationStorage, err := NewPresentationStorage(s) if err != nil { @@ -82,7 +76,6 @@ func NewPresentationService(config config.PresentationServiceConfig, s storage.S storage: presentationStorage, keystore: keystore, opsStorage: opsStorage, - config: config, resolver: resolver, schema: schema, verifier: verifier, diff --git a/pkg/service/schema/service.go b/pkg/service/schema/service.go index 57b562991..77c1a8cb2 100644 --- a/pkg/service/schema/service.go +++ b/pkg/service/schema/service.go @@ -28,7 +28,6 @@ import ( type Service struct { storage *Storage - config config.SchemaServiceConfig // external dependencies keyStore *keystore.Service @@ -59,11 +58,7 @@ func (s Service) Status() framework.Status { return framework.Status{Status: framework.StatusReady} } -func (s Service) Config() config.SchemaServiceConfig { - return s.config -} - -func NewSchemaService(config config.SchemaServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, +func NewSchemaService(s storage.ServiceStorage, keyStore *keystore.Service, resolver resolution.Resolver) (*Service, error) { schemaStorage, err := NewSchemaStorage(s) if err != nil { @@ -71,7 +66,6 @@ func NewSchemaService(config config.SchemaServiceConfig, s storage.ServiceStorag } service := Service{ storage: schemaStorage, - config: config, keyStore: keyStore, resolver: resolver, } @@ -122,7 +116,7 @@ func (s Service) CreateSchema(ctx context.Context, request CreateSchemaRequest) // if the schema is a credential schema, the credential's id is a fully qualified URI // if the schema is a JSON schema, the schema's id is a fully qualified URI schemaID := uuid.NewString() - schemaURI := strings.Join([]string{s.Config().ServiceEndpoint, schemaID}, "/") + schemaURI := strings.Join([]string{config.GetServicePath(framework.Schema), schemaID}, "/") // create schema for storage storedSchema := StoredSchema{ID: schemaID} diff --git a/pkg/service/service.go b/pkg/service/service.go index d3962bc36..9183c393e 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -5,6 +5,7 @@ import ( sdkutil "github.com/TBD54566975/ssi-sdk/util" "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -59,21 +60,6 @@ func validateServiceConfig(config config.ServicesConfig) error { if config.DIDConfig.IsEmpty() { return fmt.Errorf("%s no config provided", framework.DID) } - if config.IssuanceServiceConfig.IsEmpty() { - return fmt.Errorf("%s no config provided", framework.Issuance) - } - if config.SchemaConfig.IsEmpty() { - return fmt.Errorf("%s no config provided", framework.Schema) - } - if config.CredentialConfig.IsEmpty() { - return fmt.Errorf("%s no config provided", framework.Credential) - } - if config.ManifestConfig.IsEmpty() { - return fmt.Errorf("%s no config provided", framework.Manifest) - } - if config.PresentationConfig.IsEmpty() { - return fmt.Errorf("%s no config provided", framework.Presentation) - } if config.WebhookConfig.IsEmpty() { return fmt.Errorf("%s no config provided", framework.Webhook) } @@ -126,12 +112,12 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) { } didResolver := didService.GetResolver() - schemaService, err := schema.NewSchemaService(config.SchemaConfig, storageProvider, keyStoreService, didResolver) + schemaService, err := schema.NewSchemaService(storageProvider, keyStoreService, didResolver) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the schema service") } - issuanceService, err := issuance.NewIssuanceService(config.IssuanceServiceConfig, storageProvider) + issuanceService, err := issuance.NewIssuanceService(storageProvider) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the issuance service") } @@ -141,12 +127,12 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the credential service") } - presentationService, err := presentation.NewPresentationService(config.PresentationConfig, storageProvider, didResolver, schemaService, keyStoreService) + presentationService, err := presentation.NewPresentationService(storageProvider, didResolver, schemaService, keyStoreService) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the presentation service") } - manifestService, err := manifest.NewManifestService(config.ManifestConfig, storageProvider, keyStoreService, didResolver, credentialService, presentationService) + manifestService, err := manifest.NewManifestService(storageProvider, keyStoreService, didResolver, credentialService, presentationService) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the manifest service") } diff --git a/pkg/storage/db_test.go b/pkg/storage/db_test.go index 17151c9ea..72907bc0a 100644 --- a/pkg/storage/db_test.go +++ b/pkg/storage/db_test.go @@ -15,7 +15,8 @@ import ( "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tbd54566975/ssi-service/pkg/encryption" + + "github.com/tbd54566975/ssi-service/internal/encryption" ) func getDBImplementations(t *testing.T) []ServiceStorage { diff --git a/pkg/storage/encrypt.go b/pkg/storage/encrypt.go index 006f6ac13..dbca70699 100644 --- a/pkg/storage/encrypt.go +++ b/pkg/storage/encrypt.go @@ -4,7 +4,8 @@ import ( "context" "github.com/pkg/errors" - "github.com/tbd54566975/ssi-service/pkg/encryption" + + "github.com/tbd54566975/ssi-service/internal/encryption" ) type EncryptedWrapper struct { From 68a3f82e6c0e3032a024d0cba71f96c5c39f7f44 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Wed, 2 Aug 2023 19:13:24 -0400 Subject: [PATCH 14/20] Fixing code vuln (#628) * Fixing code vuln * Upgrading go --- .github/workflows/ci.yml | 4 ++-- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/integration.yml | 2 +- CONTRIBUTING.md | 4 ++-- README.md | 2 +- build/Dockerfile | 2 +- go.mod | 2 +- go.sum | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36c337a27..69ec0c32f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.20.6 + go-version: 1.20.7 cache: true - name: Install Mage @@ -37,7 +37,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.20.6 + go-version: 1.20.7 cache: true - name: Install mage diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 7785f9a28..2196c5f27 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.20.6 + go-version: 1.20.7 cache: true - name: golangci-lint diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 9157f15a2..af3733a4c 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.20.6 + go-version: 1.20.7 cache: true - name: Install Mage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bdfaff28b..ed2829445 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.6 | [go.dev](https://go.dev/doc/install) | +| Go | 1.20.7 | [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) | @@ -24,7 +24,7 @@ You may verify your `go` installation via the terminal: ``` $> go version -go version go1.20.6 darwin/amd64 +go version go1.20.7 darwin/amd64 ``` If you do not have go, we recommend installing it by: diff --git a/README.md b/README.md index 0f0233273..cdd49c3ff 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![godoc ssi-service](https://img.shields.io/badge/godoc-ssi--service-blue)](https://github.com/TBD54566975/ssi-service) -[![go version 1.20.6](https://img.shields.io/badge/go_version-1.20.6-brightgreen)](https://go.dev/) +[![go version 1.20.7](https://img.shields.io/badge/go_version-1.20.7-brightgreen)](https://go.dev/) [![license Apache 2](https://img.shields.io/badge/license-Apache%202-black)](https://github.com/TBD54566975/ssi-service/blob/main/LICENSE) [![issues](https://img.shields.io/github/issues/TBD54566975/ssi-service)](https://github.com/TBD54566975/ssi-service/issues) ![push](https://github.com/TBD54566975/ssi-service/workflows/ssi-service-ci/badge.svg?branch=main&event=push) diff --git a/build/Dockerfile b/build/Dockerfile index 087a51192..d6f1cb943 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20.6-alpine +FROM golang:1.20.7-alpine # Create directory for our app inside the container WORKDIR /app diff --git a/go.mod b/go.mod index a10a782e8..1f51d132e 100644 --- a/go.mod +++ b/go.mod @@ -164,7 +164,7 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/net v0.12.0 // indirect + golang.org/x/net v0.13.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect diff --git a/go.sum b/go.sum index 50cd64ddf..37c0e38ac 100644 --- a/go.sum +++ b/go.sum @@ -626,8 +626,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From d0f76fccb215db2a998bf81fd72127aea5b82c98 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Thu, 3 Aug 2023 11:28:27 -0400 Subject: [PATCH 15/20] Retrieve pagination parameters from the correct location (#627) * Retrieve pagination parameters from the correct location * Renam. --- pkg/server/pagination/pagination.go | 8 +++---- pkg/server/router/did.go | 2 +- pkg/server/router/presentation.go | 2 +- pkg/server/server_did_test.go | 13 +++++------- pkg/server/server_presentation_test.go | 12 +++++------ pkg/server/server_test.go | 29 +++++++------------------- 6 files changed, 24 insertions(+), 42 deletions(-) diff --git a/pkg/server/pagination/pagination.go b/pkg/server/pagination/pagination.go index 7e819f68d..1c9dc0d33 100644 --- a/pkg/server/pagination/pagination.go +++ b/pkg/server/pagination/pagination.go @@ -25,13 +25,13 @@ const ( PageTokenParam = "pageToken" ) -// ParsePaginationParams reads the PageSizeParam and PageTokenParam from the URL parameters and populates the passed in +// ParsePaginationQueryValues reads the PageSizeParam and PageTokenParam from the URL parameters and populates the passed in // pageRequest. The value encoded in PageTokenParam is assumed to be the base64url encoding of a PageToken. It is an // error for the query params to be different from the query params encoded in the PageToken. Any error during the // execution is responded to using the passed in gin.Context. The return value corresponds to whether there was an // error within the function. -func ParsePaginationParams(c *gin.Context, pageRequest *PageRequest) bool { - pageSizeStr := framework.GetParam(c, PageSizeParam) +func ParsePaginationQueryValues(c *gin.Context, pageRequest *PageRequest) bool { + pageSizeStr := framework.GetQueryValue(c, PageSizeParam) if pageSizeStr != nil { pageSize, err := strconv.Atoi(*pageSizeStr) @@ -48,7 +48,7 @@ func ParsePaginationParams(c *gin.Context, pageRequest *PageRequest) bool { pageRequest.PageSize = &pageSize } - queryPageToken := framework.GetParam(c, PageTokenParam) + queryPageToken := framework.GetQueryValue(c, PageTokenParam) if queryPageToken != nil { errMsg := "token value cannot be decoded" tokenData, err := base64.RawURLEncoding.DecodeString(*queryPageToken) diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 352bc9955..1b48a7b6e 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -276,7 +276,7 @@ func (dr DIDRouter) ListDIDsByMethod(c *gin.Context) { Deleted: getIsDeleted, } var pageRequest pagination.PageRequest - if pagination.ParsePaginationParams(c, &pageRequest) { + if pagination.ParsePaginationQueryValues(c, &pageRequest) { return } getDIDsRequest.PageRequest = pageRequest.ToServicePage() diff --git a/pkg/server/router/presentation.go b/pkg/server/router/presentation.go index 5b8fa2b81..80f5c5d3e 100644 --- a/pkg/server/router/presentation.go +++ b/pkg/server/router/presentation.go @@ -405,7 +405,7 @@ func (pr PresentationRouter) ListSubmissions(c *gin.Context) { } var pageRequest pagination.PageRequest - if pagination.ParsePaginationParams(c, &pageRequest) { + if pagination.ParsePaginationQueryValues(c, &pageRequest) { return } diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go index 7cfce51c2..2a2aa4636 100644 --- a/pkg/server/server_did_test.go +++ b/pkg/server/server_did_test.go @@ -514,12 +514,11 @@ func TestDIDAPI(t *testing.T) { w := httptest.NewRecorder() badParams := url.Values{ - "method": []string{"key"}, "pageSize": []string{"1"}, "pageToken": []string{"made up token"}, } req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+badParams.Encode(), nil) - c := newRequestContextWithURLValues(w, req, badParams) + c := newRequestContextWithParams(w, req, map[string]string{"method": "key"}) didService.ListDIDsByMethod(c) assert.Contains(tt, w.Body.String(), "token value cannot be decoded") }) @@ -536,11 +535,10 @@ func TestDIDAPI(t *testing.T) { w := httptest.NewRecorder() params := url.Values{ - "method": []string{"key"}, "pageSize": []string{"1"}, } req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - c := newRequestContextWithURLValues(w, req, params) + c := newRequestContextWithParams(w, req, map[string]string{"method": "key"}) didRouter.ListDIDsByMethod(c) @@ -553,7 +551,7 @@ func TestDIDAPI(t *testing.T) { w = httptest.NewRecorder() params["pageToken"] = []string{listDIDsByMethodResponse.NextPageToken} req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - c = newRequestContextWithURLValues(w, req, params) + c = newRequestContextWithParams(w, req, map[string]string{"method": "key"}) didRouter.ListDIDsByMethod(c) @@ -576,12 +574,11 @@ func TestDIDAPI(t *testing.T) { w := httptest.NewRecorder() params := url.Values{ - "method": []string{"key"}, "pageSize": []string{"1"}, } req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - c := newRequestContextWithURLValues(w, req, params) + c := newRequestContextWithParams(w, req, map[string]string{"method": "key"}) didRouter.ListDIDsByMethod(c) assert.True(tt, util.Is2xxResponse(w.Result().StatusCode)) @@ -595,7 +592,7 @@ func TestDIDAPI(t *testing.T) { params["pageToken"] = []string{listDIDsByMethodResponse.NextPageToken} params["deleted"] = []string{"true"} req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/key?"+params.Encode(), nil) - c = newRequestContextWithURLValues(w, req, params) + c = newRequestContextWithParams(w, req, map[string]string{"method": "key"}) didRouter.ListDIDsByMethod(c) assert.Equal(tt, http.StatusBadRequest, w.Result().StatusCode) assert.Contains(tt, w.Body.String(), "page token must be for the same query") diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go index d379365f3..a20a4d41c 100644 --- a/pkg/server/server_presentation_test.go +++ b/pkg/server/server_presentation_test.go @@ -410,7 +410,7 @@ func TestPresentationAPI(t *testing.T) { "pageSize": []string{"-1"}, } req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions?"+badParams.Encode(), nil) - c := newRequestContextWithURLValues(w, req, badParams) + c := newRequestContext(w, req) pRouter.ListSubmissions(c) assert.Contains(tt, w.Body.String(), "'pageSize' must be greater than 0") @@ -426,7 +426,7 @@ func TestPresentationAPI(t *testing.T) { "pageToken": []string{"made up token"}, } req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions?"+badParams.Encode(), nil) - c := newRequestContextWithURLValues(w, req, badParams) + c := newRequestContext(w, req) pRouter.ListSubmissions(c) assert.Contains(tt, w.Body.String(), "token value cannot be decoded") @@ -467,7 +467,7 @@ func TestPresentationAPI(t *testing.T) { "pageSize": []string{"1"}, } req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions?"+params.Encode(), nil) - c := newRequestContextWithURLValues(w, req, params) + c := newRequestContext(w, req) pRouter.ListSubmissions(c) @@ -480,7 +480,7 @@ func TestPresentationAPI(t *testing.T) { w = httptest.NewRecorder() params["pageToken"] = []string{listSubmissionResponse.NextPageToken} req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions?"+params.Encode(), nil) - c = newRequestContextWithURLValues(w, req, params) + c = newRequestContext(w, req) pRouter.ListSubmissions(c) @@ -526,7 +526,7 @@ func TestPresentationAPI(t *testing.T) { "pageSize": []string{"1"}, } req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions?"+params.Encode(), nil) - c := newRequestContextWithURLValues(w, req, params) + c := newRequestContext(w, req) pRouter.ListSubmissions(c) @@ -540,7 +540,7 @@ func TestPresentationAPI(t *testing.T) { params["pageToken"] = []string{listSubmissionResponse.NextPageToken} params["filter"] = []string{"status=\"pending\""} req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions?"+params.Encode(), nil) - c = newRequestContextWithURLValues(w, req, params) + c = newRequestContext(w, req) pRouter.ListSubmissions(c) assert.Equal(tt, http.StatusBadRequest, w.Result().StatusCode) diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 429b09f5b..00b5fc422 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -5,37 +5,32 @@ import ( "io" "net/http" "net/http/httptest" - "net/url" "os" "testing" "github.com/TBD54566975/ssi-sdk/credential/exchange" - "github.com/gin-gonic/gin" - - "github.com/tbd54566975/ssi-service/internal/util" - "github.com/tbd54566975/ssi-service/pkg/service/issuance" - "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" - "github.com/tbd54566975/ssi-service/pkg/service/webhook" - "github.com/tbd54566975/ssi-service/pkg/testutil" - manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/gin-gonic/gin" "github.com/goccy/go-json" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - credmodel "github.com/tbd54566975/ssi-service/internal/credential" - "github.com/tbd54566975/ssi-service/config" + credmodel "github.com/tbd54566975/ssi-service/internal/credential" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/did" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/issuance" "github.com/tbd54566975/ssi-service/pkg/service/keystore" "github.com/tbd54566975/ssi-service/pkg/service/manifest" + "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" "github.com/tbd54566975/ssi-service/pkg/service/schema" + "github.com/tbd54566975/ssi-service/pkg/service/webhook" "github.com/tbd54566975/ssi-service/pkg/storage" + "github.com/tbd54566975/ssi-service/pkg/testutil" ) const ( @@ -131,16 +126,6 @@ func newRequestContextWithParams(w http.ResponseWriter, req *http.Request, param return c } -func newRequestContextWithURLValues(w http.ResponseWriter, req *http.Request, params url.Values) *gin.Context { - c := newRequestContext(w, req) - for k, vs := range params { - for _, v := range vs { - c.AddParam(k, v) - } - } - return c -} - func getValidCreateManifestRequest(issuerDID, verificationMethodID, schemaID string) router.CreateManifestRequest { return router.CreateManifestRequest{ IssuerDID: issuerDID, From 5710ace1cedf9356a3d34fa030a50787b5d6de78 Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:18:14 -0700 Subject: [PATCH 16/20] Update API Docs (#629) * update api docs * update api docs --------- Co-authored-by: Andres Uribe --- doc/sip/sips/sip9/README.md | 2 +- doc/swagger.yaml | 429 ++++++++++++++----------- pkg/server/router/credential.go | 59 ++-- pkg/server/router/did.go | 33 +- pkg/server/router/did_configuration.go | 7 +- pkg/server/router/health.go | 4 +- pkg/server/router/issuance.go | 20 +- pkg/server/router/keystore.go | 15 +- pkg/server/router/manifest.go | 100 +++--- pkg/server/router/operation.go | 10 +- pkg/server/router/presentation.go | 71 ++-- pkg/server/router/readiness.go | 4 +- pkg/server/router/schema.go | 24 +- pkg/server/router/webhook.go | 45 +-- pkg/service/manifest/model/model.go | 3 +- 15 files changed, 441 insertions(+), 385 deletions(-) diff --git a/doc/sip/sips/sip9/README.md b/doc/sip/sips/sip9/README.md index fa6af85af..18eecc40f 100644 --- a/doc/sip/sips/sip9/README.md +++ b/doc/sip/sips/sip9/README.md @@ -77,7 +77,7 @@ The updates needed in SSI Service are listed below. - Updating the [Create DID Document](https://developer.tbd.website/docs/apis/ssi-service/#tag/DecentralizedIdentityAPI/paths/~1v1~1dids~1%7Bmethod%7D/put) endpoint so callers can communicate that they want to create a non-custodial DID document. - Storing keys in the DB that point to external key material. - Change the response type of the [Create Credential](https://developer.tbd.website/docs/apis/ssi-service/#tag/CredentialAPI/paths/~1v1~1credentials/put) endpoint to return an `Operation` object. This object communicates that SSI Service needs to receive a signed credential payload in order for the credential to be fully issued. -- Change the response type of the [Create Presentation Request](https://developer.tbd.website/docs/apis/ssi-service/#tag/PresentationDefinitionAPI) endpoint to return an `Operation` object similar to the point above. +- Change the response type of the [Create Presentation Request](https://developer.tbd.website/docs/apis/ssi-service/#tag/PresentationDefinitions) endpoint to return an `Operation` object similar to the point above. - Do that for manifest and for schema. - Note that the concept of Credential Application that can be *Reviewed* already exists in our API. This proposed design would be a step before Review is possible. - The `GET` endpoint `v1/signjobs`that returns all pending `SignJob` objects for a given DID. diff --git a/doc/swagger.yaml b/doc/swagger.yaml index f54ff4b02..48e0d0acc 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -1296,8 +1296,8 @@ definitions: description: value of the presentation definition to use. Must be empty if `id` is present. presentationDefinitionId: - description: id of the presentation definition created with PresentationDefinitionAPI. - Must be empty if `value` is present. + description: id of the presentation definition created with the PresentationDefinitions + API. Must be empty if `value` is present. type: string verificationMethodId: description: |- @@ -2246,9 +2246,9 @@ paths: description: OK schema: $ref: '#/definitions/pkg_server_router.GetHealthCheckResponse' - summary: Health Check + summary: Service health check tags: - - HealthCheck + - ServiceInfo /readiness: get: consumes: @@ -2263,19 +2263,18 @@ paths: description: OK schema: $ref: '#/definitions/pkg_server_router.GetReadinessResponse' - summary: Readiness + summary: Check service readiness tags: - - Readiness + - ServiceInfo /v1/credentials: get: consumes: - application/json - description: Checks for the presence of an optional query parameter and calls - the associated filtered get method. Only one optional parameter is allowed - to be specified. + description: |- + Checks for the presence of an optional query parameter and calls the associated filtered get method. + Only one optional parameter is allowed to be specified. parameters: - - description: The issuer id - example: did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp + - description: The issuer id, e.g. did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp in: query name: issuer type: string @@ -2302,13 +2301,13 @@ paths: description: Internal server error schema: type: string - summary: List Credentials + summary: List Verifiable Credentials tags: - - CredentialAPI + - Credentials put: consumes: - application/json - description: Create a verifiable credential + description: Create a Verifiable Credential parameters: - description: request body in: body @@ -2331,14 +2330,14 @@ paths: description: Internal server error schema: type: string - summary: Create Credential + summary: Create a Verifiable Credential tags: - - CredentialAPI + - Credentials /v1/credentials/{id}: delete: consumes: - application/json - description: Delete credential by ID + description: Delete a Verifiable Credential by its ID parameters: - description: ID of the credential to delete in: path @@ -2360,13 +2359,13 @@ paths: description: Internal server error schema: type: string - summary: Delete Credentials + summary: Delete a Verifiable Credential tags: - - CredentialAPI + - Credentials get: consumes: - application/json - description: Get credential by id + description: Get a Verifiable Credential by its ID parameters: - description: ID of the credential within SSI-Service. Must be a UUID. in: path @@ -2388,14 +2387,14 @@ paths: description: Internal server error schema: type: string - summary: Get Credential + summary: Get a Verifiable Credential tags: - - CredentialAPI + - Credentials /v1/credentials/{id}/status: get: consumes: - application/json - description: Get credential status by id + description: Get a Verifiable Credential's status by the credential's ID parameters: - description: ID in: path @@ -2417,14 +2416,19 @@ paths: description: Internal server error schema: type: string - summary: Get Credential Status + summary: Get a Verifiable Credential's status tags: - - CredentialAPI + - Credentials put: consumes: - application/json - description: Update a credential's status + description: Update a Verifiable Credential's status parameters: + - description: ID + in: path + name: id + required: true + type: string - description: request body in: body name: request @@ -2446,14 +2450,14 @@ paths: description: Internal server error schema: type: string - summary: Update Credential Status + summary: Update a Verifiable Credential's status tags: - - CredentialAPI + - Credentials /v1/credentials/batch: put: consumes: - application/json - description: Create a batch of verifiable credentials. + description: Create a batch of Verifiable Credentials. parameters: - description: The batch requests in: body @@ -2476,14 +2480,14 @@ paths: description: Internal server error schema: type: string - summary: Batch Create Credentials + summary: Batch create Credentials tags: - - CredentialAPI + - Credentials /v1/credentials/status/{id}: get: consumes: - application/json - description: Get credential status list by id. + description: Get a credential status list by its ID parameters: - description: ID in: path @@ -2505,15 +2509,15 @@ paths: description: Internal server error schema: type: string - summary: Get Credential Status List + summary: Get a Credential Status List tags: - - CredentialAPI + - Credentials /v1/credentials/verification: put: consumes: - application/json description: |- - Verify a given credential by its id. The system does the following levels of verification: + Verify a given Verifiable Credential by its ID. The system does the following levels of verification: 1. Makes sure the credential has a valid signature 2. Makes sure the credential has is not expired 3. Makes sure the credential complies with the VC Data Model @@ -2540,9 +2544,9 @@ paths: description: Internal server error schema: type: string - summary: Verify Credential + summary: Verify a Verifiable Credential tags: - - CredentialAPI + - Credentials /v1/did-configurations: put: consumes: @@ -2572,9 +2576,9 @@ paths: description: Internal server error schema: type: string - summary: Create DIDConfiguration + summary: Create DID Configurations tags: - - DIDConfigurationAPI + - DIDConfigurations /v1/did-configurations/verification: put: consumes: @@ -2604,7 +2608,7 @@ paths: type: string summary: Verifies a DID Configuration Resource tags: - - DIDConfigurationAPI + - DIDConfigurations /v1/dids: get: consumes: @@ -2617,15 +2621,16 @@ paths: description: OK schema: $ref: '#/definitions/pkg_server_router.ListDIDMethodsResponse' - summary: List DID Methods + summary: List DID methods tags: - - DecentralizedIdentityAPI + - DecentralizedIdentifiers /v1/dids/{method}: get: consumes: - application/json - description: List DIDs by method. Checks for an optional "deleted=true" query - parameter, which exclusively returns DIDs that have been "Soft Deleted". + description: |- + List DIDs by method. Checks for an optional "deleted=true" query parameter, which exclusively + returns DIDs that have been "Soft Deleted". parameters: - description: Method must be one returned by GET /v1/dids in: path @@ -2662,9 +2667,9 @@ paths: description: Internal server error schema: type: string - summary: List DIDs + summary: List DIDs by method tags: - - DecentralizedIdentityAPI + - DecentralizedIdentifiers put: consumes: - application/json @@ -2705,9 +2710,9 @@ paths: description: Internal server error schema: type: string - summary: Create DID Document + summary: Create a DID Document tags: - - DecentralizedIdentityAPI + - DecentralizedIdentifiers /v1/dids/{method}/{id}: delete: consumes: @@ -2716,7 +2721,7 @@ paths: When this is called with the correct did method and id it will flip the softDelete flag to true for the db entry. A user can still get the did if they know the DID ID, and the did keys will still exist, but this did will not show up in the ListDIDsByMethod call This facilitates a clean SSI-Service Admin UI but not leave any hanging VCs with inaccessible hanging DIDs. - Soft Deletes DID by method + Soft deletes a DID by its method parameters: - description: Method in: path @@ -2743,13 +2748,13 @@ paths: description: Internal server error schema: type: string - summary: Soft Delete DID + summary: Soft delete a DID tags: - - DecentralizedIdentityAPI + - DecentralizedIdentifiers get: consumes: - application/json - description: Get DID by method + description: Gets a DID Document by its DID ID parameters: - description: request body in: body @@ -2778,15 +2783,15 @@ paths: description: Bad request schema: type: string - summary: Get DID + summary: Get a DID tags: - - DecentralizedIdentityAPI + - DecentralizedIdentifiers /v1/dids/{method}/batch: put: consumes: - application/json description: |- - Create a batch of verifiable credentials. The operation is atomic, meaning that all requests will + Create a batch of DIDs. The operation is atomic, meaning that all requests will succeed or fail. This is currently only supported for the DID method named `did:key`. parameters: - description: Method. Only `key` is supported. @@ -2817,7 +2822,7 @@ paths: type: string summary: Batch Create DIDs tags: - - DecentralizedIdentityAPI + - DecentralizedIdentifiers /v1/dids/resolver/{id}: get: consumes: @@ -2842,12 +2847,12 @@ paths: type: string summary: Resolve a DID tags: - - DecentralizedIdentityAPI + - DecentralizedIdentifiers /v1/issuancetemplates: put: consumes: - application/json - description: Create issuance template + description: Creates an issuance template parameters: - description: request body in: body @@ -2870,14 +2875,14 @@ paths: description: Internal server error schema: type: string - summary: Create issuance template + summary: Create an issuance template tags: - - IssuanceAPI + - IssuanceTemplates /v1/issuancetemplates/{id}: delete: consumes: - application/json - description: Delete issuance template by ID + description: Delete an issuance template by its ID parameters: - description: ID in: path @@ -2899,13 +2904,13 @@ paths: description: Internal server error schema: type: string - summary: Delete issuance template + summary: Delete an issuance template tags: - - IssuanceAPI + - IssuanceTemplates get: consumes: - application/json - description: Get an issuance template by its id + description: Get an issuance template by its ID parameters: - description: ID in: path @@ -2923,9 +2928,9 @@ paths: description: Bad request schema: type: string - summary: Get issuance template + summary: Get an issuance template tags: - - IssuanceAPI + - IssuanceTemplates /v1/keys: put: consumes: @@ -2951,16 +2956,16 @@ paths: description: Internal server error schema: type: string - summary: Store Key + summary: Store a keys tags: - - KeyStoreAPI + - KeyStore /v1/keys/{id}: delete: consumes: - application/json - description: 'Marks the stored key as being revoked, along with the timestamps - of when it was revoked. NB: the key can still be used for signing. This will - likely be addressed before v1 is released.' + description: |- + Marks a key as being revoked, along with the timestamps of when it was revoked. + NB: the key can still be used for signing. This will likely be addressed before v1 is released. parameters: - description: ID of the key to revoke in: path @@ -2982,9 +2987,9 @@ paths: description: Internal server error schema: type: string - summary: Revoke Key + summary: Revoke a key tags: - - KeyStoreAPI + - KeyStore get: consumes: - application/json @@ -3006,15 +3011,15 @@ paths: description: Bad request schema: type: string - summary: Get Details For Key + summary: Get details for a key tags: - - KeyStoreAPI + - KeyStore /v1/manifests: get: consumes: - application/json description: Checks for the presence of a query parameter and calls the associated - filtered get method + filtered get method for Credential Manifests parameters: - description: string issuer in: query @@ -3043,13 +3048,14 @@ paths: description: Internal server error schema: type: string - summary: List manifests + summary: List Credential Manifests tags: - - ManifestAPI + - Manifests put: consumes: - application/json - description: Create manifest. Most fields map to the definitions from https://identity.foundation/credential-manifest/#general-composition. + description: Create a Credential Manifest. Most fields map to the definitions + from https://identity.foundation/credential-manifest/#general-composition. parameters: - description: request body in: body @@ -3072,14 +3078,14 @@ paths: description: Internal server error schema: type: string - summary: Create manifest + summary: Create a Credential Manifest tags: - - ManifestAPI + - Manifests /v1/manifests/{id}: delete: consumes: - application/json - description: Delete manifest by ID + description: Delete a Credential Manifest by its ID parameters: - description: ID in: path @@ -3101,13 +3107,13 @@ paths: description: Internal server error schema: type: string - summary: Delete manifests + summary: Delete a Credential Manifests tags: - - ManifestAPI + - Manifests get: consumes: - application/json - description: Get a credential manifest by its id + description: Get a Credential Manifest by its ID parameters: - description: ID in: path @@ -3125,14 +3131,14 @@ paths: description: Bad request schema: type: string - summary: Get manifest + summary: Get a Credential Manifest tags: - - ManifestAPI + - Manifests /v1/manifests/applications: get: consumes: - application/json - description: List all the existing applications. + description: List all the existing Credential Applications. produces: - application/json responses: @@ -3144,15 +3150,16 @@ paths: description: Internal server error schema: type: string - summary: List applications + summary: List Credential Applications tags: - - ApplicationAPI + - ManifestApplications put: consumes: - application/json description: |- - Submit a credential application in response to a credential manifest. The request body is expected to - be a valid JWT signed by the applicant's DID, containing two top level properties: `credential_application` and `vcs`. + Submit a Credential Application in response to a Credential Manifest request. The request body is expected to + be a valid JWT signed by the applicant's DID, containing two top level properties: `credential_application` and `vcs` + according to the spec https://identity.foundation/credential-manifest/#credential-application parameters: - description: request body in: body @@ -3176,14 +3183,14 @@ paths: description: Internal server error schema: type: string - summary: Submit application + summary: Submit a Credential Application tags: - - ApplicationAPI + - ManifestApplications /v1/manifests/applications/{id}: delete: consumes: - application/json - description: Delete application by ID + description: Delete a Credential Application by its ID parameters: - description: ID in: path @@ -3205,13 +3212,13 @@ paths: description: Internal server error schema: type: string - summary: Delete applications + summary: Delete Credential Applications tags: - - ApplicationAPI + - ManifestApplications get: consumes: - application/json - description: Get application by id + description: Get a Credential Application by its ID parameters: - description: ID in: path @@ -3229,15 +3236,22 @@ paths: description: Bad request schema: type: string - summary: Get application + summary: Get a Credential Application tags: - - ApplicationAPI + - ManifestApplications /v1/manifests/applications/{id}/review: put: consumes: - application/json - description: Reviewing an application either fulfills or denies the credential. + description: |- + Reviewing a Credential Application either fulfills or denies the credential(s) issuance according + to the spec https://identity.foundation/credential-manifest/#credential-application. parameters: + - description: ID + in: path + name: id + required: true + type: string - description: request body in: body name: request @@ -3259,14 +3273,14 @@ paths: description: Internal server error schema: type: string - summary: Reviews an application + summary: Review a Credential Application tags: - - ApplicationAPI + - ManifestApplications /v1/manifests/requests: get: consumes: - application/json - description: Lists all the existing credential manifest requests + description: Lists all the existing Credential Manifest requests produces: - application/json responses: @@ -3280,11 +3294,12 @@ paths: type: string summary: List Credential Manifest Requests tags: - - ManifestAPI + - ManifestRequests put: consumes: - application/json - description: Create manifest request from an existing credential manifest. + description: Create a Credential Manifest Request from an existing Credential + Manifest. parameters: - description: request body in: body @@ -3307,14 +3322,14 @@ paths: description: Internal server error schema: type: string - summary: Create Manifest Request Request + summary: Create a Credential Manifest Request tags: - - ManifestAPI + - ManifestRequests /v1/manifests/requests/{id}: delete: consumes: - application/json - description: Delete a manifest request by its ID + description: Delete a Credential Manifest Request by its ID parameters: - description: ID in: path @@ -3336,13 +3351,13 @@ paths: description: Internal server error schema: type: string - summary: Delete Manifest Request + summary: Delete a Credential Manifest Request tags: - - ManifestAPI + - ManifestRequests get: consumes: - application/json - description: Get a manifest request by its ID + description: Get a Credential Manifest Request by its ID parameters: - description: ID in: path @@ -3360,14 +3375,15 @@ paths: description: Bad request schema: type: string - summary: Get Manifest Request + summary: Get a Credential Manifest Request tags: - - ManifestAPI + - ManifestRequests /v1/manifests/responses: get: consumes: - application/json - description: Lists all responses + description: Lists all responses to Credential Applications associated with + a Credential Manifest produces: - application/json responses: @@ -3379,14 +3395,14 @@ paths: description: Internal server error schema: type: string - summary: List responses + summary: List Credential Manifest Responses tags: - - ResponseAPI + - ManifestResponses /v1/manifests/responses/{id}: delete: consumes: - application/json - description: Delete response by ID + description: Delete a Credential Manifest Response by its ID parameters: - description: ID in: path @@ -3408,13 +3424,13 @@ paths: description: Internal server error schema: type: string - summary: Delete responses + summary: Delete a Credential Manifest Response tags: - - ResponseAPI + - ManifestResponses get: consumes: - application/json - description: Get response by id + description: Get a Credential Manifest Response by its ID https://identity.foundation/credential-manifest/#credential-response parameters: - description: ID in: path @@ -3436,9 +3452,9 @@ paths: description: Internal server error schema: type: string - summary: Get response + summary: Get a Credential Manifest Response tags: - - ResponseAPI + - ManifestResponses /v1/operations: get: consumes: @@ -3471,7 +3487,7 @@ paths: type: string summary: List operations tags: - - OperationAPI + - Operations /v1/operations/{id}: get: consumes: @@ -3500,12 +3516,12 @@ paths: type: string summary: Get an operation tags: - - OperationAPI + - Operations /v1/operations/cancel/{id}: get: consumes: - application/json - description: Cancels an ongoing operation, if possible. + description: Cancels an active operation, if possible. parameters: - description: ID in: path @@ -3527,14 +3543,14 @@ paths: description: Internal server error schema: type: string - summary: Cancel an ongoing operation + summary: Cancel an operation tags: - - OperationAPI + - Operations /v1/presentations/definitions: get: consumes: - application/json - description: Lists all the existing presentation definitions + description: Lists all the existing Presentation Definitions produces: - application/json responses: @@ -3552,11 +3568,11 @@ paths: type: string summary: List Presentation Definitions tags: - - PresentationDefinitionAPI + - Presentations put: consumes: - application/json - description: Create presentation definition + description: Create a Presentation Definition https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition parameters: - description: request body in: body @@ -3579,14 +3595,14 @@ paths: description: Internal server error schema: type: string - summary: Create PresentationDefinition + summary: Create a Presentation Definition tags: - - PresentationDefinitionAPI + - Presentations /v1/presentations/definitions/{id}: delete: consumes: - application/json - description: Delete a presentation definition by its ID + description: Delete a Presentation Definition by its ID parameters: - description: ID in: path @@ -3608,13 +3624,13 @@ paths: description: Internal server error schema: type: string - summary: Delete PresentationDefinition + summary: Delete a Presentation Definition tags: - - PresentationDefinitionAPI + - Presentations get: consumes: - application/json - description: Get a presentation definition by its ID + description: Get a Presentation Definition by its ID parameters: - description: ID in: path @@ -3632,14 +3648,14 @@ paths: description: Bad request schema: type: string - summary: Get PresentationDefinition + summary: Get a Presentation Definition tags: - - PresentationDefinitionAPI + - Presentations /v1/presentations/requests: get: consumes: - application/json - description: Lists all the existing presentation requests + description: Lists all the existing Presentation Requests produces: - application/json responses: @@ -3653,11 +3669,13 @@ paths: type: string summary: List Presentation Requests tags: - - PresentationRequestAPI + - PresentationRequests put: consumes: - application/json - description: Create presentation request from an existing presentation definition. + description: |- + Create a Presentation Request from an existing Presentation Definition with an existing DID according + to the spec https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-request parameters: - description: request body in: body @@ -3680,14 +3698,14 @@ paths: description: Internal server error schema: type: string - summary: Create Presentation Request + summary: Create a Presentation Request tags: - - PresentationRequestAPI + - PresentationRequests /v1/presentations/requests/{id}: delete: consumes: - application/json - description: Delete a presentation request by its ID + description: Delete a Presentation Request by its ID parameters: - description: ID in: path @@ -3709,13 +3727,13 @@ paths: description: Internal server error schema: type: string - summary: Delete PresentationRequest + summary: Delete a Presentation Request tags: - - PresentationRequestAPI + - PresentationRequests get: consumes: - application/json - description: Get a presentation request by its ID + description: Get a Presentation Request by its ID parameters: - description: ID in: path @@ -3733,15 +3751,15 @@ paths: description: Bad request schema: type: string - summary: Get Presentation Request + summary: Get a Presentation Request tags: - - PresentationRequestAPI + - PresentationRequests /v1/presentations/submissions: get: consumes: - application/json - description: List existing submissions according to a filtering query. The `filter` - field follows the syntax described in https://google.aip.dev/160. + description: List existing Presentation Submissions according to a filtering + query. The `filter` field follows the syntax described in https://google.aip.dev/160. parameters: - description: 'A standard filter expression conforming to https://google.aip.dev/160. For example: `?filter=status=' @@ -3773,13 +3791,14 @@ paths: description: Internal server error schema: type: string - summary: List Submissions + summary: List Presentation Submissions tags: - - PresentationSubmissionAPI + - PresentationSubmissions put: consumes: - application/json - description: Creates a submission in this server ready to be reviewed. + description: Accepts a Presentation Submission (https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) + in this server ready to be reviewed. parameters: - description: request body in: body @@ -3802,14 +3821,14 @@ paths: description: Internal server error schema: type: string - summary: Create Submission + summary: Create a Presentation Submission tags: - - PresentationSubmissionAPI + - PresentationSubmissions /v1/presentations/submissions/{id}: get: consumes: - application/json - description: Get a submission by its ID + description: Get a Presentation Submission by its ID parameters: - description: ID in: path @@ -3827,17 +3846,22 @@ paths: description: Bad request schema: type: string - summary: Get Submission + summary: Get a Presentation Submission tags: - - PresentationSubmissionAPI + - PresentationSubmissions /v1/presentations/submissions/{id}/review: put: consumes: - application/json - description: Reviews a pending submission. After this method is called, the - operation with `id==presentations/submissions/{submission_id}` will be updated - with the result of this invocation. + description: |- + Reviews a pending Presentation Submission. After this method is called, the operation with + `id==presentations/submissions/{submission_id}` will be updated with the result of this invocation. parameters: + - description: ID + in: path + name: id + required: true + type: string - description: request body in: body name: request @@ -3859,14 +3883,14 @@ paths: description: Internal server error schema: type: string - summary: Review a pending submission + summary: Review a pending Presentation Submission tags: - - PresentationSubmissionAPI + - PresentationSubmissions /v1/schemas: get: consumes: - application/json - description: List schemas + description: List Credential Schemas stored by the service produces: - application/json responses: @@ -3878,13 +3902,13 @@ paths: description: Internal server error schema: type: string - summary: List Schemas + summary: List Credential Schemas tags: - - SchemaAPI + - Schemas put: consumes: - application/json - description: Create schema + description: Create a schema for use with a Verifiable Credential parameters: - description: request body in: body @@ -3907,14 +3931,14 @@ paths: description: Internal server error schema: type: string - summary: Create Schema + summary: Create a Credential Schema tags: - - SchemaAPI + - Schemas /v1/schemas/{id}: delete: consumes: - application/json - description: Delete a schema by its ID + description: Delete a Credential Schema by its ID parameters: - description: ID in: path @@ -3936,13 +3960,13 @@ paths: description: Internal server error schema: type: string - summary: Delete Schema + summary: Delete a Credential Schema tags: - - SchemaAPI + - Schemas get: consumes: - application/json - description: Get a schema by its ID + description: Get a Credential Schema by its ID parameters: - description: ID in: path @@ -3960,14 +3984,14 @@ paths: description: Bad request schema: type: string - summary: Get Schema + summary: Get a Credential Schema tags: - - SchemaAPI + - Schemas /v1/webhooks: get: consumes: - application/json - description: Lists all webhooks + description: Lists all webhooks stored by the service produces: - application/json responses: @@ -3979,13 +4003,13 @@ paths: description: Internal server error schema: type: string - summary: List Webhooks + summary: List webhooks tags: - - WebhookAPI + - Webhooks put: consumes: - application/json - description: Create webhook + description: Creates a webhook parameters: - description: request body in: body @@ -4008,18 +4032,23 @@ paths: description: Internal server error schema: type: string - summary: Create Webhook + summary: Create a webhook tags: - - WebhookAPI + - Webhooks /v1/webhooks/{noun}/{verb}: get: consumes: - application/json description: Get a webhook by its ID parameters: - - description: ID + - description: noun in: path - name: id + name: noun + required: true + type: string + - description: verb + in: path + name: verb required: true type: string produces: @@ -4033,18 +4062,28 @@ paths: description: Bad request schema: type: string - summary: Get Webhook + summary: Get a webhook tags: - - WebhookAPI + - Webhooks /v1/webhooks/{noun}/{verb}/{url}: delete: consumes: - application/json description: Delete a webhook by its ID parameters: - - description: ID + - description: noun in: path - name: id + name: noun + required: true + type: string + - description: verb + in: path + name: verb + required: true + type: string + - description: url + in: path + name: url required: true type: string produces: @@ -4062,9 +4101,9 @@ paths: description: Internal server error schema: type: string - summary: Delete Webhook + summary: Delete a webhook tags: - - WebhookAPI + - Webhooks /v1/webhooks/nouns: get: consumes: @@ -4077,9 +4116,9 @@ paths: description: OK schema: $ref: '#/definitions/github_com_tbd54566975_ssi-service_pkg_service_webhook.GetSupportedNounsResponse' - summary: Get Supported Nouns + summary: Get supported webhook nouns tags: - - WebhookAPI + - Webhooks /v1/webhooks/verbs: get: consumes: @@ -4092,7 +4131,7 @@ paths: description: OK schema: $ref: '#/definitions/github_com_tbd54566975_ssi-service_pkg_service_webhook.GetSupportedVerbsResponse' - summary: Get Supported Verbs + summary: Get supported webhook verbs tags: - - WebhookAPI + - Webhooks swagger: "2.0" diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go index 82fe29c90..312d652c2 100644 --- a/pkg/server/router/credential.go +++ b/pkg/server/router/credential.go @@ -8,6 +8,7 @@ import ( "github.com/TBD54566975/ssi-sdk/did" "github.com/gin-gonic/gin" "github.com/pkg/errors" + credmodel "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" @@ -57,9 +58,9 @@ type BatchCreateCredentialsResponse struct { // BatchCreateCredentials godoc // -// @Summary Batch Create Credentials -// @Description Create a batch of verifiable credentials. -// @Tags CredentialAPI +// @Summary Batch create Credentials +// @Description Create a batch of Verifiable Credentials. +// @Tags Credentials // @Accept json // @Produce json // @Param request body BatchCreateCredentialsRequest true "The batch requests" @@ -155,9 +156,9 @@ type CreateCredentialResponse struct { // CreateCredential godoc // -// @Summary Create Credential -// @Description Create a verifiable credential -// @Tags CredentialAPI +// @Summary Create a Verifiable Credential +// @Description Create a Verifiable Credential +// @Tags Credentials // @Accept json // @Produce json // @Param request body CreateCredentialRequest true "request body" @@ -198,9 +199,9 @@ type GetCredentialResponse struct { // GetCredential godoc // -// @Summary Get Credential -// @Description Get credential by id -// @Tags CredentialAPI +// @Summary Get a Verifiable Credential +// @Description Get a Verifiable Credential by its ID +// @Tags Credentials // @Accept json // @Produce json // @Param id path string true "ID of the credential within SSI-Service. Must be a UUID." @@ -239,9 +240,9 @@ type GetCredentialStatusResponse struct { // GetCredentialStatus godoc // -// @Summary Get Credential Status -// @Description Get credential status by id -// @Tags CredentialAPI +// @Summary Get a Verifiable Credential's status +// @Description Get a Verifiable Credential's status by the credential's ID +// @Tags Credentials // @Accept json // @Produce json // @Param id path string true "ID" @@ -283,9 +284,9 @@ type GetCredentialStatusListResponse struct { // GetCredentialStatusList godoc // -// @Summary Get Credential Status List -// @Description Get credential status list by id. -// @Tags CredentialAPI +// @Summary Get a Credential Status List +// @Description Get a credential status list by its ID +// @Tags Credentials // @Accept json // @Produce json // @Param id path string true "ID" @@ -340,11 +341,12 @@ type UpdateCredentialStatusResponse struct { // UpdateCredentialStatus godoc // -// @Summary Update Credential Status -// @Description Update a credential's status -// @Tags CredentialAPI +// @Summary Update a Verifiable Credential's status +// @Description Update a Verifiable Credential's status +// @Tags Credentials // @Accept json // @Produce json +// @Param id path string true "ID" // @Param request body UpdateCredentialStatusRequest true "request body" // @Success 201 {object} UpdateCredentialStatusResponse // @Failure 400 {string} string "Bad request" @@ -412,13 +414,13 @@ type VerifyCredentialResponse struct { // VerifyCredential godoc // -// @Summary Verify Credential -// @Description Verify a given credential by its id. The system does the following levels of verification: +// @Summary Verify a Verifiable Credential +// @Description Verify a given Verifiable Credential by its ID. The system does the following levels of verification: // @Description 1. Makes sure the credential has a valid signature // @Description 2. Makes sure the credential has is not expired // @Description 3. Makes sure the credential complies with the VC Data Model // @Description 4. If the credential has a schema, makes sure its data complies with the schema -// @Tags CredentialAPI +// @Tags Credentials // @Accept json // @Produce json // @Param request body VerifyCredentialRequest true "request body" @@ -461,12 +463,13 @@ type ListCredentialsResponse struct { // ListCredentials godoc // -// @Summary List Credentials -// @Description Checks for the presence of an optional query parameter and calls the associated filtered get method. Only one optional parameter is allowed to be specified. -// @Tags CredentialAPI +// @Summary List Verifiable Credentials +// @Description Checks for the presence of an optional query parameter and calls the associated filtered get method. +// @Description Only one optional parameter is allowed to be specified. +// @Tags Credentials // @Accept json // @Produce json -// @Param issuer query string false "The issuer id" example(did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp) +// @Param issuer query string false "The issuer id, e.g. did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" // @Param schema query string false "The credentialSchema.id value to filter by" // @Param subject query string false "The credentialSubject.id value to filter by" // @Success 200 {object} ListCredentialsResponse @@ -557,9 +560,9 @@ func (cr CredentialRouter) listCredentialsBySchema(c *gin.Context, schema string // DeleteCredential godoc // -// @Summary Delete Credentials -// @Description Delete credential by ID -// @Tags CredentialAPI +// @Summary Delete a Verifiable Credential +// @Description Delete a Verifiable Credential by its ID +// @Tags Credentials // @Accept json // @Produce json // @Param id path string true "ID of the credential to delete" diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 1b48a7b6e..28cddeeab 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -48,9 +48,9 @@ type ListDIDMethodsResponse struct { // ListDIDMethods godoc // -// @Summary List DID Methods +// @Summary List DID methods // @Description Get the list of supported DID methods -// @Tags DecentralizedIdentityAPI +// @Tags DecentralizedIdentifiers // @Accept json // @Produce json // @Success 200 {object} ListDIDMethodsResponse @@ -75,12 +75,12 @@ type CreateDIDByMethodResponse struct { // CreateDIDByMethod godoc // -// @Summary Create DID Document +// @Summary Create a DID Document // @Description Creates a fully custodial DID document with the given method. The document created is stored internally // @Description and can be retrieved using the GetOperation. Method dependent registration (for example, DID web // @Description registration) is left up to the clients of this API. The private key(s) created by the method are stored // @Description internally never leave the service boundary. -// @Tags DecentralizedIdentityAPI +// @Tags DecentralizedIdentifiers // @Accept json // @Produce json // @Param method path string true "Method" @@ -183,9 +183,9 @@ type GetDIDByMethodResponse struct { // GetDIDByMethod godoc // -// @Summary Get DID -// @Description Get DID by method -// @Tags DecentralizedIdentityAPI +// @Summary Get a DID +// @Description Gets a DID Document by its DID ID +// @Tags DecentralizedIdentifiers // @Accept json // @Produce json // @Param request body CreateDIDByMethodRequest true "request body" @@ -237,9 +237,10 @@ type GetDIDsRequest struct { // ListDIDsByMethod godoc // -// @Summary List DIDs -// @Description List DIDs by method. Checks for an optional "deleted=true" query parameter, which exclusively returns DIDs that have been "Soft Deleted". -// @Tags DecentralizedIdentityAPI +// @Summary List DIDs by method +// @Description List DIDs by method. Checks for an optional "deleted=true" query parameter, which exclusively +// @Description returns DIDs that have been "Soft Deleted". +// @Tags DecentralizedIdentifiers // @Accept json // @Produce json // @Param method path string true "Method must be one returned by GET /v1/dids" @@ -308,9 +309,9 @@ type ResolveDIDResponse struct { // @Description When this is called with the correct did method and id it will flip the softDelete flag to true for the db entry. // @Description A user can still get the did if they know the DID ID, and the did keys will still exist, but this did will not show up in the ListDIDsByMethod call // @Description This facilitates a clean SSI-Service Admin UI but not leave any hanging VCs with inaccessible hanging DIDs. -// @Summary Soft Delete DID -// @Description Soft Deletes DID by method -// @Tags DecentralizedIdentityAPI +// @Summary Soft delete a DID +// @Description Soft deletes a DID by its method +// @Tags DecentralizedIdentifiers // @Accept json // @Produce json // @Param method path string true "Method" @@ -347,7 +348,7 @@ func (dr DIDRouter) SoftDeleteDIDByMethod(c *gin.Context) { // // @Summary Resolve a DID // @Description Resolve a DID that may not be stored in this service -// @Tags DecentralizedIdentityAPI +// @Tags DecentralizedIdentifiers // @Accept json // @Produce json // @Param id path string true "ID" @@ -407,9 +408,9 @@ func NewBatchDIDRouter(svc *did.BatchService) *BatchDIDRouter { // BatchCreateDIDs godoc // // @Summary Batch Create DIDs -// @Description Create a batch of verifiable credentials. The operation is atomic, meaning that all requests will +// @Description Create a batch of DIDs. The operation is atomic, meaning that all requests will // @Description succeed or fail. This is currently only supported for the DID method named `did:key`. -// @Tags DecentralizedIdentityAPI +// @Tags DecentralizedIdentifiers // @Accept json // @Produce json // @Param method path string true "Method. Only `key` is supported." diff --git a/pkg/server/router/did_configuration.go b/pkg/server/router/did_configuration.go index d9824385e..2f0078026 100644 --- a/pkg/server/router/did_configuration.go +++ b/pkg/server/router/did_configuration.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/pkg/server/framework" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" @@ -65,10 +66,10 @@ type CreateDIDConfigurationResponse struct { // CreateDIDConfiguration godoc // -// @Summary Create DIDConfiguration +// @Summary Create DID Configurations // @Description Creates a DID Configuration Resource which conforms to https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource // @Description The `didConfiguration` can be hosted at the `wellKnownLocation` specified in the response. -// @Tags DIDConfigurationAPI +// @Tags DIDConfigurations // @Accept json // @Produce json // @Param request body CreateDIDConfigurationRequest true "request body" @@ -113,7 +114,7 @@ func (wr DIDConfigurationRouter) CreateDIDConfiguration(c *gin.Context) { // // @Summary Verifies a DID Configuration Resource // @Description Verifies a DID Configuration Resource according to https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification -// @Tags DIDConfigurationAPI +// @Tags DIDConfigurations // @Accept json // @Produce json // @Param request body wellknown.VerifyDIDConfigurationRequest true "request body" diff --git a/pkg/server/router/health.go b/pkg/server/router/health.go index 84c370f71..852016fe6 100644 --- a/pkg/server/router/health.go +++ b/pkg/server/router/health.go @@ -19,9 +19,9 @@ const ( // Health godoc // -// @Summary Health Check +// @Summary Service health check // @Description Health is a simple handler that always responds with a 200 OK -// @Tags HealthCheck +// @Tags ServiceInfo // @Accept json // @Produce json // @Success 200 {object} GetHealthCheckResponse diff --git a/pkg/server/router/issuance.go b/pkg/server/router/issuance.go index 8ae65a630..82380103e 100644 --- a/pkg/server/router/issuance.go +++ b/pkg/server/router/issuance.go @@ -26,9 +26,9 @@ func NewIssuanceRouter(svc svcframework.Service) (*IssuanceRouter, error) { // GetIssuanceTemplate godoc // -// @Summary Get issuance template -// @Description Get an issuance template by its id -// @Tags IssuanceAPI +// @Summary Get an issuance template +// @Description Get an issuance template by its ID +// @Tags IssuanceTemplates // @Accept json // @Produce json // @Param id path string true "ID" @@ -62,9 +62,9 @@ func (r CreateIssuanceTemplateRequest) toServiceRequest() *issuance.CreateIssuan // CreateIssuanceTemplate godoc // -// @Summary Create issuance template -// @Description Create issuance template -// @Tags IssuanceAPI +// @Summary Create an issuance template +// @Description Creates an issuance template +// @Tags IssuanceTemplates // @Accept json // @Produce json // @Param request body CreateIssuanceTemplateRequest true "request body" @@ -92,9 +92,9 @@ func (ir IssuanceRouter) CreateIssuanceTemplate(c *gin.Context) { // DeleteIssuanceTemplate godoc // -// @Summary Delete issuance template -// @Description Delete issuance template by ID -// @Tags IssuanceAPI +// @Summary Delete an issuance template +// @Description Delete an issuance template by its ID +// @Tags IssuanceTemplates // @Accept json // @Produce json // @Param id path string true "ID" @@ -127,7 +127,7 @@ type ListIssuanceTemplatesResponse struct { // // @Summary Lists issuance templates // @Description Lists all issuance templates stored in this service. -// @Tags IssuanceAPI +// @Tags Issuance // @Accept json // @Produce json // @Success 200 {object} ListIssuanceTemplatesResponse diff --git a/pkg/server/router/keystore.go b/pkg/server/router/keystore.go index c6883f5cc..11147dd5d 100644 --- a/pkg/server/router/keystore.go +++ b/pkg/server/router/keystore.go @@ -66,9 +66,9 @@ func (sk StoreKeyRequest) ToServiceRequest() (*keystore.StoreKeyRequest, error) // StoreKey godoc // -// @Summary Store Key +// @Summary Store a keys // @Description Stores a key to be used by the service -// @Tags KeyStoreAPI +// @Tags KeyStore // @Accept json // @Produce json // @Param request body StoreKeyRequest true "request body" @@ -115,9 +115,9 @@ type GetKeyDetailsResponse struct { // GetKeyDetails godoc // -// @Summary Get Details For Key +// @Summary Get details for a key // @Description Get details about a stored key -// @Tags KeyStoreAPI +// @Tags KeyStore // @Accept json // @Produce json // @Param id path string true "ID of the key to get" @@ -155,9 +155,10 @@ type RevokeKeyResponse struct { // RevokeKey godoc // -// @Summary Revoke Key -// @Description Marks the stored key as being revoked, along with the timestamps of when it was revoked. NB: the key can still be used for signing. This will likely be addressed before v1 is released. -// @Tags KeyStoreAPI +// @Summary Revoke a key +// @Description Marks a key as being revoked, along with the timestamps of when it was revoked. +// @Description NB: the key can still be used for signing. This will likely be addressed before v1 is released. +// @Tags KeyStore // @Accept json // @Produce json // @Param id path string true "ID of the key to revoke" diff --git a/pkg/server/router/manifest.go b/pkg/server/router/manifest.go index bf64b9984..a5d75641d 100644 --- a/pkg/server/router/manifest.go +++ b/pkg/server/router/manifest.go @@ -10,6 +10,7 @@ import ( "github.com/TBD54566975/ssi-sdk/did" "github.com/gin-gonic/gin" "github.com/goccy/go-json" + "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" @@ -101,9 +102,9 @@ type CreateManifestResponse struct { // CreateManifest godoc // -// @Summary Create manifest -// @Description Create manifest. Most fields map to the definitions from https://identity.foundation/credential-manifest/#general-composition. -// @Tags ManifestAPI +// @Summary Create a Credential Manifest +// @Description Create a Credential Manifest. Most fields map to the definitions from https://identity.foundation/credential-manifest/#general-composition. +// @Tags Manifests // @Accept json // @Produce json // @Param request body CreateManifestRequest true "request body" @@ -144,9 +145,9 @@ type ListManifestResponse struct { // GetManifest godoc // -// @Summary Get manifest -// @Description Get a credential manifest by its id -// @Tags ManifestAPI +// @Summary Get a Credential Manifest +// @Description Get a Credential Manifest by its ID +// @Tags Manifests // @Accept json // @Produce json // @Param id path string true "ID" @@ -181,9 +182,9 @@ type ListManifestsResponse struct { // ListManifests godoc // -// @Summary List manifests -// @Description Checks for the presence of a query parameter and calls the associated filtered get method -// @Tags ManifestAPI +// @Summary List Credential Manifests +// @Description Checks for the presence of a query parameter and calls the associated filtered get method for Credential Manifests +// @Tags Manifests // @Accept json // @Produce json // @Param issuer query string false "string issuer" @@ -215,9 +216,9 @@ func (mr ManifestRouter) ListManifests(c *gin.Context) { // DeleteManifest godoc // -// @Summary Delete manifests -// @Description Delete manifest by ID -// @Tags ManifestAPI +// @Summary Delete a Credential Manifests +// @Description Delete a Credential Manifest by its ID +// @Tags Manifests // @Accept json // @Produce json // @Param id path string true "ID" @@ -315,10 +316,11 @@ type SubmitApplicationResponse struct { // SubmitApplication godoc // -// @Summary Submit application -// @Description Submit a credential application in response to a credential manifest. The request body is expected to -// @Description be a valid JWT signed by the applicant's DID, containing two top level properties: `credential_application` and `vcs`. -// @Tags ApplicationAPI +// @Summary Submit a Credential Application +// @Description Submit a Credential Application in response to a Credential Manifest request. The request body is expected to +// @Description be a valid JWT signed by the applicant's DID, containing two top level properties: `credential_application` and `vcs` +// @Description according to the spec https://identity.foundation/credential-manifest/#credential-application +// @Tags ManifestApplications // @Accept json // @Produce json // @Param request body SubmitApplicationRequest true "request body" @@ -358,9 +360,9 @@ type GetApplicationResponse struct { // GetApplication godoc // -// @Summary Get application -// @Description Get application by id -// @Tags ApplicationAPI +// @Summary Get a Credential Application +// @Description Get a Credential Application by its ID +// @Tags ManifestApplications // @Accept json // @Produce json // @Param id path string true "ID" @@ -395,9 +397,9 @@ type ListApplicationsResponse struct { // ListApplications godoc // -// @Summary List applications -// @Description List all the existing applications. -// @Tags ApplicationAPI +// @Summary List Credential Applications +// @Description List all the existing Credential Applications. +// @Tags ManifestApplications // @Accept json // @Produce json // @Success 200 {object} ListApplicationsResponse @@ -417,9 +419,9 @@ func (mr ManifestRouter) ListApplications(c *gin.Context) { // DeleteApplication godoc // -// @Summary Delete applications -// @Description Delete application by ID -// @Tags ApplicationAPI +// @Summary Delete Credential Applications +// @Description Delete a Credential Application by its ID +// @Tags ManifestApplications // @Accept json // @Produce json // @Param id path string true "ID" @@ -453,9 +455,9 @@ type GetResponseResponse struct { // GetResponse godoc // -// @Summary Get response -// @Description Get response by id -// @Tags ResponseAPI +// @Summary Get a Credential Manifest Response +// @Description Get a Credential Manifest Response by its ID https://identity.foundation/credential-manifest/#credential-response +// @Tags ManifestResponses // @Accept json // @Produce json // @Param id path string true "ID" @@ -492,9 +494,9 @@ type ListResponsesResponse struct { // ListResponses godoc // -// @Summary List responses -// @Description Lists all responses -// @Tags ResponseAPI +// @Summary List Credential Manifest Responses +// @Description Lists all responses to Credential Applications associated with a Credential Manifest +// @Tags ManifestResponses // @Accept json // @Produce json // @Success 200 {object} ListResponsesResponse @@ -514,9 +516,9 @@ func (mr ManifestRouter) ListResponses(c *gin.Context) { // DeleteResponse godoc // -// @Summary Delete responses -// @Description Delete response by ID -// @Tags ResponseAPI +// @Summary Delete a Credential Manifest Response +// @Description Delete a Credential Manifest Response by its ID +// @Tags ManifestResponses // @Accept json // @Produce json // @Param id path string true "ID" @@ -561,11 +563,13 @@ func (r ReviewApplicationRequest) toServiceRequest(id string) model.ReviewApplic // ReviewApplication godoc // -// @Summary Reviews an application -// @Description Reviewing an application either fulfills or denies the credential. -// @Tags ApplicationAPI +// @Summary Review a Credential Application +// @Description Reviewing a Credential Application either fulfills or denies the credential(s) issuance according +// @Description to the spec https://identity.foundation/credential-manifest/#credential-application. +// @Tags ManifestApplications // @Accept json // @Produce json +// @Param id path string true "ID" // @Param request body ReviewApplicationRequest true "request body" // @Success 201 {object} SubmitApplicationResponse "Credential Response" // @Failure 400 {string} string "Bad request" @@ -612,9 +616,9 @@ type CreateManifestRequestResponse struct { // CreateRequest godoc // -// @Summary Create Manifest Request Request -// @Description Create manifest request from an existing credential manifest. -// @Tags ManifestAPI +// @Summary Create a Credential Manifest Request +// @Description Create a Credential Manifest Request from an existing Credential Manifest. +// @Tags ManifestRequests // @Accept json // @Produce json // @Param request body CreateManifestRequestRequest true "request body" @@ -668,8 +672,8 @@ type ListManifestRequestsResponse struct { // ListRequests godoc // // @Summary List Credential Manifest Requests -// @Description Lists all the existing credential manifest requests -// @Tags ManifestAPI +// @Description Lists all the existing Credential Manifest requests +// @Tags ManifestRequests // @Accept json // @Produce json // @Success 200 {object} ListManifestRequestsResponse @@ -695,9 +699,9 @@ type GetManifestRequestResponse struct { // GetRequest godoc // -// @Summary Get Manifest Request -// @Description Get a manifest request by its ID -// @Tags ManifestAPI +// @Summary Get a Credential Manifest Request +// @Description Get a Credential Manifest Request by its ID +// @Tags ManifestRequests // @Accept json // @Produce json // @Param id path string true "ID" @@ -721,9 +725,9 @@ func (mr ManifestRouter) GetRequest(c *gin.Context) { // DeleteRequest godoc // -// @Summary Delete Manifest Request -// @Description Delete a manifest request by its ID -// @Tags ManifestAPI +// @Summary Delete a Credential Manifest Request +// @Description Delete a Credential Manifest Request by its ID +// @Tags ManifestRequests // @Accept json // @Produce json // @Param id path string true "ID" diff --git a/pkg/server/router/operation.go b/pkg/server/router/operation.go index fc38f701c..86e392e9d 100644 --- a/pkg/server/router/operation.go +++ b/pkg/server/router/operation.go @@ -58,7 +58,7 @@ type OperationResult struct { // // @Summary Get an operation // @Description Get operation by its ID -// @Tags OperationAPI +// @Tags Operations // @Accept json // @Produce json // @Param id path string true "ID" @@ -141,7 +141,7 @@ type ListOperationsResponse struct { // // @Summary List operations // @Description List operations according to the request -// @Tags OperationAPI +// @Tags Operations // @Accept json // @Produce json // @Param parent query string false "The name of the parent's resource. For example: `?parent=/presentation/submissions`" @@ -224,9 +224,9 @@ func routerModel(op operation.Operation) Operation { // CancelOperation godoc // -// @Summary Cancel an ongoing operation -// @Description Cancels an ongoing operation, if possible. -// @Tags OperationAPI +// @Summary Cancel an operation +// @Description Cancels an active operation, if possible. +// @Tags Operations // @Accept json // @Produce json // @Param id path string true "ID" diff --git a/pkg/server/router/presentation.go b/pkg/server/router/presentation.go index 80f5c5d3e..5acebd1b4 100644 --- a/pkg/server/router/presentation.go +++ b/pkg/server/router/presentation.go @@ -55,9 +55,9 @@ type CreatePresentationDefinitionResponse struct { // CreateDefinition godoc // -// @Summary Create PresentationDefinition -// @Description Create presentation definition -// @Tags PresentationDefinitionAPI +// @Summary Create a Presentation Definition +// @Description Create a Presentation Definition https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition +// @Tags Presentations // @Accept json // @Produce json // @Param request body CreatePresentationDefinitionRequest true "request body" @@ -134,9 +134,9 @@ type GetPresentationDefinitionResponse struct { // GetDefinition godoc // -// @Summary Get PresentationDefinition -// @Description Get a presentation definition by its ID -// @Tags PresentationDefinitionAPI +// @Summary Get a Presentation Definition +// @Description Get a Presentation Definition by its ID +// @Tags Presentations // @Accept json // @Produce json // @Param id path string true "ID" @@ -171,8 +171,8 @@ type ListDefinitionsResponse struct { // ListDefinitions godoc // // @Summary List Presentation Definitions -// @Description Lists all the existing presentation definitions -// @Tags PresentationDefinitionAPI +// @Description Lists all the existing Presentation Definitions +// @Tags Presentations // @Accept json // @Produce json // @Success 200 {object} ListDefinitionsResponse @@ -193,9 +193,9 @@ func (pr PresentationRouter) ListDefinitions(c *gin.Context) { // DeleteDefinition godoc // -// @Summary Delete PresentationDefinition -// @Description Delete a presentation definition by its ID -// @Tags PresentationDefinitionAPI +// @Summary Delete a Presentation Definition +// @Description Delete a Presentation Definition by its ID +// @Tags Presentations // @Accept json // @Produce json // @Param id path string true "ID" @@ -263,9 +263,9 @@ func (r CreateSubmissionRequest) toServiceRequest() (*model.CreateSubmissionRequ // CreateSubmission godoc // -// @Summary Create Submission -// @Description Creates a submission in this server ready to be reviewed. -// @Tags PresentationSubmissionAPI +// @Summary Create a Presentation Submission +// @Description Accepts a Presentation Submission (https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) in this server ready to be reviewed. +// @Tags PresentationSubmissions // @Accept json // @Produce json // @Param request body CreateSubmissionRequest true "request body" @@ -304,9 +304,9 @@ type GetSubmissionResponse struct { // GetSubmission godoc // -// @Summary Get Submission -// @Description Get a submission by its ID -// @Tags PresentationSubmissionAPI +// @Summary Get a Presentation Submission +// @Description Get a Presentation Submission by its ID +// @Tags PresentationSubmissions // @Accept json // @Produce json // @Param id path string true "ID" @@ -350,9 +350,9 @@ type ListSubmissionResponse struct { // ListSubmissions godoc // -// @Summary List Submissions -// @Description List existing submissions according to a filtering query. The `filter` field follows the syntax described in https://google.aip.dev/160. -// @Tags PresentationSubmissionAPI +// @Summary List Presentation Submissions +// @Description List existing Presentation Submissions according to a filtering query. The `filter` field follows the syntax described in https://google.aip.dev/160. +// @Tags PresentationSubmissions // @Accept json // @Produce json // @Param filter query string false "A standard filter expression conforming to https://google.aip.dev/160. For example: `?filter=status="pending"`" @@ -444,11 +444,13 @@ type ReviewSubmissionResponse struct { // ReviewSubmission godoc // -// @Summary Review a pending submission -// @Description Reviews a pending submission. After this method is called, the operation with `id==presentations/submissions/{submission_id}` will be updated with the result of this invocation. -// @Tags PresentationSubmissionAPI +// @Summary Review a pending Presentation Submission +// @Description Reviews a pending Presentation Submission. After this method is called, the operation with +// @Description `id==presentations/submissions/{submission_id}` will be updated with the result of this invocation. +// @Tags PresentationSubmissions // @Accept json // @Produce json +// @Param id path string true "ID" // @Param request body ReviewSubmissionRequest true "request body" // @Success 200 {object} ReviewSubmissionResponse // @Failure 400 {string} string "Bad request" @@ -495,9 +497,10 @@ type GetRequestResponse struct { // CreateRequest godoc // -// @Summary Create Presentation Request -// @Description Create presentation request from an existing presentation definition. -// @Tags PresentationRequestAPI +// @Summary Create a Presentation Request +// @Description Create a Presentation Request from an existing Presentation Definition with an existing DID according +// @Description to the spec https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-request +// @Tags PresentationRequests // @Accept json // @Produce json // @Param request body CreateRequestRequest true "request body" @@ -545,9 +548,9 @@ func (pr PresentationRouter) serviceRequestFromRequest(request CreateRequestRequ // GetRequest godoc // -// @Summary Get Presentation Request -// @Description Get a presentation request by its ID -// @Tags PresentationRequestAPI +// @Summary Get a Presentation Request +// @Description Get a Presentation Request by its ID +// @Tags PresentationRequests // @Accept json // @Produce json // @Param id path string true "ID" @@ -577,8 +580,8 @@ type ListPresentationRequestsResponse struct { // ListRequests godoc // // @Summary List Presentation Requests -// @Description Lists all the existing presentation requests -// @Tags PresentationRequestAPI +// @Description Lists all the existing Presentation Requests +// @Tags PresentationRequests // @Accept json // @Produce json // @Success 200 {object} ListPresentationRequestsResponse @@ -600,9 +603,9 @@ func (pr PresentationRouter) ListRequests(c *gin.Context) { // DeleteRequest godoc // -// @Summary Delete PresentationRequest -// @Description Delete a presentation request by its ID -// @Tags PresentationRequestAPI +// @Summary Delete a Presentation Request +// @Description Delete a Presentation Request by its ID +// @Tags PresentationRequests // @Accept json // @Produce json // @Param id path string true "ID" diff --git a/pkg/server/router/readiness.go b/pkg/server/router/readiness.go index 0869a564f..8d1ac8886 100644 --- a/pkg/server/router/readiness.go +++ b/pkg/server/router/readiness.go @@ -28,10 +28,10 @@ type GetReadinessResponse struct { // Readiness godoc // -// @Summary Readiness +// @Summary Check service readiness // @Description Readiness runs a number of application specific checks to see if all the relied upon services are // @Description healthy. -// @Tags Readiness +// @Tags ServiceInfo // @Accept json // @Produce json // @Success 200 {object} GetReadinessResponse diff --git a/pkg/server/router/schema.go b/pkg/server/router/schema.go index 95f264263..6fafac22e 100644 --- a/pkg/server/router/schema.go +++ b/pkg/server/router/schema.go @@ -86,9 +86,9 @@ type SchemaResponse struct { // CreateSchema godoc // -// @Summary Create Schema -// @Description Create schema -// @Tags SchemaAPI +// @Summary Create a Credential Schema +// @Description Create a schema for use with a Verifiable Credential +// @Tags Schemas // @Accept json // @Produce json // @Param request body CreateSchemaRequest true "request body" @@ -145,9 +145,9 @@ func (sr SchemaRouter) CreateSchema(c *gin.Context) { // GetSchema godoc // -// @Summary Get Schema -// @Description Get a schema by its ID -// @Tags SchemaAPI +// @Summary Get a Credential Schema +// @Description Get a Credential Schema by its ID +// @Tags Schemas // @Accept json // @Produce json // @Param id path string true "ID" @@ -189,9 +189,9 @@ type ListSchemasResponse struct { // ListSchemas godoc // -// @Summary List Schemas -// @Description List schemas -// @Tags SchemaAPI +// @Summary List Credential Schemas +// @Description List Credential Schemas stored by the service +// @Tags Schemas // @Accept json // @Produce json // @Success 200 {object} ListSchemasResponse @@ -227,9 +227,9 @@ type GetSchemaResponse struct { // DeleteSchema godoc // -// @Summary Delete Schema -// @Description Delete a schema by its ID -// @Tags SchemaAPI +// @Summary Delete a Credential Schema +// @Description Delete a Credential Schema by its ID +// @Tags Schemas // @Accept json // @Produce json // @Param id path string true "ID" diff --git a/pkg/server/router/webhook.go b/pkg/server/router/webhook.go index 90af13fde..29c35c3f5 100644 --- a/pkg/server/router/webhook.go +++ b/pkg/server/router/webhook.go @@ -44,9 +44,9 @@ type CreateWebhookResponse struct { // CreateWebhook godoc // -// @Summary Create Webhook -// @Description Create webhook -// @Tags WebhookAPI +// @Summary Create a webhook +// @Description Creates a webhook +// @Tags Webhooks // @Accept json // @Produce json // @Param request body CreateWebhookRequest true "request body" @@ -92,14 +92,15 @@ type ListWebhookResponse struct { // GetWebhook godoc // -// @Summary Get Webhook +// @Summary Get a webhook // @Description Get a webhook by its ID -// @Tags WebhookAPI +// @Tags Webhooks // @Accept json // @Produce json -// @Param id path string true "ID" -// @Success 200 {object} ListWebhookResponse -// @Failure 400 {string} string "Bad request" +// @Param noun path string true "noun" +// @Param verb path string true "verb" +// @Success 200 {object} ListWebhookResponse +// @Failure 400 {string} string "Bad request" // @Router /v1/webhooks/{noun}/{verb} [get] func (wr WebhookRouter) GetWebhook(c *gin.Context) { noun := framework.GetParam(c, "noun") @@ -133,9 +134,9 @@ type ListWebhooksResponse struct { // ListWebhooks godoc // -// @Summary List Webhooks -// @Description Lists all webhooks -// @Tags WebhookAPI +// @Summary List webhooks +// @Description Lists all webhooks stored by the service +// @Tags Webhooks // @Accept json // @Produce json // @Success 200 {object} ListWebhooksResponse @@ -166,15 +167,17 @@ type DeleteWebhookRequest struct { // DeleteWebhook godoc // -// @Summary Delete Webhook +// @Summary Delete a webhook // @Description Delete a webhook by its ID -// @Tags WebhookAPI +// @Tags Webhooks // @Accept json // @Produce json -// @Param id path string true "ID" -// @Success 204 {string} string "No Content" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" +// @Param noun path string true "noun" +// @Param verb path string true "verb" +// @Param url path string true "url" +// @Success 204 {string} string "No Content" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" // @Router /v1/webhooks/{noun}/{verb}/{url} [delete] func (wr WebhookRouter) DeleteWebhook(c *gin.Context) { var request DeleteWebhookRequest @@ -205,9 +208,9 @@ type GetSupportedNounsResponse struct { // GetSupportedNouns godoc // -// @Summary Get Supported Nouns +// @Summary Get supported webhook nouns // @Description Get supported nouns for webhook generation -// @Tags WebhookAPI +// @Tags Webhooks // @Accept json // @Produce json // @Success 200 {object} webhook.GetSupportedNounsResponse @@ -223,9 +226,9 @@ type GetSupportedVerbsResponse struct { // GetSupportedVerbs godoc // -// @Summary Get Supported Verbs +// @Summary Get supported webhook verbs // @Description Get supported verbs for webhook generation -// @Tags WebhookAPI +// @Tags Webhooks // @Accept json // @Produce json // @Success 200 {object} webhook.GetSupportedVerbsResponse diff --git a/pkg/service/manifest/model/model.go b/pkg/service/manifest/model/model.go index e1602eaef..2927096ee 100644 --- a/pkg/service/manifest/model/model.go +++ b/pkg/service/manifest/model/model.go @@ -6,6 +6,7 @@ import ( "github.com/TBD54566975/ssi-sdk/credential/exchange" manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" sdkutil "github.com/TBD54566975/ssi-sdk/util" + "github.com/tbd54566975/ssi-service/pkg/service/common" cred "github.com/tbd54566975/ssi-service/internal/credential" @@ -188,7 +189,7 @@ type Request struct { } type PresentationDefinitionRef struct { - // id of the presentation definition created with PresentationDefinitionAPI. Must be empty if `value` is present. + // id of the presentation definition created with the PresentationDefinitions API. Must be empty if `value` is present. ID *string `json:"presentationDefinitionId"` // value of the presentation definition to use. Must be empty if `id` is present. From f1b0954086a9e5fa61a9b86e6df69b7bcbea3467 Mon Sep 17 00:00:00 2001 From: Gabe <7622243+decentralgabe@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:54:39 -0700 Subject: [PATCH 17/20] Add an endpoint to verify presentations (#630) * back to normal? * tmp * naming * it should work * tests * mage spec * renames * swagger * renames * merge * Apply suggestions from code review Co-authored-by: Andres Uribe * pr comments --------- Co-authored-by: Andres Uribe --- doc/swagger.yaml | 60 +- .../verification.go | 135 +++-- pkg/server/router/credential.go | 4 +- pkg/server/router/presentation.go | 63 +- pkg/server/server_did_configuration_test.go | 3 +- pkg/server/server_presentation_test.go | 545 +++++++++++------- pkg/service/credential/service.go | 6 +- pkg/service/did/ion.go | 1 + pkg/service/presentation/service.go | 37 +- pkg/service/well-known/did_configuration.go | 14 +- 10 files changed, 582 insertions(+), 286 deletions(-) rename internal/{credential => verification}/verification.go (58%) diff --git a/doc/swagger.yaml b/doc/swagger.yaml index 48e0d0acc..f750e439c 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -1991,6 +1991,23 @@ definitions: description: Whether the credential was verified. type: boolean type: object + pkg_server_router.VerifyPresentationRequest: + properties: + presentationJwt: + description: A JWT that encodes a verifiable presentation according to https://www.w3.org/TR/vc-data-model/#json-web-token + type: string + required: + - presentationJwt + type: object + pkg_server_router.VerifyPresentationResponse: + properties: + reason: + description: The reason why this presentation couldn't be verified. + type: string + verified: + description: Whether the presentation was verified. + type: boolean + type: object rendering.ColorResource: properties: color: @@ -2517,10 +2534,10 @@ paths: consumes: - application/json description: |- - Verify a given Verifiable Credential by its ID. The system does the following levels of verification: + Verifies a given verifiable credential. The system does the following levels of verification: 1. Makes sure the credential has a valid signature 2. Makes sure the credential has is not expired - 3. Makes sure the credential complies with the VC Data Model + 3. Makes sure the credential complies with the VC Data Model v1.1 4. If the credential has a schema, makes sure its data complies with the schema parameters: - description: request body @@ -3886,6 +3903,45 @@ paths: summary: Review a pending Presentation Submission tags: - PresentationSubmissions + /v1/presentations/verification: + put: + consumes: + - application/json + description: |- + Verifies a given presentation. The system does the following levels of verification: + 1. Makes sure the presentation has a valid signature + 2. Makes sure the presentation is not expired + 3. Makes sure the presentation complies with https://www.w3.org/TR/vc-data-model/#presentations-0 of VC Data Model v1.1 + 4. For each credential in the presentation, makes sure: + a. Makes sure the credential has a valid signature + b. Makes sure the credential is not expired + c. Makes sure the credential complies with the VC Data Model + d. If the credential has a schema, makes sure its data complies with the schema + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/pkg_server_router.VerifyPresentationRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_server_router.VerifyPresentationResponse' + "400": + description: Bad request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Verifies a Verifiable Presentation + tags: + - Presentations /v1/schemas: get: consumes: diff --git a/internal/credential/verification.go b/internal/verification/verification.go similarity index 58% rename from internal/credential/verification.go rename to internal/verification/verification.go index 536b12e95..20848e28c 100644 --- a/internal/credential/verification.go +++ b/internal/verification/verification.go @@ -1,4 +1,4 @@ -package credential +package verification import ( "context" @@ -10,26 +10,27 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/cryptosuite/jws2020" + "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/did/resolution" sdkutil "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" - "github.com/lestrrat-go/jwx/jws" "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/internal/credential" didint "github.com/tbd54566975/ssi-service/internal/did" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/schema" ) -type Validator struct { +type Verifier struct { validator *validation.CredentialValidator didResolver resolution.Resolver schemaResolver schema.Resolution } -// NewCredentialValidator creates a new credential validator which executes both signature and static verification checks. -// In the future the set of verification checks will be configurable. -func NewCredentialValidator(didResolver resolution.Resolver, schemaResolver schema.Resolution) (*Validator, error) { +// NewVerifiableDataVerifier creates a new verifier for both verifiable credentials and verifiable presentations. The verifier +// executes both signature and static verification checks. In the future the set of verification checks will be configurable. +func NewVerifiableDataVerifier(didResolver resolution.Resolver, schemaResolver schema.Resolution) (*Verifier, error) { if didResolver == nil { return nil, errors.New("didResolver cannot be nil") } @@ -40,30 +41,19 @@ func NewCredentialValidator(didResolver resolution.Resolver, schemaResolver sche validators := validation.GetKnownVerifiers() validator, err := validation.NewCredentialValidator(validators) if err != nil { - return nil, errors.Wrap(err, "failed to create static credential validator") + return nil, errors.Wrap(err, "failed to create static validator") } - return &Validator{ + return &Verifier{ validator: validator, didResolver: didResolver, schemaResolver: schemaResolver, }, nil } -// VerifyJWTCredential first parses and checks the signature on the given JWT credential. Next, it runs -// a set of static verification checks on the credential as per the credential service's configuration. -func (v Validator) VerifyJWTCredential(ctx context.Context, token keyaccess.JWT) error { - _, err := integrity.VerifyJWTCredential(ctx, token.String(), v.didResolver) - if err != nil { - return errors.Wrap(err, "verifying JWT credential") - } - _, _, cred, err := integrity.ParseVerifiableCredentialFromJWT(token.String()) - if err != nil { - return errors.Wrap(err, "parsing vc from jwt") - } - return v.staticValidationChecks(ctx, *cred) -} - -func (v Validator) Verify(ctx context.Context, credential Container) error { +// VerifyCredential first parses and checks the signature on the given credential. Next, it runs +// a set of static verification checks on the credential as per the service's configuration. +// Works for both JWT and LD securing mechanisms. +func (v Verifier) VerifyCredential(ctx context.Context, credential credential.Container) error { if credential.HasJWTCredential() { err := v.VerifyJWTCredential(ctx, *credential.CredentialJWT) if err != nil { @@ -77,9 +67,23 @@ func (v Validator) Verify(ctx context.Context, credential Container) error { return nil } -// VerifyDataIntegrityCredential first checks the signature on the given data integrity credential. Next, it runs -// a set of static verification checks on the credential as per the credential service's configuration. -func (v Validator) VerifyDataIntegrityCredential(ctx context.Context, credential credsdk.VerifiableCredential) error { +// VerifyJWTCredential first parses and checks the signature on the given JWT verification. Next, it runs +// a set of static verification checks on the credential as per the service's configuration. +func (v Verifier) VerifyJWTCredential(ctx context.Context, token keyaccess.JWT) error { + _, err := integrity.VerifyJWTCredential(ctx, token.String(), v.didResolver) + if err != nil { + return errors.Wrap(err, "verifying JWT credential") + } + _, _, cred, err := integrity.ParseVerifiableCredentialFromJWT(token.String()) + if err != nil { + return errors.Wrap(err, "parsing vc from jwt") + } + return v.staticValidationChecks(ctx, *cred) +} + +// VerifyDataIntegrityCredential first checks the signature on the given data integrity verification. Next, it runs +// a set of static verification checks on the credential as per the service's configuration. +func (v Verifier) VerifyDataIntegrityCredential(ctx context.Context, credential credsdk.VerifiableCredential) error { // resolve the issuer's key material issuer, ok := credential.Issuer.(string) if !ok { @@ -120,56 +124,67 @@ func (v Validator) VerifyDataIntegrityCredential(ctx context.Context, credential return v.staticValidationChecks(ctx, credential) } -func getKeyFromProof(proof crypto.Proof, key string) (any, error) { - proofBytes, err := json.Marshal(proof) +// VerifyJWTPresentation first parses and checks the signature on the given JWT presentation. Next, it runs +// a set of static verification checks on the presentation's credentials as per the service's configuration. +func (v Verifier) VerifyJWTPresentation(ctx context.Context, token keyaccess.JWT) error { + headers, jwt, vp, err := integrity.ParseVerifiablePresentationFromJWT(token.String()) if err != nil { - return nil, err - } - var proofMap map[string]any - if err = json.Unmarshal(proofBytes, &proofMap); err != nil { - return nil, err + return errors.Wrap(err, "parsing JWT presentation") } - return proofMap[key], nil -} -func (v Validator) VerifyJWT(ctx context.Context, did string, token keyaccess.JWT) error { - // parse headers - headers, err := keyaccess.GetJWTHeaders([]byte(token)) - if err != nil { - return sdkutil.LoggingErrorMsg(err, "could not parse JWT headers") + // get key to verify the presentation with + issuerKID := headers.KeyID() + if issuerKID == "" { + return errors.Errorf("missing kid in header of presentation<%s>", jwt.JwtID()) } - jwtKID, ok := headers.Get(jws.KeyIDKey) - if !ok { - return sdkutil.LoggingNewError("JWT does not contain a kid") + issuerDID, err := v.didResolver.Resolve(ctx, jwt.Issuer()) + if err != nil { + return errors.Wrapf(err, "getting issuer DID<%s> to verify presentation<%s>", jwt.Issuer(), jwt.JwtID()) } - kid, ok := jwtKID.(string) - if !ok { - return sdkutil.LoggingNewError("JWT kid is not a string") + issuerPublicKey, err := did.GetKeyFromVerificationMethod(issuerDID.Document, issuerKID) + if err != nil { + return errors.Wrapf(err, "getting key to verify presentation<%s>", jwt.JwtID()) } - // resolve key material from the DID - pubKey, err := didint.ResolveKeyForDID(ctx, v.didResolver, did, kid) + // construct a verifier and verify the signature on the presentation + // note: this also verifies the signature of each credential in the presentation + verifier, err := jwx.NewJWXVerifier(issuerDID.ID, issuerKID, issuerPublicKey) if err != nil { - return sdkutil.LoggingError(err) + return errors.Wrapf(err, "constructing verifier for presentation<%s>", jwt.JwtID()) } - - // construct a signature validator from the verification information - verifier, err := keyaccess.NewJWKKeyAccessVerifier(did, kid, pubKey) + _, _, _, err = integrity.VerifyVerifiablePresentationJWT(ctx, *verifier, v.didResolver, token.String()) if err != nil { - return sdkutil.LoggingErrorMsgf(err, "could not create validator for kid %s", kid) + return errors.Wrapf(err, "verifying presentation<%s>", jwt.JwtID()) } - // verify the signature on the credential - if err = verifier.Verify(token); err != nil { - return sdkutil.LoggingErrorMsg(err, "could not verify the JWT signature") + // for each credential in the presentation, run a set of static verification checks + creds, err := credential.NewCredentialContainerFromArray(vp.VerifiableCredential) + if err != nil { + return errors.Wrapf(err, "error parsing credentials in presentation<%s>", vp.ID) + } + for _, cred := range creds { + if err = v.staticValidationChecks(ctx, *cred.Credential); err != nil { + return errors.Wrapf(err, "error running static validation checks on credential in presentation<%v>", cred.ID) + } } - return nil } -// staticValidationChecks runs a set of static validation checks on the credential as per the credential -// service's configuration, such as checking the credential's schema, expiration, and object validity. -func (v Validator) staticValidationChecks(ctx context.Context, credential credsdk.VerifiableCredential) error { +func getKeyFromProof(proof crypto.Proof, key string) (any, error) { + proofBytes, err := json.Marshal(proof) + if err != nil { + return nil, err + } + var proofMap map[string]any + if err = json.Unmarshal(proofBytes, &proofMap); err != nil { + return nil, err + } + return proofMap[key], nil +} + +// staticValidationChecks runs a set of static validation checks on the credential as per the +// service's configuration, such as checking the verification's schema, expiration, and object validity. +func (v Verifier) staticValidationChecks(ctx context.Context, credential credsdk.VerifiableCredential) error { // if the credential has a schema, resolve it before it is to be used in verification var validationOpts []validation.Option if credential.CredentialSchema != nil { diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go index 312d652c2..50f4b8483 100644 --- a/pkg/server/router/credential.go +++ b/pkg/server/router/credential.go @@ -415,10 +415,10 @@ type VerifyCredentialResponse struct { // VerifyCredential godoc // // @Summary Verify a Verifiable Credential -// @Description Verify a given Verifiable Credential by its ID. The system does the following levels of verification: +// @Description Verifies a given verifiable credential. The system does the following levels of verification: // @Description 1. Makes sure the credential has a valid signature // @Description 2. Makes sure the credential has is not expired -// @Description 3. Makes sure the credential complies with the VC Data Model +// @Description 3. Makes sure the credential complies with the VC Data Model v1.1 // @Description 4. If the credential has a schema, makes sure its data complies with the schema // @Tags Credentials // @Accept json diff --git a/pkg/server/router/presentation.go b/pkg/server/router/presentation.go index 5acebd1b4..e73e8961b 100644 --- a/pkg/server/router/presentation.go +++ b/pkg/server/router/presentation.go @@ -7,16 +7,16 @@ import ( "github.com/TBD54566975/ssi-sdk/credential/exchange" "github.com/TBD54566975/ssi-sdk/credential/integrity" + "github.com/TBD54566975/ssi-sdk/util" "github.com/gin-gonic/gin" "github.com/goccy/go-json" "github.com/pkg/errors" "go.einride.tech/aip/filtering" - "github.com/tbd54566975/ssi-service/pkg/server/pagination" - credint "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/pkg/server/framework" + "github.com/tbd54566975/ssi-service/pkg/server/pagination" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/presentation" "github.com/tbd54566975/ssi-service/pkg/service/presentation/model" @@ -37,6 +37,65 @@ func NewPresentationRouter(s svcframework.Service) (*PresentationRouter, error) return &PresentationRouter{service: service}, nil } +type VerifyPresentationRequest struct { + // A JWT that encodes a verifiable presentation according to https://www.w3.org/TR/vc-data-model/#json-web-token + PresentationJWT *keyaccess.JWT `json:"presentationJwt,omitempty" validate:"required"` +} + +type VerifyPresentationResponse struct { + // Whether the presentation was verified. + Verified bool `json:"verified"` + + // The reason why this presentation couldn't be verified. + Reason string `json:"reason,omitempty"` +} + +// VerifyPresentation godoc +// +// @Summary Verifies a Verifiable Presentation +// @Description Verifies a given presentation. The system does the following levels of verification: +// @Description 1. Makes sure the presentation has a valid signature +// @Description 2. Makes sure the presentation is not expired +// @Description 3. Makes sure the presentation complies with https://www.w3.org/TR/vc-data-model/#presentations-0 of VC Data Model v1.1 +// @Description 4. For each credential in the presentation, makes sure: +// @Description a. Makes sure the credential has a valid signature +// @Description b. Makes sure the credential is not expired +// @Description c. Makes sure the credential complies with the VC Data Model +// @Description d. If the credential has a schema, makes sure its data complies with the schema +// @Tags Presentations +// @Accept json +// @Produce json +// @Param request body VerifyPresentationRequest true "request body" +// @Success 200 {object} VerifyPresentationResponse +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /v1/presentations/verification [put] +func (pr PresentationRouter) VerifyPresentation(c *gin.Context) { + var request VerifyPresentationRequest + if err := framework.Decode(c.Request, &request); err != nil { + errMsg := "invalid verify presentation request" + framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusBadRequest) + return + } + + if err := util.IsValidStruct(request); err != nil { + framework.LoggingRespondError(c, err, http.StatusBadRequest) + return + } + + verificationResult, err := pr.service.VerifyPresentation(c, presentation.VerifyPresentationRequest{ + PresentationJWT: request.PresentationJWT, + }) + if err != nil { + errMsg := "could not verify presentation" + framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError) + return + } + + resp := VerifyPresentationResponse{Verified: verificationResult.Verified, Reason: verificationResult.Reason} + framework.Respond(c, resp, http.StatusOK) +} + type CreatePresentationDefinitionRequest struct { Name string `json:"name,omitempty"` Purpose string `json:"purpose,omitempty"` diff --git a/pkg/server/server_did_configuration_test.go b/pkg/server/server_did_configuration_test.go index e7f7be5af..368560586 100644 --- a/pkg/server/server_did_configuration_test.go +++ b/pkg/server/server_did_configuration_test.go @@ -10,6 +10,8 @@ import ( "github.com/TBD54566975/ssi-sdk/did/resolution" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "gopkg.in/h2non/gock.v1" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -17,7 +19,6 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/schema" wellknown "github.com/tbd54566975/ssi-service/pkg/service/well-known" "github.com/tbd54566975/ssi-service/pkg/testutil" - "gopkg.in/h2non/gock.v1" ) const w3cCredentialContext = `{ diff --git a/pkg/server/server_presentation_test.go b/pkg/server/server_presentation_test.go index a20a4d41c..93c4a5306 100644 --- a/pkg/server/server_presentation_test.go +++ b/pkg/server/server_presentation_test.go @@ -8,6 +8,7 @@ import ( "net/url" "strings" "testing" + "time" "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/credential/exchange" @@ -55,16 +56,146 @@ func TestPresentationAPI(t *testing.T) { assert.NoError(t, err) for _, test := range testutil.TestDatabases { - t.Run(test.Name, func(t *testing.T) { - t.Run("Create, Get, and Delete PresentationDefinition", func(tt *testing.T) { - s := test.ServiceStorage(tt) - pRouter, _ := setupPresentationRouter(tt, s) + t.Run(test.Name, func(tt *testing.T) { + tt.Run("Verify a Verifiable Presentation", func(ttt *testing.T) { + db := test.ServiceStorage(ttt) + presRouter, _ := setupPresentationRouter(ttt, db) + + // first, create a credential using the service + keyStoreService, _ := testKeyStoreService(ttt, db) + didService, _ := testDIDService(ttt, db, keyStoreService, nil) + schemaService := testSchemaService(ttt, db, keyStoreService, didService) + credRouter := testCredentialRouter(ttt, db, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(ttt, err) + assert.NotEmpty(ttt, issuerDID) + + // good request + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + VerificationMethodID: issuerDID.DID.VerificationMethod[0].ID, + Subject: "did:car:911", + Data: map[string]any{ + "firstName": "Frank", + "lastName": "Ocean", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(ttt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + credRouter.CreateCredential(c) + assert.True(ttt, util.Is2xxResponse(w.Code)) + + var createResp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&createResp) + assert.NoError(ttt, err) + + assert.NotEmpty(ttt, createResp.CredentialJWT) + assert.NoError(ttt, err) + assert.Equal(ttt, createResp.Credential.Issuer, issuerDID.DID.ID) + + holderSigner, holderDID := getSigner(ttt) + testPresentation := credential.VerifiablePresentation{ + Context: []string{"https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/jws-2020/v1"}, + Type: []string{"VerifiablePresentation"}, + Holder: holderDID.String(), + } + + ttt.Run("Invalid Verifiable Presentation with no credentials", func(tttt *testing.T) { + // use the sdk to create a vp + emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation) + assert.NoError(tttt, err) + + badPresentation := string(emptyPresentation[:10]) + value := newRequestValue(tttt, router.VerifyPresentationRequest{PresentationJWT: keyaccess.JWTPtr(badPresentation)}) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/presentations/verification"), value) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + presRouter.VerifyPresentation(c) + assert.True(tttt, util.Is2xxResponse(w.Code)) + + var resp router.VerifyPresentationResponse + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.False(tttt, resp.Verified) + assert.Equal(tttt, resp.Reason, "parsing JWT presentation: parsing vp token: invalid JWT") + }) + + ttt.Run("Valid Verifiable Presentation with no credentials", func(tttt *testing.T) { + // use the sdk to create a vp + emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation) + assert.NoError(tttt, err) + + value := newRequestValue(t, router.VerifyPresentationRequest{PresentationJWT: keyaccess.JWTPtr(string(emptyPresentation))}) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/presentations/verification"), value) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + presRouter.VerifyPresentation(c) + assert.True(tttt, util.Is2xxResponse(w.Code)) + + var resp router.VerifyPresentationResponse + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.True(tttt, resp.Verified) + }) + + ttt.Run("Invalid Verifiable Presentation with invalid credential signature", func(tttt *testing.T) { + // add credential to the vp + badCredJWT := createResp.CredentialJWT.String()[:10] + testPresentation.VerifiableCredential = []any{badCredJWT} + + // use the sdk to create a vp + emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation) + assert.NoError(tt, err) + + value := newRequestValue(tttt, router.VerifyPresentationRequest{PresentationJWT: keyaccess.JWTPtr(string(emptyPresentation))}) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/presentations/verification"), value) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + presRouter.VerifyPresentation(c) + assert.True(tttt, util.Is2xxResponse(w.Code)) + + var resp router.VerifyPresentationResponse + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.False(tttt, resp.Verified) + assert.Contains(tttt, resp.Reason, "verifying credential 0: parsing JWT: parsing credential token: invalid JWT") + }) + + ttt.Run("Valid Verifiable Presentation with valid credential signature", func(tttt *testing.T) { + // add credential to the vp + testPresentation.VerifiableCredential = []any{createResp.CredentialJWT} + + // use the sdk to create a vp + emptyPresentation, err := integrity.SignVerifiablePresentationJWT(holderSigner, integrity.JWTVVPParameters{Audience: []string{holderSigner.ID}}, testPresentation) + assert.NoError(tttt, err) + + value := newRequestValue(tttt, router.VerifyPresentationRequest{PresentationJWT: keyaccess.JWTPtr(string(emptyPresentation))}) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/presentations/verification"), value) + w := httptest.NewRecorder() + c := newRequestContext(w, req) + presRouter.VerifyPresentation(c) + assert.True(tttt, util.Is2xxResponse(w.Code)) + + var resp router.VerifyPresentationResponse + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.True(tttt, resp.Verified) + }) + }) + + tt.Run("Create, Get, and Delete Presentation Definition", func(ttt *testing.T) { + s := test.ServiceStorage(ttt) + pRouter, _ := setupPresentationRouter(ttt, s) var createdID string { - resp := createPresentationDefinition(tt, pRouter, WithInputDescriptors(inputDescriptors)) + resp := createPresentationDefinition(ttt, pRouter, WithInputDescriptors(inputDescriptors)) if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { - t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) + ttt.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) } createdID = resp.PresentationDefinition.ID @@ -75,13 +206,13 @@ func TestPresentationAPI(t *testing.T) { w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) pRouter.GetDefinition(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(ttt, util.Is2xxResponse(w.Code)) var resp router.GetPresentationDefinitionResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(tt, createdID, resp.PresentationDefinition.ID) + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Equal(ttt, createdID, resp.PresentationDefinition.ID) if diff := cmp.Diff(*pd, resp.PresentationDefinition, cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { - t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) + ttt.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) } } { @@ -91,14 +222,14 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContext(w, req) pRouter.ListDefinitions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(ttt, util.Is2xxResponse(w.Code)) var resp router.ListDefinitionsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(tt, resp.Definitions, 1) - assert.Equal(tt, createdID, resp.Definitions[0].ID) + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(ttt, resp.Definitions, 1) + assert.Equal(ttt, createdID, resp.Definitions[0].ID) if diff := cmp.Diff(pd, resp.Definitions[0], cmpopts.IgnoreFields(exchange.PresentationDefinition{}, "ID")); diff != "" { - t.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) + ttt.Errorf("PresentationDefinition mismatch (-want +got):\n%s", diff) } } { @@ -107,7 +238,7 @@ func TestPresentationAPI(t *testing.T) { w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) pRouter.DeleteDefinition(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(ttt, util.Is2xxResponse(w.Code)) } { // And we cannot get the PD after it's been deleted. @@ -115,11 +246,11 @@ func TestPresentationAPI(t *testing.T) { w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) pRouter.GetDefinition(c) - assert.Contains(tt, w.Body.String(), "not found") + assert.Contains(ttt, w.Body.String(), "not found") } }) - t.Run("List presentation requests returns empty", func(tt *testing.T) { + tt.Run("List presentation requests returns empty", func(tt *testing.T) { s := test.ServiceStorage(tt) pRouter, _ := setupPresentationRouter(tt, s) @@ -134,12 +265,12 @@ func TestPresentationAPI(t *testing.T) { assert.Empty(tt, resp.Requests) }) - t.Run("Get presentation requests returns created request", func(tt *testing.T) { - s := test.ServiceStorage(tt) - pRouter, didService := setupPresentationRouter(tt, s) - issuerDID := createDID(tt, didService) - def := createPresentationDefinition(tt, pRouter) - req1 := createPresentationRequest(tt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) + tt.Run("Get presentation requests returns created request", func(ttt *testing.T) { + s := test.ServiceStorage(ttt) + pRouter, didService := setupPresentationRouter(ttt, s) + issuerDID := createDID(ttt, didService) + def := createPresentationDefinition(ttt, pRouter) + req1 := createPresentationRequest(ttt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/requests/"+req1.Request.ID, nil) w := httptest.NewRecorder() @@ -148,39 +279,39 @@ func TestPresentationAPI(t *testing.T) { } c := newRequestContextWithParams(w, req, params) pRouter.GetRequest(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(ttt, util.Is2xxResponse(w.Code)) var resp router.GetRequestResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(tt, req1.Request, resp.Request) - assert.Equal(tt, "my_callback_url", resp.Request.CallbackURL) + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Equal(ttt, req1.Request, resp.Request) + assert.Equal(ttt, "my_callback_url", resp.Request.CallbackURL) }) - t.Run("List presentation requests returns many requests", func(tt *testing.T) { - s := test.ServiceStorage(tt) - pRouter, didService := setupPresentationRouter(tt, s) - issuerDID := createDID(tt, didService) - def := createPresentationDefinition(tt, pRouter) - req1 := createPresentationRequest(tt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) - req2 := createPresentationRequest(tt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) + tt.Run("List presentation requests returns many requests", func(ttt *testing.T) { + s := test.ServiceStorage(ttt) + pRouter, didService := setupPresentationRouter(ttt, s) + issuerDID := createDID(ttt, didService) + def := createPresentationDefinition(ttt, pRouter) + req1 := createPresentationRequest(ttt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) + req2 := createPresentationRequest(ttt, pRouter, def.PresentationDefinition.ID, issuerDID.DID) req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/requests", nil) w := httptest.NewRecorder() c := newRequestContext(w, req) pRouter.ListRequests(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(ttt, util.Is2xxResponse(w.Code)) var resp router.ListPresentationRequestsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(tt, resp.Requests, 2) - assert.ElementsMatch(tt, resp.Requests, []model.Request{ + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(ttt, resp.Requests, 2) + assert.ElementsMatch(ttt, resp.Requests, []model.Request{ *req1.Request, *req2.Request, }) - assert.Equal(tt, "my_callback_url", resp.Requests[0].CallbackURL) + assert.Equal(ttt, "my_callback_url", resp.Requests[0].CallbackURL) }) - t.Run("List definitions returns empty", func(tt *testing.T) { + tt.Run("List definitions returns empty", func(tt *testing.T) { s := test.ServiceStorage(tt) pRouter, _ := setupPresentationRouter(tt, s) @@ -195,82 +326,82 @@ func TestPresentationAPI(t *testing.T) { assert.Empty(tt, resp.Definitions) }) - t.Run("List definitions returns many definitions", func(tt *testing.T) { - s := test.ServiceStorage(tt) - pRouter, _ := setupPresentationRouter(tt, s) - def1 := createPresentationDefinition(tt, pRouter) - def2 := createPresentationDefinition(tt, pRouter) + tt.Run("List definitions returns many definitions", func(ttt *testing.T) { + s := test.ServiceStorage(ttt) + pRouter, _ := setupPresentationRouter(ttt, s) + def1 := createPresentationDefinition(ttt, pRouter) + def2 := createPresentationDefinition(ttt, pRouter) req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/definitions", nil) w := httptest.NewRecorder() c := newRequestContext(w, req) pRouter.ListDefinitions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(ttt, util.Is2xxResponse(w.Code)) var resp router.ListDefinitionsResponse - assert.NoError(tt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(tt, resp.Definitions, 2) - assert.ElementsMatch(tt, resp.Definitions, []*exchange.PresentationDefinition{ + assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(ttt, resp.Definitions, 2) + assert.ElementsMatch(ttt, resp.Definitions, []*exchange.PresentationDefinition{ &def1.PresentationDefinition, &def2.PresentationDefinition, }) }) - t.Run("Create returns error without input descriptors", func(tt *testing.T) { - s := test.ServiceStorage(tt) - pRouter, _ := setupPresentationRouter(tt, s) + tt.Run("Create returns error without input descriptors", func(ttt *testing.T) { + s := test.ServiceStorage(ttt) + pRouter, _ := setupPresentationRouter(ttt, s) request := router.CreatePresentationDefinitionRequest{} - value := newRequestValue(tt, request) + value := newRequestValue(ttt, request) req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/definitions", value) w := httptest.NewRecorder() c := newRequestContext(w, req) pRouter.CreateDefinition(c) - assert.Contains(t, w.Body.String(), "inputDescriptors is a required field") + assert.Contains(ttt, w.Body.String(), "inputDescriptors is a required field") }) - t.Run("Get without an ID returns error", func(tt *testing.T) { - s := test.ServiceStorage(tt) - pRouter, _ := setupPresentationRouter(tt, s) + tt.Run("Get without an ID returns error", func(ttt *testing.T) { + s := test.ServiceStorage(ttt) + pRouter, _ := setupPresentationRouter(ttt, s) req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil) w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"id": pd.ID}) pRouter.GetDefinition(c) - assert.Contains(t, w.Body.String(), "not found") + assert.Contains(ttt, w.Body.String(), "not found") }) - t.Run("Delete without an ID returns error", func(tt *testing.T) { - s := test.ServiceStorage(tt) - pRouter, _ := setupPresentationRouter(tt, s) + tt.Run("Delete without an ID returns error", func(ttt *testing.T) { + s := test.ServiceStorage(ttt) + pRouter, _ := setupPresentationRouter(ttt, s) req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/presentations/definitions/%s", pd.ID), nil) w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"id": pd.ID}) pRouter.DeleteDefinition(c) - assert.Contains(t, w.Body.String(), fmt.Sprintf("could not delete presentation definition with id: %s", pd.ID)) + assert.Contains(ttt, w.Body.String(), fmt.Sprintf("could not delete presentation definition with id: %s", pd.ID)) }) - t.Run("Submission endpoints", func(tt *testing.T) { - tt.Run("Get non-existing ID returns error", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) + tt.Run("Submission endpoints", func(ttt *testing.T) { + ttt.Run("Get non-existing ID returns error", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, _ := setupPresentationRouter(tttt, s) req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions/myrandomid", nil) w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"id": "myrandomid"}) pRouter.GetSubmission(c) - assert.Contains(ttt, w.Body.String(), "not found") + assert.Contains(tttt, w.Body.String(), "not found") }) - tt.Run("Get returns submission after creation", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) + ttt.Run("Get returns submission after creation", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - op := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + op := createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "McLovin", "dateOfBirth": "1987-01-02", @@ -284,23 +415,23 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContextWithParams(w, req, map[string]string{"id": opstorage.StatusObjectID(op.ID)}) pRouter.GetSubmission(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(tttt, util.Is2xxResponse(w.Code)) var resp router.GetSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(ttt, opstorage.StatusObjectID(op.ID), resp.GetSubmission().ID) - assert.Equal(ttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) - assert.Equal(ttt, "pending", resp.Submission.Status) + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Equal(tttt, opstorage.StatusObjectID(op.ID), resp.GetSubmission().ID) + assert.Equal(tttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) + assert.Equal(tttt, "pending", resp.Submission.Status) }) - tt.Run("Create well formed submission returns operation", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) + ttt.Run("Create well formed submission returns operation", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - request := createSubmissionRequest(ttt, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + request := createSubmissionRequest(tttt, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "McLovin", "dateOfBirth": "1987-01-02", @@ -309,36 +440,36 @@ func TestPresentationAPI(t *testing.T) { "id": "did:web:andresuribe.com", })), holderSigner, holderDID) - value := newRequestValue(ttt, request) + value := newRequestValue(tttt, request) req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/presentations/submissions", value) w := httptest.NewRecorder() c := newRequestContext(w, req) pRouter.CreateSubmission(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(tttt, util.Is2xxResponse(w.Code)) var resp router.Operation - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Contains(ttt, resp.ID, "presentations/submissions/") - assert.False(ttt, resp.Done) - assert.Zero(ttt, resp.Result) + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Contains(tttt, resp.ID, "presentations/submissions/") + assert.False(tttt, resp.Done) + assert.Zero(tttt, resp.Result) }) - tt.Run("Review submission returns approved submission", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) + ttt.Run("Review submission returns approved submission", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + submissionOp := createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) request := router.ReviewSubmissionRequest{ Approved: true, Reason: "because I want to", } - value := newRequestValue(ttt, request) + value := newRequestValue(tttt, request) createdID := opstorage.StatusObjectID(submissionOp.ID) req := httptest.NewRequest( http.MethodPut, @@ -348,33 +479,33 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) pRouter.ReviewSubmission(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(tttt, util.Is2xxResponse(w.Code)) var resp router.ReviewSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Equal(ttt, "because I want to", resp.Reason) - assert.NotEmpty(ttt, resp.GetSubmission().ID) - assert.Equal(ttt, "approved", resp.Status) - assert.Equal(ttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Equal(tttt, "because I want to", resp.Reason) + assert.NotEmpty(tttt, resp.GetSubmission().ID) + assert.Equal(tttt, "approved", resp.Status) + assert.Equal(tttt, definition.PresentationDefinition.ID, resp.GetSubmission().DefinitionID) }) - tt.Run("Review submission twice fails", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) + ttt.Run("Review submission twice fails", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - submissionOp := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + submissionOp := createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential(), holderDID, holderSigner) createdID := opstorage.StatusObjectID(submissionOp.ID) - _ = reviewSubmission(ttt, pRouter, createdID) + _ = reviewSubmission(tttt, pRouter, createdID) request := router.ReviewSubmissionRequest{ Approved: true, Reason: "because I want to review again", } - value := newRequestValue(ttt, request) + value := newRequestValue(tttt, request) req := httptest.NewRequest( http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions/%s/review", createdID), @@ -382,28 +513,28 @@ func TestPresentationAPI(t *testing.T) { w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"id": createdID}) pRouter.ReviewSubmission(c) - assert.Contains(ttt, w.Body.String(), "operation already marked as done") + assert.Contains(tttt, w.Body.String(), "operation already marked as done") }) - tt.Run("List submissions returns empty when there are none", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) + ttt.Run("List submissions returns empty when there are none", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, _ := setupPresentationRouter(tttt, s) req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/presentations/submissions", nil) w := httptest.NewRecorder() c := newRequestContext(w, req) pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(tttt, util.Is2xxResponse(w.Code)) var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(ttt, resp.Submissions) + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(tttt, resp.Submissions) }) - tt.Run("List submissions invalid page size fails", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) + ttt.Run("List submissions invalid page size fails", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, _ := setupPresentationRouter(tttt, s) w := httptest.NewRecorder() badParams := url.Values{ @@ -413,12 +544,12 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContext(w, req) pRouter.ListSubmissions(c) - assert.Contains(tt, w.Body.String(), "'pageSize' must be greater than 0") + assert.Contains(tttt, w.Body.String(), "'pageSize' must be greater than 0") }) - tt.Run("List submissions made up token fails", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) + ttt.Run("List submissions made up token fails", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, _ := setupPresentationRouter(tttt, s) w := httptest.NewRecorder() badParams := url.Values{ @@ -429,22 +560,22 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContext(w, req) pRouter.ListSubmissions(c) - assert.Contains(tt, w.Body.String(), "token value cannot be decoded") + assert.Contains(tttt, w.Body.String(), "token value cannot be decoded") }) - tt.Run("List submissions pagination", func(ttt *testing.T) { + ttt.Run("List submissions pagination", func(tttt *testing.T) { // TODO: Fix pagesize issue on redis - https://github.com/TBD54566975/ssi-service/issues/538 if strings.Contains(test.Name, "Redis") { - ttt.Skip("skipping pagination test for Redis") + tttt.Skip("skipping pagination test for Redis") } - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) - authorDID := createDID(ttt, didService) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - _ = createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + _ = createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "McLovin", "dateOfBirth": "1987-01-02", @@ -453,8 +584,8 @@ func TestPresentationAPI(t *testing.T) { "id": "did:web:andresuribe.com", })), holderDID, holderSigner) - mrTeeSigner, mrTeeDID := getSigner(ttt) - _ = createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + mrTeeSigner, mrTeeDID := getSigner(tttt) + _ = createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "Mr. T", "dateOfBirth": "1999-01-02", @@ -473,9 +604,9 @@ func TestPresentationAPI(t *testing.T) { var listSubmissionResponse router.ListSubmissionResponse err := json.NewDecoder(w.Body).Decode(&listSubmissionResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, listSubmissionResponse.NextPageToken) - assert.Len(tt, listSubmissionResponse.Submissions, 1) + assert.NoError(tttt, err) + assert.NotEmpty(tttt, listSubmissionResponse.NextPageToken) + assert.Len(tttt, listSubmissionResponse.Submissions, 1) w = httptest.NewRecorder() params["pageToken"] = []string{listSubmissionResponse.NextPageToken} @@ -486,24 +617,24 @@ func TestPresentationAPI(t *testing.T) { var listSubmissionsResponse2 router.ListSubmissionResponse err = json.NewDecoder(w.Body).Decode(&listSubmissionsResponse2) - assert.NoError(tt, err) - assert.Empty(tt, listSubmissionsResponse2.NextPageToken) - assert.Len(tt, listSubmissionsResponse2.Submissions, 1) + assert.NoError(tttt, err) + assert.Empty(tttt, listSubmissionsResponse2.NextPageToken) + assert.Len(tttt, listSubmissionsResponse2.Submissions, 1) }) - tt.Run("List submissions pagination change query between calls returns error", func(ttt *testing.T) { + ttt.Run("List submissions pagination change query between calls returns error", func(tttt *testing.T) { // TODO: Fix pagesize issue on redis - https://github.com/TBD54566975/ssi-service/issues/538 if strings.Contains(test.Name, "Redis") { - ttt.Skip("skipping pagination test for Redis") + tttt.Skip("skipping pagination test for Redis") } - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) - authorDID := createDID(ttt, didService) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - _ = createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + _ = createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "McLovin", "dateOfBirth": "1987-01-02", @@ -512,8 +643,8 @@ func TestPresentationAPI(t *testing.T) { "id": "did:web:andresuribe.com", })), holderDID, holderSigner) - mrTeeSigner, mrTeeDID := getSigner(ttt) - _ = createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + mrTeeSigner, mrTeeDID := getSigner(tttt) + _ = createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "Mr. T", "dateOfBirth": "1999-01-02", @@ -532,9 +663,9 @@ func TestPresentationAPI(t *testing.T) { var listSubmissionResponse router.ListSubmissionResponse err := json.NewDecoder(w.Body).Decode(&listSubmissionResponse) - assert.NoError(tt, err) - assert.NotEmpty(tt, listSubmissionResponse.NextPageToken) - assert.Len(tt, listSubmissionResponse.Submissions, 1) + assert.NoError(tttt, err) + assert.NotEmpty(tttt, listSubmissionResponse.NextPageToken) + assert.Len(tttt, listSubmissionResponse.Submissions, 1) w = httptest.NewRecorder() params["pageToken"] = []string{listSubmissionResponse.NextPageToken} @@ -543,18 +674,18 @@ func TestPresentationAPI(t *testing.T) { c = newRequestContext(w, req) pRouter.ListSubmissions(c) - assert.Equal(tt, http.StatusBadRequest, w.Result().StatusCode) - assert.Contains(tt, w.Body.String(), "page token must be for the same query") + assert.Equal(tttt, http.StatusBadRequest, w.Result().StatusCode) + assert.Contains(tttt, w.Body.String(), "page token must be for the same query") }) - tt.Run("List submissions returns many submissions", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) + ttt.Run("List submissions returns many submissions", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - op := createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + op := createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "McLovin", "dateOfBirth": "1987-01-02", @@ -563,8 +694,8 @@ func TestPresentationAPI(t *testing.T) { "id": "did:web:andresuribe.com", })), holderDID, holderSigner) - mrTeeSigner, mrTeeDID := getSigner(ttt) - op2 := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + mrTeeSigner, mrTeeDID := getSigner(tttt) + op2 := createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "Mr. T", "dateOfBirth": "1999-01-02", @@ -577,11 +708,11 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContext(w, req) pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(tttt, util.Is2xxResponse(w.Code)) var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Len(ttt, resp.Submissions, 2) + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Len(tttt, resp.Submissions, 2) expectedSubmissions := []model.Submission{ { @@ -608,12 +739,12 @@ func TestPresentationAPI(t *testing.T) { }), ) if diff != "" { - ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) + tttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) } - assert.Len(ttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) - assert.Len(ttt, resp.Submissions[1].VerifiablePresentation.VerifiableCredential, 1) + assert.Len(tttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) + assert.Len(tttt, resp.Submissions[1].VerifiablePresentation.VerifiableCredential, 1) - assert.ElementsMatch(ttt, + assert.ElementsMatch(tttt, []string{ opstorage.StatusObjectID(op.ID), opstorage.StatusObjectID(op2.ID)}, @@ -621,7 +752,7 @@ func TestPresentationAPI(t *testing.T) { resp.Submissions[0].GetSubmission().ID, resp.Submissions[1].GetSubmission().ID, }) - assert.Equal(ttt, + assert.Equal(tttt, []string{ definition.PresentationDefinition.ID, definition.PresentationDefinition.ID, @@ -632,9 +763,9 @@ func TestPresentationAPI(t *testing.T) { }) }) - tt.Run("bad filter returns error", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, _ := setupPresentationRouter(ttt, s) + ttt.Run("bad filter returns error", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, _ := setupPresentationRouter(tttt, s) query := url.QueryEscape("im a baaad filter that's trying to break a lot of stuff") req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/presentations/submissions?filter=%s", query), nil) @@ -642,17 +773,17 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) pRouter.ListSubmissions(c) - assert.Contains(ttt, w.Body.String(), "invalid filter") + assert.Contains(tttt, w.Body.String(), "invalid filter") }) - tt.Run("List submissions filters based on status", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) + ttt.Run("List submissions filters based on status", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - op := createSubmission(ttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + op := createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "McLovin", "dateOfBirth": "1987-01-02", @@ -667,10 +798,10 @@ func TestPresentationAPI(t *testing.T) { c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(tttt, util.Is2xxResponse(w.Code)) var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) expectedSubmissions := []model.Submission{ { @@ -689,23 +820,23 @@ func TestPresentationAPI(t *testing.T) { }), ) if diff != "" { - ttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) + tttt.Errorf("Mismatch on submissions (-want +got):\n%s", diff) } - assert.Len(ttt, resp.Submissions, 1) - assert.Len(ttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) - assert.Equal(ttt, opstorage.StatusObjectID(op.ID), resp.Submissions[0].GetSubmission().ID) - assert.Equal(ttt, definition.PresentationDefinition.ID, resp.Submissions[0].GetSubmission().DefinitionID) + assert.Len(tttt, resp.Submissions, 1) + assert.Len(tttt, resp.Submissions[0].VerifiablePresentation.VerifiableCredential, 1) + assert.Equal(tttt, opstorage.StatusObjectID(op.ID), resp.Submissions[0].GetSubmission().ID) + assert.Equal(tttt, definition.PresentationDefinition.ID, resp.Submissions[0].GetSubmission().DefinitionID) }) - tt.Run("List submissions filter returns empty when status does not match", func(ttt *testing.T) { - s := test.ServiceStorage(ttt) - pRouter, didService := setupPresentationRouter(ttt, s) - authorDID := createDID(ttt, didService) + ttt.Run("List submissions filter returns empty when status does not match", func(tttt *testing.T) { + s := test.ServiceStorage(tttt) + pRouter, didService := setupPresentationRouter(tttt, s) + authorDID := createDID(tttt, didService) - holderSigner, holderDID := getSigner(ttt) - definition := createPresentationDefinition(ttt, pRouter) - _ = createSubmission(t, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( + holderSigner, holderDID := getSigner(tttt) + definition := createPresentationDefinition(tttt, pRouter) + _ = createSubmission(tttt, pRouter, definition.PresentationDefinition.ID, authorDID.DID.ID, VerifiableCredential( WithCredentialSubject(credential.CredentialSubject{ "additionalName": "McLovin", "dateOfBirth": "1987-01-02", @@ -719,11 +850,11 @@ func TestPresentationAPI(t *testing.T) { w := httptest.NewRecorder() c := newRequestContextWithParams(w, req, map[string]string{"filter": query}) pRouter.ListSubmissions(c) - assert.True(tt, util.Is2xxResponse(w.Code)) + assert.True(tttt, util.Is2xxResponse(w.Code)) var resp router.ListSubmissionResponse - assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp)) - assert.Empty(ttt, resp.Submissions) + assert.NoError(tttt, json.NewDecoder(w.Body).Decode(&resp)) + assert.Empty(tttt, resp.Submissions) }) }) }) diff --git a/pkg/service/credential/service.go b/pkg/service/credential/service.go index 7f8f02290..296de0592 100644 --- a/pkg/service/credential/service.go +++ b/pkg/service/credential/service.go @@ -19,6 +19,7 @@ import ( credint "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/internal/verification" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/keystore" "github.com/tbd54566975/ssi-service/pkg/service/schema" @@ -28,7 +29,7 @@ import ( type Service struct { storage *Storage config config.CredentialServiceConfig - verifier *credint.Validator + verifier *verification.Verifier // external dependencies keyStore *keystore.Service @@ -72,7 +73,7 @@ func NewCredentialService(config config.CredentialServiceConfig, s storage.Servi if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate storage for the credential service") } - verifier, err := credint.NewCredentialValidator(didResolver, schema) + verifier, err := verification.NewVerifiableDataVerifier(didResolver, schema) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate verifier for the credential service") } @@ -314,7 +315,6 @@ type VerifyCredentialResponse struct { // 3. Makes sure the credential complies with the VC Data Model // 4. If the credential has a schema, makes sure its data complies with the schema // LATER: Makes sure the credential has not been revoked, other checks. -// Note: https://github.com/TBD54566975/ssi-sdk/issues/213 func (s Service) VerifyCredential(ctx context.Context, request VerifyCredentialRequest) (*VerifyCredentialResponse, error) { logrus.Debugf("verifying credential: %+v", request) diff --git a/pkg/service/did/ion.go b/pkg/service/did/ion.go index be8afc316..f817defaa 100644 --- a/pkg/service/did/ion.go +++ b/pkg/service/did/ion.go @@ -17,6 +17,7 @@ import ( "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/pkg/service/keystore" ) diff --git a/pkg/service/presentation/service.go b/pkg/service/presentation/service.go index 30f182bb2..d3b99aa65 100644 --- a/pkg/service/presentation/service.go +++ b/pkg/service/presentation/service.go @@ -12,9 +12,9 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/tbd54566975/ssi-service/internal/credential" didint "github.com/tbd54566975/ssi-service/internal/did" "github.com/tbd54566975/ssi-service/internal/keyaccess" + "github.com/tbd54566975/ssi-service/internal/verification" "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/keystore" @@ -35,7 +35,7 @@ type Service struct { opsStorage *operation.Storage resolver resolution.Resolver schema *schema.Service - verifier *credential.Validator + verifier *verification.Verifier reqStorage common.RequestStorage } @@ -67,7 +67,7 @@ func NewPresentationService(s storage.ServiceStorage, if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate storage for the operations") } - verifier, err := credential.NewCredentialValidator(resolver, schema) + verifier, err := verification.NewVerifiableDataVerifier(resolver, schema) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate verifier") } @@ -87,6 +87,37 @@ func NewPresentationService(s storage.ServiceStorage, return &service, nil } +type VerifyPresentationRequest struct { + PresentationJWT *keyaccess.JWT `json:"presentationJwt,omitempty" validate:"required"` +} + +type VerifyPresentationResponse struct { + Verified bool `json:"verified"` + Reason string `json:"reason,omitempty"` +} + +// VerifyPresentation does a series of verification on a presentation: +// 1. Makes sure the presentation has a valid signature +// 2. Makes sure the presentation is not expired +// 3. Makes sure the presentation complies with the VC Data Model v1.1 +// 4. For each verification in the presentation, makes sure: +// a. Makes sure the verification has a valid signature +// b. Makes sure the verification is not expired +// c. Makes sure the verification complies with the VC Data Model +func (s Service) VerifyPresentation(ctx context.Context, request VerifyPresentationRequest) (*VerifyPresentationResponse, error) { + logrus.Debugf("verifying presentation: %+v", request) + + if err := sdkutil.IsValidStruct(request); err != nil { + return nil, sdkutil.LoggingErrorMsg(err, "invalid verify presentation request") + } + + if err := s.verifier.VerifyJWTPresentation(ctx, *request.PresentationJWT); err != nil { + return &VerifyPresentationResponse{Verified: false, Reason: err.Error()}, nil + } + + return &VerifyPresentationResponse{Verified: true}, nil +} + // CreatePresentationDefinition houses the main service logic for presentation definition creation. It validates the input, and // produces a presentation definition value that conforms with the PresentationDefinition specification. func (s Service) CreatePresentationDefinition(ctx context.Context, diff --git a/pkg/service/well-known/did_configuration.go b/pkg/service/well-known/did_configuration.go index 555b815c7..568b666c6 100644 --- a/pkg/service/well-known/did_configuration.go +++ b/pkg/service/well-known/did_configuration.go @@ -15,31 +15,33 @@ import ( "github.com/TBD54566975/ssi-sdk/did/resolution" "github.com/goccy/go-json" "github.com/pkg/errors" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + credint "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/internal/verification" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/keystore" "github.com/tbd54566975/ssi-service/pkg/service/schema" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) type DIDConfigurationService struct { keyStoreService *keystore.Service - validator *credint.Validator + validator *verification.Verifier HTTPClient *http.Client } func NewDIDConfigurationService(keyStoreService *keystore.Service, didResolver resolution.Resolver, schema *schema.Service) (*DIDConfigurationService, error) { client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} - validator, err := credint.NewCredentialValidator(didResolver, schema) + verifier, err := verification.NewVerifiableDataVerifier(didResolver, schema) if err != nil { - return nil, errors.Wrap(err, "could not instantiate validator for the credential service") + return nil, errors.Wrap(err, "could not instantiate verifier for the credential service") } return &DIDConfigurationService{ keyStoreService: keyStoreService, - validator: validator, + validator: verifier, HTTPClient: client, }, nil } @@ -120,7 +122,7 @@ func (s DIDConfigurationService) VerifyDIDConfiguration(ctx context.Context, req // 4. The implementer MUST perform DID resolution on the DID specified in the Issuer of the Domain Linkage Credential to obtain the associated DID document. // 5. Using the retrieved DID document, the implementer MUST validate the signature of the Domain Linkage Credential against key material referenced in the assertionMethod section of the DID document. - if err := s.validator.Verify(ctx, domainLinkageCredential); err != nil { + if err := s.validator.VerifyCredential(ctx, domainLinkageCredential); err != nil { response.Reason = err.Error() return &response, nil } From 028dbf93be843864a9db56e4375d85bff148ca87 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Thu, 3 Aug 2023 17:50:46 -0400 Subject: [PATCH 18/20] Support adding/removing services and keys to an ion DID (#562) * Support adding/removing services and keys to an ion DID * Reviews * Fixes 560 where multiple instances of the service used different encryption keys * Factories, refactoring, and integration test. * Move to actually dealing with patches. * spec * 200 and spec * Moving around bits --- doc/swagger.yaml | 102 +++++ integration/common.go | 9 + integration/didion_integration_test.go | 18 +- .../testdata/update-did-ion-input.json | 29 ++ pkg/server/router/did.go | 95 +++++ pkg/server/router/did_test.go | 6 +- pkg/server/router/testutils_test.go | 2 +- pkg/server/server.go | 1 + pkg/server/server_did_test.go | 138 +++++- pkg/server/server_test.go | 2 +- pkg/service/did/ion.go | 397 ++++++++++++++++-- pkg/service/did/ion_test.go | 20 +- pkg/service/did/model.go | 24 ++ pkg/service/did/service.go | 30 +- pkg/service/service.go | 2 +- 15 files changed, 817 insertions(+), 58 deletions(-) create mode 100644 integration/testdata/update-did-ion-input.json diff --git a/doc/swagger.yaml b/doc/swagger.yaml index f750e439c..93b92bb0c 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -875,6 +875,33 @@ definitions: description: Whether the DIDConfiguration was verified. type: boolean type: object + ion.PublicKey: + properties: + id: + type: string + publicKeyJwk: + $ref: '#/definitions/jwx.PublicKeyJWK' + purposes: + items: + $ref: '#/definitions/ion.PublicKeyPurpose' + type: array + type: + type: string + type: object + ion.PublicKeyPurpose: + enum: + - authentication + - assertionMethod + - capabilityInvocation + - capabilityDelegation + - keyAgreement + type: string + x-enum-varnames: + - Authentication + - AssertionMethod + - CapabilityInvocation + - CapabilityDelegation + - KeyAgreement jwx.PublicKeyJWK: properties: alg: @@ -1904,6 +1931,25 @@ definitions: id: type: string type: object + pkg_server_router.StateChange: + properties: + publicKeyIdsToRemove: + items: + type: string + type: array + publicKeysToAdd: + items: + $ref: '#/definitions/ion.PublicKey' + type: array + serviceIdsToRemove: + items: + type: string + type: array + servicesToAdd: + items: + $ref: '#/definitions/github_com_TBD54566975_ssi-sdk_did.Service' + type: array + type: object pkg_server_router.StoreKeyRequest: properties: base58PrivateKey: @@ -1971,6 +2017,21 @@ definitions: suspended: type: boolean type: object + pkg_server_router.UpdateDIDByMethodRequest: + properties: + stateChange: + allOf: + - $ref: '#/definitions/pkg_server_router.StateChange' + description: Expected to be populated when `method == "ion"`. Describes the + changes that are requested. + required: + - stateChange + type: object + pkg_server_router.UpdateDIDByMethodResponse: + properties: + did: + $ref: '#/definitions/did.Document' + type: object pkg_server_router.VerifyCredentialRequest: properties: credential: @@ -2803,6 +2864,47 @@ paths: summary: Get a DID tags: - DecentralizedIdentifiers + put: + consumes: + - application/json + description: |- + Updates a DID for which SSI is the custodian. The DID must have been previously created by calling + the "Create DID Document" endpoint. Currently, only ION dids support updates. + parameters: + - description: Method + in: path + name: method + required: true + type: string + - description: ID + in: path + name: id + required: true + type: string + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/pkg_server_router.UpdateDIDByMethodRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_server_router.UpdateDIDByMethodResponse' + "400": + description: Bad request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Updates a DID document. + tags: + - DecentralizedIdentityAPI /v1/dids/{method}/batch: put: consumes: diff --git a/integration/common.go b/integration/common.go index 09628ce52..3070a9dc3 100644 --- a/integration/common.go +++ b/integration/common.go @@ -123,6 +123,15 @@ func CreateDIDION() (string, error) { return output, nil } +func UpdateDIDION(id string) (string, error) { + output, err := put(endpoint+version+"dids/ion/"+id, getJSONFromFile("update-did-ion-input.json")) + if err != nil { + return "", errors.Wrapf(err, "did endpoint with output: %s", output) + } + + return output, nil +} + func ListWebDIDs() (string, error) { urlValues := url.Values{ "pageSize": []string{"10"}, diff --git a/integration/didion_integration_test.go b/integration/didion_integration_test.go index 11778f8b2..249f5cbff 100644 --- a/integration/didion_integration_test.go +++ b/integration/didion_integration_test.go @@ -6,7 +6,6 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" "github.com/TBD54566975/ssi-sdk/did/key" "github.com/stretchr/testify/assert" - "github.com/tbd54566975/ssi-service/pkg/service/operation/storage" ) @@ -68,6 +67,23 @@ func TestCreateIssuerDIDIONIntegration(t *testing.T) { assert.Equal(t, "test-kid", verificationMethod2KID) } +func TestUpdateDIDIONIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + didIONOutput, err := CreateDIDION() + assert.NoError(t, err) + + issuerDID, err := getJSONElement(didIONOutput, "$.did.id") + assert.NoError(t, err) + assert.Contains(t, issuerDID, "did:ion") + + // Because ION nodes do not allow updates immediately after creation of a DID, we expect the following error code. + _, err = UpdateDIDION(issuerDID) + assert.Error(t, err) + assert.ErrorContains(t, err, "queueing_multiple_operations_per_did_not_allowed") +} + func TestCreateAliceDIDKeyForDIDIONIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/integration/testdata/update-did-ion-input.json b/integration/testdata/update-did-ion-input.json new file mode 100644 index 000000000..ffc301e94 --- /dev/null +++ b/integration/testdata/update-did-ion-input.json @@ -0,0 +1,29 @@ +{ + "stateChange": { + "publicKeysToAdd": [ + { + "id": "publicKeyModel1Id", + "type": "JsonWebKey2020", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "Z4Y3NNOxv0J6tCgqOBFnHnaZhJF6LdulT7z8A-2D5_8", + "y": "i5a2NtJoUKXkLm6q8nOEu9WOkso1Ag6FTUT6k_LMnGk" + }, + "purposes": [ + "authentication" + ] + }, + { + "id": "publicKeyModel2Id", + "type": "JsonWebKey2020", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ" + }, + "purposes": ["keyAgreement"] + } + ] + } +} \ No newline at end of file diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 28cddeeab..208ee4744 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -7,6 +7,7 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/did/ion" "github.com/TBD54566975/ssi-sdk/did/resolution" "github.com/gin-gonic/gin" "github.com/goccy/go-json" @@ -127,6 +128,100 @@ func (dr DIDRouter) CreateDIDByMethod(c *gin.Context) { framework.Respond(c, resp, http.StatusCreated) } +type StateChange struct { + ServicesToAdd []didsdk.Service `json:"servicesToAdd,omitempty"` + ServiceIDsToRemove []string `json:"serviceIdsToRemove,omitempty"` + PublicKeysToAdd []ion.PublicKey `json:"publicKeysToAdd,omitempty"` + PublicKeyIDsToRemove []string `json:"publicKeyIdsToRemove"` +} + +type UpdateDIDByMethodRequest struct { + // Expected to be populated when `method == "ion"`. Describes the changes that are requested. + StateChange StateChange `json:"stateChange" validate:"required"` +} + +type UpdateDIDByMethodResponse struct { + DID didsdk.Document `json:"did,omitempty"` +} + +// UpdateDIDByMethod godoc +// +// @Summary Updates a DID document. +// @Description Updates a DID for which SSI is the custodian. The DID must have been previously created by calling +// @Description the "Create DID Document" endpoint. Currently, only ION dids support updates. +// @Tags DecentralizedIdentityAPI +// @Accept json +// @Produce json +// @Param method path string true "Method" +// @Param id path string true "ID" +// @Param request body UpdateDIDByMethodRequest true "request body" +// @Success 200 {object} UpdateDIDByMethodResponse +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /v1/dids/{method}/{id} [put] +func (dr DIDRouter) UpdateDIDByMethod(c *gin.Context) { + method := framework.GetParam(c, MethodParam) + if method == nil { + errMsg := "update DID by method request missing method parameter" + framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest) + return + } + if *method != didsdk.IONMethod.String() { + framework.LoggingRespondErrMsg(c, "ion is the only method supported", http.StatusBadRequest) + } + + id := framework.GetParam(c, IDParam) + if id == nil { + errMsg := fmt.Sprintf("update DID request missing id parameter for method: %s", *method) + framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest) + return + } + var request UpdateDIDByMethodRequest + invalidRequest := "invalid update DID request" + if err := framework.Decode(c.Request, &request); err != nil { + framework.LoggingRespondErrWithMsg(c, err, invalidRequest, http.StatusBadRequest) + return + } + + if err := framework.ValidateRequest(request); err != nil { + framework.LoggingRespondErrWithMsg(c, err, invalidRequest, http.StatusBadRequest) + return + } + + updateDIDRequest, err := toUpdateIONDIDRequest(*id, request) + if err != nil { + errMsg := fmt.Sprintf("%s: could not update DID for method<%s>", invalidRequest, *method) + framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusBadRequest) + return + } + updateIONDIDResponse, err := dr.service.UpdateIONDID(c, *updateDIDRequest) + if err != nil { + errMsg := fmt.Sprintf("could not update DID for method<%s>", *method) + framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError) + return + } + + resp := CreateDIDByMethodResponse{DID: updateIONDIDResponse.DID} + framework.Respond(c, resp, http.StatusOK) + +} + +func toUpdateIONDIDRequest(id string, request UpdateDIDByMethodRequest) (*did.UpdateIONDIDRequest, error) { + didION := ion.ION(id) + if !didION.IsValid() { + return nil, errors.Errorf("invalid ion did %s", id) + } + return &did.UpdateIONDIDRequest{ + DID: didION, + StateChange: ion.StateChange{ + ServicesToAdd: request.StateChange.ServicesToAdd, + ServiceIDsToRemove: request.StateChange.ServiceIDsToRemove, + PublicKeysToAdd: request.StateChange.PublicKeysToAdd, + PublicKeyIDsToRemove: request.StateChange.PublicKeyIDsToRemove, + }, + }, nil +} + // toCreateDIDRequest converts CreateDIDByMethodRequest to did.CreateDIDRequest, parsing options according to method func toCreateDIDRequest(m didsdk.Method, request CreateDIDByMethodRequest) (*did.CreateDIDRequest, error) { createRequest := did.CreateDIDRequest{ diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 6a2c0d23b..41f5f18a6 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -42,7 +42,7 @@ func TestDIDRouter(t *testing.T) { keyStoreService := testKeyStoreService(tt, db) methods := []string{didsdk.KeyMethod.String()} serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} - didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) + didService, err := did.NewDIDService(serviceConfig, db, keyStoreService, nil) assert.NoError(tt, err) assert.NotEmpty(tt, didService) createDID(tt, didService) @@ -84,7 +84,7 @@ func TestDIDRouter(t *testing.T) { keyStoreService := testKeyStoreService(tt, db) methods := []string{didsdk.KeyMethod.String()} serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} - didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) + didService, err := did.NewDIDService(serviceConfig, db, keyStoreService, nil) assert.NoError(tt, err) assert.NotEmpty(tt, didService) @@ -171,7 +171,7 @@ func TestDIDRouter(t *testing.T) { keyStoreService := testKeyStoreService(tt, db) methods := []string{didsdk.KeyMethod.String(), didsdk.WebMethod.String()} serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods} - didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) + didService, err := did.NewDIDService(serviceConfig, db, keyStoreService, nil) assert.NoError(tt, err) assert.NotEmpty(tt, didService) diff --git a/pkg/server/router/testutils_test.go b/pkg/server/router/testutils_test.go index 71d5b8b89..dda9ae049 100644 --- a/pkg/server/router/testutils_test.go +++ b/pkg/server/router/testutils_test.go @@ -46,7 +46,7 @@ func testDIDService(t *testing.T, db storage.ServiceStorage, keyStore *keystore. LocalResolutionMethods: []string{"key"}, } // create a did service - didService, err := did.NewDIDService(serviceConfig, db, keyStore) + didService, err := did.NewDIDService(serviceConfig, db, keyStore, nil) require.NoError(t, err) require.NotEmpty(t, didService) return didService diff --git a/pkg/server/server.go b/pkg/server/server.go index dbc760428..6e3bb2824 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -170,6 +170,7 @@ func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, did didAPI := rg.Group(DIDsPrefix) didAPI.GET("", didRouter.ListDIDMethods) didAPI.PUT("/:method", middleware.Webhook(webhookService, webhook.DID, webhook.Create), didRouter.CreateDIDByMethod) + didAPI.PUT("/:method/:id", didRouter.UpdateDIDByMethod) didAPI.PUT("/:method/batch", middleware.Webhook(webhookService, webhook.DID, webhook.BatchCreate), batchDIDRouter.BatchCreateDIDs) didAPI.GET("/:method", didRouter.ListDIDsByMethod) didAPI.GET("/:method/:id", didRouter.GetDIDByMethod) diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go index 2a2aa4636..32c71c23e 100644 --- a/pkg/server/server_did_test.go +++ b/pkg/server/server_did_test.go @@ -10,7 +10,9 @@ import ( "testing" "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/crypto/jwx" didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/did/ion" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -178,7 +180,7 @@ func TestDIDAPI(t *testing.T) { require.NotEmpty(tt, db) _, keyStoreService, _ := testKeyStore(tt, db) - didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, nil) + didRouter, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, nil) // create DID by method - ion - missing body req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", nil) @@ -188,7 +190,7 @@ func TestDIDAPI(t *testing.T) { } c := newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) + didRouter.CreateDIDByMethod(c) assert.Contains(tt, w.Body.String(), "invalid create DID request") // reset recorder between calls @@ -206,7 +208,7 @@ func TestDIDAPI(t *testing.T) { req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) + didRouter.CreateDIDByMethod(c) assert.True(tt, util.Is2xxResponse(w.Code)) // reset recorder between calls @@ -221,7 +223,7 @@ func TestDIDAPI(t *testing.T) { req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) + didRouter.CreateDIDByMethod(c) assert.Contains(tt, w.Body.String(), "could not create DID for method with key type: bad") // reset recorder between calls @@ -239,16 +241,140 @@ func TestDIDAPI(t *testing.T) { Post("/operations"). Reply(200). BodyString(string(BasicDIDResolution)) - defer gock.Off() c = newRequestContextWithParams(w, req, params) - didService.CreateDIDByMethod(c) + didRouter.CreateDIDByMethod(c) assert.True(tt, util.Is2xxResponse(w.Code)) var resp router.CreateDIDByMethodResponse err := json.NewDecoder(w.Body).Decode(&resp) assert.NoError(tt, err) assert.Contains(tt, resp.DID.ID, didsdk.IONMethod) + + gock.Off() + // good params + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/ion/"+resp.DID.ID, nil) + goodParams := map[string]string{ + "method": "ion", + "id": resp.DID.ID, + } + c = newRequestContextWithParams(w, req, goodParams) + didRouter.GetDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var getDIDResp router.GetDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&getDIDResp) + assert.NoError(tt, err) + assert.Equal(tt, resp.DID.ID, getDIDResp.DID.ID) + }) + + t.Run("Test Update DID By Method: ION", func(tt *testing.T) { + db := test.ServiceStorage(t) + require.NotEmpty(tt, db) + + _, keyStoreService, keyStoreServiceFactory := testKeyStore(tt, db) + didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, keyStoreServiceFactory) + + params := map[string]string{ + "method": "ion", + } + // reset recorder between calls + w := httptest.NewRecorder() + + gock.New(testIONResolverURL). + Post("/operations"). + Reply(200). + JSON(string(BasicDIDResolution)) + defer gock.Off() + + // with body, good key type, no options + createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519} + requestReader := newRequestValue(tt, createDIDRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader) + + c := newRequestContextWithParams(w, req, params) + didService.CreateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var createDIDResponse router.CreateDIDByMethodResponse + err := json.NewDecoder(w.Body).Decode(&createDIDResponse) + assert.NoError(tt, err) + + updateDIDRequest := router.UpdateDIDByMethodRequest{ + StateChange: router.StateChange{ + PublicKeysToAdd: []ion.PublicKey{ + { + ID: "testPublicKeyModel1Id", + Type: "EcdsaSecp256k1VerificationKey2019", + PublicKeyJWK: jwx.PublicKeyJWK{ + KTY: "EC", + CRV: "secp256k1", + X: "tXSKB_rubXS7sCjXqupVJEzTcW3MsjmEvq1YpXn96Zg", + Y: "dOicXqbjFxoGJ-K0-GJ1kHYJqic_D_OMuUwkQ7Ol6nk", + }, + Purposes: []ion.PublicKeyPurpose{ + ion.Authentication, ion.KeyAgreement, + }, + }, + }, + }, + } + w = httptest.NewRecorder() + params["id"] = createDIDResponse.DID.ID + requestReader = newRequestValue(tt, updateDIDRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion/"+createDIDResponse.DID.ID, requestReader) + + gock.New(testIONResolverURL). + Post("/operations"). + Reply(200). + JSON("{}") + defer gock.Off() + + c = newRequestContextWithParams(w, req, params) + didService.UpdateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + updateDIDRequest2 := router.UpdateDIDByMethodRequest{ + StateChange: router.StateChange{ + ServicesToAdd: []didsdk.Service{ + { + ID: "testService1Id", + Type: "testService1Type", + ServiceEndpoint: "http://www.service1.com", + }, + }, + }, + } + w = httptest.NewRecorder() + requestReader = newRequestValue(tt, updateDIDRequest2) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion/"+createDIDResponse.DID.ID, requestReader) + + gock.New(testIONResolverURL). + Post("/operations"). + Reply(200). + JSON("{}") + defer gock.Off() + + c = newRequestContextWithParams(w, req, params) + didService.UpdateDIDByMethod(c) + assert.True(tt, util.Is2xxResponse(w.Code)) + + var updateDIDResponse router.UpdateDIDByMethodResponse + err = json.NewDecoder(w.Body).Decode(&updateDIDResponse) + assert.NoError(tt, err) + assert.Contains(tt, updateDIDResponse.DID.ID, didsdk.IONMethod) + + assert.Equal(tt, "#testService1Id", updateDIDResponse.DID.Services[0].ID) + // Check that the correct updates were performed. + assert.Len(tt, updateDIDResponse.DID.VerificationMethod, 1+len(createDIDResponse.DID.VerificationMethod)) + assert.Equal(tt, createDIDResponse.DID.VerificationMethod[0].ID, updateDIDResponse.DID.VerificationMethod[0].ID) + assert.Equal(tt, "#testPublicKeyModel1Id", updateDIDResponse.DID.VerificationMethod[1].ID) + assert.Len(tt, updateDIDResponse.DID.Authentication, 1+len(createDIDResponse.DID.Authentication)) + assert.Len(tt, updateDIDResponse.DID.AssertionMethod, 0+len(createDIDResponse.DID.AssertionMethod)) + assert.Len(tt, updateDIDResponse.DID.KeyAgreement, 1+len(createDIDResponse.DID.KeyAgreement)) + assert.Len(tt, updateDIDResponse.DID.CapabilityInvocation, 0+len(createDIDResponse.DID.CapabilityInvocation)) + assert.Len(tt, updateDIDResponse.DID.CapabilityInvocation, 0+len(createDIDResponse.DID.CapabilityInvocation)) + }) t.Run("Test Create Duplicate DID:Webs", func(tt *testing.T) { diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 00b5fc422..f3b624617 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -243,7 +243,7 @@ func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystor } // create a did service - didService, err := did.NewDIDService(serviceConfig, bolt, keyStore) + didService, err := did.NewDIDService(serviceConfig, bolt, keyStore, factory) require.NoError(t, err) require.NotEmpty(t, didService) diff --git a/pkg/service/did/ion.go b/pkg/service/did/ion.go index f817defaa..ceb83d26a 100644 --- a/pkg/service/did/ion.go +++ b/pkg/service/did/ion.go @@ -9,7 +9,6 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto/jwx" "github.com/TBD54566975/ssi-sdk/did" "github.com/TBD54566975/ssi-sdk/did/ion" - "github.com/TBD54566975/ssi-sdk/did/resolution" "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" "github.com/google/uuid" @@ -20,6 +19,7 @@ import ( "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/pkg/service/keystore" + "github.com/tbd54566975/ssi-service/pkg/storage" ) const ( @@ -27,7 +27,7 @@ const ( recoverKeySuffix string = "recover" ) -func NewIONHandler(baseURL string, s *Storage, ks *keystore.Service) (MethodHandler, error) { +func NewIONHandler(baseURL string, s *Storage, ks *keystore.Service, factory keystore.ServiceFactory, storageFactory StorageFactory) (MethodHandler, error) { if baseURL == "" { return nil, errors.New("baseURL cannot be empty") } @@ -41,14 +41,23 @@ func NewIONHandler(baseURL string, s *Storage, ks *keystore.Service) (MethodHand if err != nil { return nil, errors.Wrap(err, "creating ion resolver") } - return &ionHandler{method: did.IONMethod, resolver: r, storage: s, keyStore: ks}, nil + return &ionHandler{ + method: did.IONMethod, + resolver: r, + storage: s, + keyStore: ks, + keyStoreFactory: factory, + didStorageFactory: storageFactory, + }, nil } type ionHandler struct { - method did.Method - resolver *ion.Resolver - storage *Storage - keyStore *keystore.Service + method did.Method + resolver *ion.Resolver + storage *Storage + keyStore *keystore.Service + keyStoreFactory keystore.ServiceFactory + didStorageFactory StorageFactory } // Verify interface compliance https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance @@ -95,6 +104,250 @@ func (i ionStoredDID) IsSoftDeleted() bool { return i.SoftDeleted } +type PreAnchor struct { + UpdateOperation *ion.UpdateRequest + NextUpdatePublicJWK *jwx.PublicKeyJWK + UpdatedDID *ionStoredDID + NextUpdatePrivateJWKID string +} + +type Anchor struct { + // The result of calling anchor. + Err string +} + +type updateState struct { + ID string + Status UpdateRequestStatus + PreAnchor *PreAnchor + Anchor *Anchor +} + +func (h *ionHandler) UpdateDID(ctx context.Context, request UpdateIONDIDRequest) (*UpdateIONDIDResponse, error) { + if err := request.StateChange.IsValid(); err != nil { + return nil, errors.Wrap(err, "validating StateChange") + } + + updateStatesKey := request.DID.String() + watchKeys := []storage.WatchKey{ + { + Namespace: updateRequestStatesNamespace, + Key: updateStatesKey, + }, + } + + execResp, err := h.storage.db.Execute(ctx, h.prepareUpdate(request), watchKeys) + if err != nil { + return nil, errors.Wrapf(err, "executing transition to %s", PreAnchorStatus) + } + updateStates := execResp.([]updateState) + state := &updateStates[len(updateStates)-1] + + if state.Status == PreAnchorStatus { + state.Anchor = new(Anchor) + _, err := h.resolver.Anchor(ctx, state.PreAnchor.UpdateOperation) + if err != nil { + // Signature errors are OK, as they mean that the update operation has already been applied. It means we haven't updated our updateKey to the latest one. + state.Anchor.Err = err.Error() + if isPreviouslyAnchoredError(err) { + state.Status = AnchoredStatus + } else { + state.Status = AnchorErrorStatus + if storeErr := h.storeUpdateStates(ctx, h.storage.db, request.DID.String(), updateStates); storeErr != nil { + return nil, storeErr + } + return nil, err + } + } else { + state.Status = AnchoredStatus + } + if err := h.storeUpdateStates(ctx, h.storage.db, request.DID.String(), updateStates); err != nil { + return nil, err + } + } + + _, err = h.storage.db.Execute(ctx, h.applyUpdate(state.ID), watchKeys) + if err != nil { + return nil, errors.Wrapf(err, "executing transition to %s", DoneStatus) + } + + return &UpdateIONDIDResponse{ + DID: state.PreAnchor.UpdatedDID.DID, + }, nil +} + +func (h *ionHandler) applyUpdate(id string) func(ctx context.Context, tx storage.Tx) (any, error) { + return func(ctx context.Context, tx storage.Tx) (any, error) { + updateStates, _, err := h.readUpdateStates(ctx, id) + if err != nil { + return nil, err + } + state := &updateStates[len(updateStates)-1] + if state.Status == AnchoredStatus { + keyStore, err := h.keyStoreFactory(tx) + if err != nil { + return nil, errors.Wrap(err, "creating key store service") + } + + gotKey, err := keyStore.GetKey(ctx, keystore.GetKeyRequest{ID: state.PreAnchor.NextUpdatePrivateJWKID}) + if err != nil { + return nil, errors.Wrap(err, "getting key from keystore") + } + _, nextUpdatePrivateJWK, err := jwx.PrivateKeyToPrivateKeyJWK(gotKey.ID, gotKey.Key) + if err != nil { + return nil, errors.Wrap(err, "converting stored key to JWK") + } + + updateStoreRequest, err := keyToStoreRequest(updateKeyID(state.ID), *nextUpdatePrivateJWK, state.ID) + if err != nil { + return nil, errors.Wrap(err, "converting update private key to store request") + } + if err := keyStore.StoreKey(ctx, *updateStoreRequest); err != nil { + return nil, errors.Wrap(err, "could not store did:ion update private key") + } + + didStorage, err := h.didStorageFactory(tx) + if err != nil { + return nil, errors.Wrap(err, "creating did storage") + } + if err := didStorage.StoreDID(ctx, state.PreAnchor.UpdatedDID); err != nil { + return nil, errors.Wrap(err, "storing DID in storage") + } + + state.Status = DoneStatus + if err := h.storeUpdateStates(ctx, tx, state.ID, updateStates); err != nil { + return nil, err + } + } + return nil, nil + } +} + +func (h *ionHandler) prepareUpdate(request UpdateIONDIDRequest) func(ctx context.Context, tx storage.Tx) (any, error) { + return func(ctx context.Context, tx storage.Tx) (any, error) { + updateStates, updatePrivateKey, err := h.readUpdateStates(ctx, request.DID.String()) + if err != nil { + return nil, err + } + state := &updateStates[len(updateStates)-1] + if state.Status == DoneStatus || state.Status == AnchorErrorStatus { + updateStates = append(updateStates, updateState{ + ID: request.DID.String(), + }) + state = &updateStates[len(updateStates)-1] + } + if state.Status == "" { + + didSuffix, err := request.DID.Suffix() + if err != nil { + return nil, errors.Wrap(err, "getting did suffix") + } + + updateKey := updatePrivateKey.ToPublicKeyJWK() + // ION does not like keys that have KID nor ALG. See https://github.com/decentralized-identity/sidetree-reference-impl/blob/bf1f7aeab251083cfb5ea5d612f481cd41f0ab1b/lib/core/versions/latest/util/Jwk.ts#L35 + updateKey.ALG = "" + updateKey.KID = "" + + signer, err := ion.NewBTCSignerVerifier(*updatePrivateKey) + if err != nil { + return nil, errors.Wrap(err, "creating btc signer verifier") + } + + nextUpdateKey, nextUpdatePrivateKey, err := h.nextUpdateKey() + if err != nil { + return nil, err + } + + updateOp, err := ion.NewUpdateRequest(didSuffix, updateKey, *nextUpdateKey, *signer, request.StateChange) + if err != nil { + return nil, errors.Wrap(err, "creating update request") + } + + keyStore, err := h.keyStoreFactory(tx) + if err != nil { + return nil, errors.Wrap(err, "creating key store service") + } + storeRequestForUpdateKey, err := keyToStoreRequest("staging:"+request.DID.String(), *nextUpdatePrivateKey, request.DID.String()) + if err != nil { + return nil, errors.Wrap(err, "converting update private key to store request") + } + if err := keyStore.StoreKey(ctx, *storeRequestForUpdateKey); err != nil { + return nil, errors.Wrap(err, "could not store did:ion update private key") + } + + storedDID := new(ionStoredDID) + if err := h.storage.GetDID(ctx, request.DID.String(), storedDID); err != nil { + return nil, errors.Wrap(err, "getting ion did from storage") + } + + updatedLongForm, updatedDIDDoc, err := updateLongForm(request.DID.String(), storedDID.LongFormDID, updateOp) + if err != nil { + return nil, err + } + + updatedDID := &ionStoredDID{ + ID: storedDID.ID, + DID: *updatedDIDDoc, + SoftDeleted: storedDID.SoftDeleted, + LongFormDID: updatedLongForm, + Operations: append(storedDID.Operations, updateOp), + } + + state.PreAnchor = &PreAnchor{ + UpdateOperation: updateOp, + UpdatedDID: updatedDID, + NextUpdatePrivateJWKID: storeRequestForUpdateKey.ID, + NextUpdatePublicJWK: nextUpdateKey, + } + state.Status = PreAnchorStatus + if err := h.storeUpdateStates(ctx, tx, request.DID.String(), updateStates); err != nil { + return nil, err + } + } + + return updateStates, nil + } +} + +func updateLongForm(shortFormDID string, longFormDID string, updateOp *ion.UpdateRequest) (string, *did.Document, error) { + _, initialState, err := ion.DecodeLongFormDID(longFormDID) + if err != nil { + return "", nil, errors.Wrap(err, "invalid long form DID") + } + + delta := ion.Delta{ + Patches: append(initialState.Delta.Patches, updateOp.Delta.GetPatches()...), + UpdateCommitment: updateOp.Delta.UpdateCommitment, + } + suffixData := initialState.SuffixData + createRequest := ion.CreateRequest{ + Type: ion.Create, + SuffixData: suffixData, + Delta: delta, + } + updatedInitialState := ion.InitialState{ + Delta: createRequest.Delta, + SuffixData: createRequest.SuffixData, + } + initialStateBytesCanonical, err := ion.CanonicalizeAny(updatedInitialState) + if err != nil { + return "", nil, errors.Wrap(err, "canonicalizing long form DID suffix data") + } + encoded := ion.Encode(initialStateBytesCanonical) + newLongFormDID := shortFormDID + ":" + encoded + + didDoc, err := ion.PatchesToDIDDocument(shortFormDID, newLongFormDID, createRequest.Delta.GetPatches()) + if err != nil { + return "", nil, errors.Wrap(err, "patching the updated did") + } + return newLongFormDID, didDoc, nil +} + +func isPreviouslyAnchoredError(_ error) bool { + // TODO: figure out how to determine this error from the body of the response. + return false +} + func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) { // process options var opts CreateIONDIDOptions @@ -171,15 +424,24 @@ func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* } // submit the create operation to the ION service - var resolutionResult *resolution.Result - if resolutionResult, err = h.resolver.Anchor(ctx, createOp); err != nil { + if _, err = h.resolver.Anchor(ctx, createOp); err != nil { return nil, errors.Wrap(err, "anchoring create operation") } + _, initialState, err := ion.DecodeLongFormDID(ionDID.LongForm()) + if err != nil { + return nil, errors.Wrap(err, "invalid long form DID") + } + // TODO: remove the first parameter once it is removed in the SDK (https://github.com/TBD54566975/ssi-sdk/issues/438) + didDoc, err := ion.PatchesToDIDDocument("unused", ionDID.ID(), initialState.Delta.Patches) + if err != nil { + return nil, errors.Wrap(err, "patching the did document locally") + } + // store the did document storedDID := ionStoredDID{ - ID: resolutionResult.Document.ID, - DID: resolutionResult.Document, + ID: ionDID.ID(), + DID: *didDoc, SoftDeleted: false, LongFormDID: ionDID.LongForm(), Operations: ionDID.Operations(), @@ -188,36 +450,43 @@ func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* return nil, errors.Wrap(err, "storing ion did document") } - // store associated keys - // 1. update key - // 2. recovery key + if err := h.storeKeys(ctx, ionDID); err != nil { + return nil, err + } + // 3. key(s) in the did docs - updateStoreRequest, err := keyToStoreRequest(resolutionResult.Document.ID+"#"+updateKeySuffix, ionDID.GetUpdatePrivateKey(), resolutionResult.Document.ID) + keyStoreRequest, err := keyToStoreRequest(did.FullyQualifiedVerificationMethodID(ionDID.ID(), didDoc.VerificationMethod[0].ID), *privKeyJWK, ionDID.ID()) if err != nil { - return nil, errors.Wrap(err, "converting update private key to store request") + return nil, errors.Wrap(err, "converting private key to store request") } - if err = h.keyStore.StoreKey(ctx, *updateStoreRequest); err != nil { - return nil, errors.Wrap(err, "could not store did:ion update private key") + if err = h.keyStore.StoreKey(ctx, *keyStoreRequest); err != nil { + return nil, errors.Wrap(err, "could not store did:ion private key") } - recoveryStoreRequest, err := keyToStoreRequest(resolutionResult.Document.ID+"#"+recoverKeySuffix, ionDID.GetRecoveryPrivateKey(), resolutionResult.Document.ID) + return &CreateDIDResponse{DID: *didDoc}, nil +} + +func (h *ionHandler) storeKeys(ctx context.Context, ionDID *ion.DID) error { + // store associated keys + // 1. update key + // 2. recovery key + updateStoreRequest, err := keyToStoreRequest(updateKeyID(ionDID.ID()), ionDID.GetUpdatePrivateKey(), ionDID.ID()) if err != nil { - return nil, errors.Wrap(err, "converting recovery private key to store request") + return errors.Wrap(err, "converting update private key to store request") } - if err = h.keyStore.StoreKey(ctx, *recoveryStoreRequest); err != nil { - return nil, errors.Wrap(err, "could not store did:ion recovery private key") + if err = h.keyStore.StoreKey(ctx, *updateStoreRequest); err != nil { + return errors.Wrap(err, "could not store did:ion update private key") } - keyStoreID := did.FullyQualifiedVerificationMethodID(resolutionResult.Document.ID, resolutionResult.Document.VerificationMethod[0].ID) - keyStoreRequest, err := keyToStoreRequest(keyStoreID, *privKeyJWK, resolutionResult.Document.ID) + recoveryStoreRequest, err := keyToStoreRequest(recoveryKeyID(ionDID.ID()), ionDID.GetRecoveryPrivateKey(), ionDID.ID()) if err != nil { - return nil, errors.Wrap(err, "converting private key to store request") + return errors.Wrap(err, "converting recovery private key to store request") } - if err = h.keyStore.StoreKey(ctx, *keyStoreRequest); err != nil { - return nil, errors.Wrap(err, "could not store did:ion private key") + if err = h.keyStore.StoreKey(ctx, *recoveryStoreRequest); err != nil { + return errors.Wrap(err, "could not store did:ion recovery private key") } - return &CreateDIDResponse{DID: storedDID.DID}, nil + return nil } func keyToStoreRequest(kid string, privateKeyJWK jwx.PrivateKeyJWK, controller string) (*keystore.StoreKeyRequest, error) { @@ -317,3 +586,75 @@ func (h *ionHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest return h.storage.StoreDID(ctx, *gotDID) } + +func (h *ionHandler) readUpdatePrivateKey(ctx context.Context, did string) (*jwx.PrivateKeyJWK, error) { + keyID := updateKeyID(did) + getKeyRequest := keystore.GetKeyRequest{ID: keyID} + key, err := h.keyStore.GetKey(ctx, getKeyRequest) + if err != nil { + return nil, errors.Wrap(err, "fetching update private key") + } + _, privateJWK, err := jwx.PrivateKeyToPrivateKeyJWK(keyID, key.Key) + if err != nil { + return nil, errors.Wrap(err, "getting update private key") + } + return privateJWK, err +} + +func updateKeyID(did string) string { + return did + "#" + updateKeySuffix +} + +func recoveryKeyID(did string) string { + return did + "#" + recoverKeySuffix +} + +func (h *ionHandler) nextUpdateKey() (*jwx.PublicKeyJWK, *jwx.PrivateKeyJWK, error) { + _, nextUpdatePrivateKey, err := crypto.GenerateSECP256k1Key() + if err != nil { + return nil, nil, errors.Wrap(err, "generating next update keypair") + } + nextUpdatePubKeyJWK, nextUpdatePrivateKeyJWK, err := jwx.PrivateKeyToPrivateKeyJWK(uuid.NewString(), nextUpdatePrivateKey) + if err != nil { + return nil, nil, errors.Wrap(err, "converting next update key pair to JWK") + } + return nextUpdatePubKeyJWK, nextUpdatePrivateKeyJWK, nil +} + +const updateRequestStatesNamespace = "update-request-states" + +func (h *ionHandler) readUpdateStates(ctx context.Context, id string) ([]updateState, *jwx.PrivateKeyJWK, error) { + privateUpdateJWK, err := h.readUpdatePrivateKey(ctx, id) + if err != nil { + return nil, nil, err + } + + readData, err := h.storage.db.Read(ctx, updateRequestStatesNamespace, id) + if err != nil { + return nil, nil, errors.Wrap(err, "reading update status") + } + if readData == nil { + return []updateState{{ + ID: id, + }}, privateUpdateJWK, nil + } + var statuses []updateState + if err := json.Unmarshal(readData, &statuses); err != nil { + return nil, nil, errors.Wrap(err, "unmarhsalling status array") + } + + return statuses, privateUpdateJWK, nil + +} + +func (h *ionHandler) storeUpdateStates(ctx context.Context, tx storage.Tx, id string, states []updateState) error { + bytes, err := json.Marshal(states) + if err != nil { + return errors.Wrap(err, "marshalling json") + } + + if err := tx.Write(ctx, updateRequestStatesNamespace, id, bytes); err != nil { + return errors.Wrap(err, "writing update states") + } + return nil +} diff --git a/pkg/service/did/ion_test.go b/pkg/service/did/ion_test.go index 1a7323112..f9385748e 100644 --- a/pkg/service/did/ion_test.go +++ b/pkg/service/did/ion_test.go @@ -28,7 +28,7 @@ func TestIONHandler(t *testing.T) { for _, test := range testutil.TestDatabases { t.Run(test.Name, func(t *testing.T) { t.Run("Create ION Handler", func(tt *testing.T) { - handler, err := NewIONHandler("", nil, nil) + handler, err := NewIONHandler("", nil, nil, nil, nil) assert.Error(tt, err) assert.Empty(tt, handler) assert.Contains(tt, err.Error(), "baseURL cannot be empty") @@ -37,22 +37,22 @@ func TestIONHandler(t *testing.T) { keystoreService := testKeyStoreService(tt, s) didStorage, err := NewDIDStorage(s) assert.NoError(tt, err) - handler, err = NewIONHandler("bad", nil, keystoreService) + handler, err = NewIONHandler("bad", nil, keystoreService, nil, nil) assert.Error(tt, err) assert.Empty(tt, handler) assert.Contains(tt, err.Error(), "storage cannot be empty") - handler, err = NewIONHandler("bad", didStorage, nil) + handler, err = NewIONHandler("bad", didStorage, nil, nil, nil) assert.Error(tt, err) assert.Empty(tt, handler) assert.Contains(tt, err.Error(), "keystore cannot be empty") - handler, err = NewIONHandler("bad", didStorage, keystoreService) + handler, err = NewIONHandler("bad", didStorage, keystoreService, nil, nil) assert.Error(tt, err) assert.Empty(tt, handler) assert.Contains(tt, err.Error(), "invalid resolution URL") - handler, err = NewIONHandler("https://example.com", didStorage, keystoreService) + handler, err = NewIONHandler("https://example.com", didStorage, keystoreService, nil, nil) assert.NoError(tt, err) assert.NotEmpty(tt, handler) @@ -65,7 +65,7 @@ func TestIONHandler(t *testing.T) { keystoreService := testKeyStoreService(tt, s) didStorage, err := NewDIDStorage(s) assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil) assert.NoError(tt, err) assert.NotEmpty(tt, handler) @@ -137,7 +137,7 @@ func TestIONHandler(t *testing.T) { keystoreService := testKeyStoreService(tt, s) didStorage, err := NewDIDStorage(s) assert.NoError(tt, err) - handler, err := NewIONHandler("https://ion.tbddev.org", didStorage, keystoreService) + handler, err := NewIONHandler("https://ion.tbddev.org", didStorage, keystoreService, nil, nil) assert.NoError(tt, err) assert.NotEmpty(tt, handler) @@ -164,7 +164,7 @@ func TestIONHandler(t *testing.T) { keystoreService := testKeyStoreService(tt, s) didStorage, err := NewDIDStorage(s) assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil) assert.NoError(tt, err) assert.NotEmpty(tt, handler) @@ -203,7 +203,7 @@ func TestIONHandler(t *testing.T) { keystoreService := testKeyStoreService(tt, s) didStorage, err := NewDIDStorage(s) assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil) assert.NoError(tt, err) assert.NotEmpty(tt, handler) @@ -261,7 +261,7 @@ func TestIONHandler(t *testing.T) { keystoreService := testKeyStoreService(tt, s) didStorage, err := NewDIDStorage(s) assert.NoError(tt, err) - handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService) + handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil) assert.NoError(tt, err) assert.NotEmpty(tt, handler) diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go index 2873ba740..d3c4766f6 100644 --- a/pkg/service/did/model.go +++ b/pkg/service/did/model.go @@ -5,6 +5,7 @@ import ( "github.com/TBD54566975/ssi-sdk/crypto" didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/did/ion" "github.com/TBD54566975/ssi-sdk/did/resolution" "github.com/tbd54566975/ssi-service/pkg/service/common" ) @@ -84,3 +85,26 @@ type DeleteDIDRequest struct { Method didsdk.Method `json:"method" validate:"required"` ID string `json:"id" validate:"required"` } + +type UpdateIONDIDRequest struct { + DID ion.ION `json:"did"` + + StateChange ion.StateChange `json:"stateChange"` +} + +type UpdateIONDIDResponse struct { + DID didsdk.Document `json:"did"` +} + +type UpdateRequestStatus string + +func (s UpdateRequestStatus) Bytes() []byte { + return []byte(s) +} + +const ( + PreAnchorStatus UpdateRequestStatus = "pre-anchor" + AnchorErrorStatus UpdateRequestStatus = "anchor-error" + AnchoredStatus UpdateRequestStatus = "anchored" + DoneStatus UpdateRequestStatus = "done" +) diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go index 4108105a2..d3608bcc7 100644 --- a/pkg/service/did/service.go +++ b/pkg/service/did/service.go @@ -27,7 +27,9 @@ type Service struct { resolver *resolution.ServiceResolver // external dependencies - keyStore *keystore.Service + keyStore *keystore.Service + keyStoreFactory keystore.ServiceFactory + didStorageFactory StorageFactory } func (s *Service) Type() framework.Type { @@ -66,17 +68,19 @@ func (s *Service) GetResolver() didresolution.Resolver { return s.resolver } -func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) { +func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, factory keystore.ServiceFactory) (*Service, error) { didStorage, err := NewDIDStorage(s) if err != nil { return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service") } service := Service{ - config: config, - storage: didStorage, - handlers: make(map[didsdk.Method]MethodHandler), - keyStore: keyStore, + config: config, + storage: didStorage, + didStorageFactory: NewDIDStorageFactory(s), + handlers: make(map[didsdk.Method]MethodHandler), + keyStore: keyStore, + keyStoreFactory: factory, } // instantiate all handlers for DID methods @@ -122,7 +126,7 @@ func (s *Service) instantiateHandlerForMethod(method didsdk.Method) error { } s.handlers[method] = wh case didsdk.IONMethod: - ih, err := NewIONHandler(s.Config().IONResolverURL, s.storage, s.keyStore) + ih, err := NewIONHandler(s.Config().IONResolverURL, s.storage, s.keyStore, s.keyStoreFactory, s.didStorageFactory) if err != nil { return errors.Wrap(err, "instantiating ion handler") } @@ -168,6 +172,18 @@ func (s *Service) CreateDIDByMethod(ctx context.Context, request CreateDIDReques return handler.CreateDID(ctx, request) } +func (s *Service) UpdateIONDID(ctx context.Context, request UpdateIONDIDRequest) (*UpdateIONDIDResponse, error) { + handler, err := s.getHandler(didsdk.IONMethod) + if err != nil { + return nil, sdkutil.LoggingErrorMsgf(err, "could not get handler for method<%s>", didsdk.IONMethod) + } + ionHandlerImpl, ok := handler.(*ionHandler) + if !ok { + return nil, errors.New("cannot assert that handler is an ionHandler") + } + return ionHandlerImpl.UpdateDID(ctx, request) +} + func (s *Service) GetDIDByMethod(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) { handler, err := s.getHandler(request.Method) if err != nil { diff --git a/pkg/service/service.go b/pkg/service/service.go index 9183c393e..ffabf3579 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -106,7 +106,7 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate batch DID service") } - didService, err := did.NewDIDService(config.DIDConfig, storageProvider, keyStoreService) + didService, err := did.NewDIDService(config.DIDConfig, storageProvider, keyStoreService, keyStoreServiceFactory) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the DID service") } From fa6477d7d58a4c22b3aa6047db9d54758521b89e Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Fri, 4 Aug 2023 15:07:20 -0400 Subject: [PATCH 19/20] Add pagination to schemas (#638) * Added pagination to schemas. * Added pagination integration test for schemas. * tags --- doc/swagger.yaml | 15 +++++++ integration/pagination_integration_test.go | 49 ++++++++++++++++++++++ pkg/server/router/schema.go | 24 +++++++++-- pkg/server/router/schema_test.go | 8 ++-- pkg/service/schema/model.go | 8 +++- pkg/service/schema/service.go | 10 ++--- pkg/service/schema/storage.go | 26 +++++++----- 7 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 integration/pagination_integration_test.go diff --git a/doc/swagger.yaml b/doc/swagger.yaml index 93b92bb0c..5bda830f8 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -1824,6 +1824,10 @@ definitions: type: object pkg_server_router.ListSchemasResponse: properties: + nextPageToken: + description: Pagination token to retrieve the next page of results. If the + value is "", it means no further results for the request. + type: string schemas: description: Schemas is the list of all schemas the service holds items: @@ -4049,6 +4053,17 @@ paths: consumes: - application/json description: List Credential Schemas stored by the service + parameters: + - description: Hint to the server of the maximum elements to return. More may + be returned. When not set, the server will return all elements. + in: query + name: pageSize + type: number + - description: Used to indicate to the server to return a specific page of the + list results. Must match a previous requests' `nextPageToken`. + in: query + name: pageToken + type: string produces: - application/json responses: diff --git a/integration/pagination_integration_test.go b/integration/pagination_integration_test.go new file mode 100644 index 000000000..91f08a6b1 --- /dev/null +++ b/integration/pagination_integration_test.go @@ -0,0 +1,49 @@ +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestListSchemaIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + // In this test we create a couple of schemas, fetch them all using pagination, and validate that the created ones + // were returned in any of the results. + schema1, err := CreateKYCSchema() + assert.NoError(t, err) + + schema1ID, err := getJSONElement(schema1, "$.id") + assert.NoError(t, err) + + schema2, err := CreateKYCSchema() + assert.NoError(t, err) + + schema2ID, err := getJSONElement(schema2, "$.id") + assert.NoError(t, err) + + schemasPage, err := get(endpoint + version + "schemas?pageSize=1") + assert.NoError(t, err) + + var allSchemaIDs string + allSchemaIDs = schemasPage + + nextPageToken, err := getJSONElement(schemasPage, "$.nextPageToken") + assert.NoError(t, err) + + for nextPageToken != "" { + schemasPage, err := get(endpoint + version + "schemas?pageSize=1&pageToken=" + nextPageToken) + assert.NoError(t, err) + + allSchemaIDs += schemasPage + + nextPageToken, err = getJSONElement(schemasPage, "$.nextPageToken") + assert.NoError(t, err) + } + + assert.Contains(t, allSchemaIDs, schema1ID) + assert.Contains(t, allSchemaIDs, schema2ID) +} diff --git a/pkg/server/router/schema.go b/pkg/server/router/schema.go index 6fafac22e..13a91ec40 100644 --- a/pkg/server/router/schema.go +++ b/pkg/server/router/schema.go @@ -8,9 +8,9 @@ import ( "github.com/TBD54566975/ssi-sdk/did" "github.com/gin-gonic/gin" "github.com/pkg/errors" - "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/pkg/server/framework" + "github.com/tbd54566975/ssi-service/pkg/server/pagination" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/schema" ) @@ -185,6 +185,9 @@ func (sr SchemaRouter) GetSchema(c *gin.Context) { type ListSchemasResponse struct { // Schemas is the list of all schemas the service holds Schemas []GetSchemaResponse `json:"schemas,omitempty"` + + // Pagination token to retrieve the next page of results. If the value is "", it means no further results for the request. + NextPageToken string `json:"nextPageToken"` } // ListSchemas godoc @@ -194,11 +197,20 @@ type ListSchemasResponse struct { // @Tags Schemas // @Accept json // @Produce json -// @Success 200 {object} ListSchemasResponse -// @Failure 500 {string} string "Internal server error" +// @Param pageSize query number false "Hint to the server of the maximum elements to return. More may be returned. When not set, the server will return all elements." +// @Param pageToken query string false "Used to indicate to the server to return a specific page of the list results. Must match a previous requests' `nextPageToken`." +// @Success 200 {object} ListSchemasResponse +// @Failure 500 {string} string "Internal server error" // @Router /v1/schemas [get] func (sr SchemaRouter) ListSchemas(c *gin.Context) { - gotSchemas, err := sr.service.ListSchemas(c) + var pageRequest pagination.PageRequest + if pagination.ParsePaginationQueryValues(c, &pageRequest) { + return + } + + gotSchemas, err := sr.service.ListSchemas(c, schema.ListSchemasRequest{ + PageRequest: &pageRequest, + }) if err != nil { errMsg := "could not list schemas" framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError) @@ -218,6 +230,10 @@ func (sr SchemaRouter) ListSchemas(c *gin.Context) { } resp := ListSchemasResponse{Schemas: schemas} + + if pagination.MaybeSetNextPageToken(c, gotSchemas.NextPageToken, &resp.NextPageToken) { + return + } framework.Respond(c, resp, http.StatusOK) } diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go index 9ec338463..cc3cc8c6c 100644 --- a/pkg/server/router/schema_test.go +++ b/pkg/server/router/schema_test.go @@ -50,7 +50,7 @@ func TestSchemaRouter(t *testing.T) { assert.Equal(tt, framework.StatusReady, schemaService.Status().Status) // get all schemas (none) - gotSchemas, err := schemaService.ListSchemas(context.Background()) + gotSchemas, err := schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{}) assert.NoError(tt, err) assert.Empty(tt, gotSchemas.Schemas) @@ -74,7 +74,7 @@ func TestSchemaRouter(t *testing.T) { assert.EqualValues(tt, createdSchema.Schema, gotSchema.Schema) // get all schemas, expect one - gotSchemas, err = schemaService.ListSchemas(context.Background()) + gotSchemas, err = schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{}) assert.NoError(tt, err) assert.NotEmpty(tt, gotSchemas.Schemas) assert.Len(tt, gotSchemas.Schemas, 1) @@ -88,7 +88,7 @@ func TestSchemaRouter(t *testing.T) { assert.Equal(tt, credschema.JSONSchema2023Type, createdSchema.Type) // get all schemas, expect two - gotSchemas, err = schemaService.ListSchemas(context.Background()) + gotSchemas, err = schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{}) assert.NoError(tt, err) assert.NotEmpty(tt, gotSchemas.Schemas) assert.Len(tt, gotSchemas.Schemas, 2) @@ -101,7 +101,7 @@ func TestSchemaRouter(t *testing.T) { assert.NoError(tt, err) // get all schemas, expect one - gotSchemas, err = schemaService.ListSchemas(context.Background()) + gotSchemas, err = schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{}) assert.NoError(tt, err) assert.NotEmpty(tt, gotSchemas.Schemas) assert.Len(tt, gotSchemas.Schemas, 1) diff --git a/pkg/service/schema/model.go b/pkg/service/schema/model.go index 0a18eac1b..ab78fd50b 100644 --- a/pkg/service/schema/model.go +++ b/pkg/service/schema/model.go @@ -3,6 +3,7 @@ package schema import ( "github.com/TBD54566975/ssi-sdk/credential/schema" "github.com/TBD54566975/ssi-sdk/util" + "github.com/tbd54566975/ssi-service/pkg/server/pagination" "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/internal/keyaccess" @@ -40,8 +41,13 @@ type CreateSchemaResponse struct { CredentialSchema *keyaccess.JWT `json:"credentialSchema,omitempty"` } +type ListSchemasRequest struct { + PageRequest *pagination.PageRequest +} + type ListSchemasResponse struct { - Schemas []GetSchemaResponse `json:"schemas,omitempty"` + Schemas []GetSchemaResponse `json:"schemas,omitempty"` + NextPageToken string `json:"nextPageToken,omitempty"` } type GetSchemaRequest struct { diff --git a/pkg/service/schema/service.go b/pkg/service/schema/service.go index 77c1a8cb2..7927d9023 100644 --- a/pkg/service/schema/service.go +++ b/pkg/service/schema/service.go @@ -195,15 +195,15 @@ func (s Service) signCredentialSchema(ctx context.Context, cred credential.Verif return credToken, nil } -func (s Service) ListSchemas(ctx context.Context) (*ListSchemasResponse, error) { +func (s Service) ListSchemas(ctx context.Context, request ListSchemasRequest) (*ListSchemasResponse, error) { logrus.Debug("listing all schemas") - storedSchemas, err := s.storage.ListSchemas(ctx) + storedSchemas, err := s.storage.ListSchemas(ctx, *request.PageRequest.ToServicePage()) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "error getting schemas") } - schemas := make([]GetSchemaResponse, 0, len(storedSchemas)) - for _, stored := range storedSchemas { + schemas := make([]GetSchemaResponse, 0, len(storedSchemas.Schemas)) + for _, stored := range storedSchemas.Schemas { schemas = append(schemas, GetSchemaResponse{ ID: stored.ID, Type: stored.Type, @@ -212,7 +212,7 @@ func (s Service) ListSchemas(ctx context.Context) (*ListSchemasResponse, error) }) } - return &ListSchemasResponse{Schemas: schemas}, nil + return &ListSchemasResponse{Schemas: schemas, NextPageToken: storedSchemas.NextPageToken}, nil } func (s Service) GetSchema(ctx context.Context, request GetSchemaRequest) (*GetSchemaResponse, error) { diff --git a/pkg/service/schema/storage.go b/pkg/service/schema/storage.go index 5c6215727..07a86f76b 100644 --- a/pkg/service/schema/storage.go +++ b/pkg/service/schema/storage.go @@ -3,12 +3,12 @@ package schema import ( "context" + "github.com/TBD54566975/ssi-sdk/credential/schema" "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/sirupsen/logrus" - - "github.com/TBD54566975/ssi-sdk/credential/schema" + "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/pkg/storage" @@ -18,6 +18,11 @@ const ( namespace = "schema" ) +type StoredSchemas struct { + Schemas []StoredSchema + NextPageToken string +} + type StoredSchema struct { ID string `json:"id"` Type schema.VCJSONSchemaType `json:"type"` @@ -64,15 +69,13 @@ func (s *Storage) GetSchema(ctx context.Context, id string) (*StoredSchema, erro } // ListSchemas attempts to get all stored schemas. It will return those it can even if it has trouble with some. -func (s *Storage) ListSchemas(ctx context.Context) ([]StoredSchema, error) { - gotSchemas, err := s.db.ReadAll(ctx, namespace) +func (s *Storage) ListSchemas(ctx context.Context, page common.Page) (*StoredSchemas, error) { + token, size := page.ToStorageArgs() + gotSchemas, nextPageToken, err := s.db.ReadPage(ctx, namespace, token, size) if err != nil { - return nil, util.LoggingErrorMsg(err, "could not list schemas") - } - if len(gotSchemas) == 0 { - logrus.Info("no schemas to list") - return nil, nil + return nil, errors.Wrap(err, "reading page of schemas") } + stored := make([]StoredSchema, 0, len(gotSchemas)) for _, schemaBytes := range gotSchemas { var nextSchema StoredSchema @@ -82,7 +85,10 @@ func (s *Storage) ListSchemas(ctx context.Context) ([]StoredSchema, error) { } stored = append(stored, nextSchema) } - return stored, nil + return &StoredSchemas{ + Schemas: stored, + NextPageToken: nextPageToken, + }, nil } func (s *Storage) DeleteSchema(ctx context.Context, id string) error { From 55f9827e551d18cca4f4190889d72892127a4598 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Fri, 4 Aug 2023 15:28:29 -0400 Subject: [PATCH 20/20] Added pagination for operations (#639) * Pagination for operations * Added integration test. * Skip the test when not in integration. * feedback --- doc/swagger.yaml | 14 +++++++ .../presentation_exchange_integration_test.go | 21 ++++++++++ pkg/server/router/operation.go | 40 +++++++++++++------ pkg/service/operation/model.go | 9 +++-- pkg/service/operation/service.go | 9 +++-- pkg/service/operation/storage.go | 12 ++++-- pkg/service/operation/storage/storage.go | 5 +++ 7 files changed, 88 insertions(+), 22 deletions(-) diff --git a/doc/swagger.yaml b/doc/swagger.yaml index 5bda830f8..731848a48 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -1802,6 +1802,10 @@ definitions: type: object pkg_server_router.ListOperationsResponse: properties: + nextPageToken: + description: Pagination token to retrieve the next page of results. If the + value is "", it means no further results for the request. + type: string operations: items: $ref: '#/definitions/pkg_server_router.Operation' @@ -3593,6 +3597,16 @@ paths: in: query name: filter type: string + - description: Hint to the server of the maximum elements to return. More may + be returned. When not set, the server will return all elements. + in: query + name: pageSize + type: number + - description: Used to indicate to the server to return a specific page of the + list results. Must match a previous requests' `nextPageToken`. + in: query + name: pageToken + type: string produces: - application/json responses: diff --git a/integration/presentation_exchange_integration_test.go b/integration/presentation_exchange_integration_test.go index 53b8cdcbb..548bc2d73 100644 --- a/integration/presentation_exchange_integration_test.go +++ b/integration/presentation_exchange_integration_test.go @@ -268,3 +268,24 @@ func TestSubmissionFlowExternalCredential(t *testing.T) { s, _ := getJSONElement(reviewOutput, "$") assert.Equal(t, s, opResponse) } + +func TestListOperationsWithPagination(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + // This test simply ensures that we can retrieve all operations from a parent using pagination. + schemasPage, err := get(endpoint + version + "operations?parent=presentations/submissions&pageSize=1") + assert.NoError(t, err) + + nextPageToken, err := getJSONElement(schemasPage, "$.nextPageToken") + assert.NoError(t, err) + + for nextPageToken != "" { + schemasPage, err := get(endpoint + version + "operations?parent=presentations/submissions&pageSize=1&pageToken=" + nextPageToken) + assert.NoError(t, err) + + nextPageToken, err = getJSONElement(schemasPage, "$.nextPageToken") + assert.NoError(t, err) + } +} diff --git a/pkg/server/router/operation.go b/pkg/server/router/operation.go index 86e392e9d..b99bf6789 100644 --- a/pkg/server/router/operation.go +++ b/pkg/server/router/operation.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/pkg/server/pagination" "go.einride.tech/aip/filtering" "github.com/tbd54566975/ssi-service/pkg/server/framework" @@ -104,7 +105,7 @@ const ( const FilterCharacterLimit = 1024 -func (r listOperationsRequest) toServiceRequest() (operation.ListOperationsRequest, error) { +func (r listOperationsRequest) toServiceRequest(pageRequest pagination.PageRequest) (operation.ListOperationsRequest, error) { var opReq operation.ListOperationsRequest opReq.Parent = r.Parent @@ -130,11 +131,15 @@ func (r listOperationsRequest) toServiceRequest() (operation.ListOperationsReque return opReq, errors.Wrap(err, "parsing filter") } opReq.Filter = filter + opReq.PageRequest = &pageRequest return opReq, nil } type ListOperationsResponse struct { Operations []Operation `json:"operations"` + + // Pagination token to retrieve the next page of results. If the value is "", it means no further results for the request. + NextPageToken string `json:"nextPageToken"` } // ListOperations godoc @@ -144,15 +149,17 @@ type ListOperationsResponse struct { // @Tags Operations // @Accept json // @Produce json -// @Param parent query string false "The name of the parent's resource. For example: `?parent=/presentation/submissions`" -// @Param filter query string false "A standard filter expression conforming to https://google.aip.dev/160. For example: `?filter=done="true"`" -// @Success 200 {object} ListOperationsResponse "OK" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" +// @Param parent query string false "The name of the parent's resource. For example: `?parent=/presentation/submissions`" +// @Param filter query string false "A standard filter expression conforming to https://google.aip.dev/160. For example: `?filter=done="true"`" +// @Param pageSize query number false "Hint to the server of the maximum elements to return. More may be returned. When not set, the server will return all elements." +// @Param pageToken query string false "Used to indicate to the server to return a specific page of the list results. Must match a previous requests' `nextPageToken`." +// @Success 200 {object} ListOperationsResponse "OK" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" // @Router /v1/operations [get] func (o OperationRouter) ListOperations(c *gin.Context) { - parentParam := framework.GetParam(c, ParentParam) - filterParam := framework.GetParam(c, FilterParam) + parentParam := framework.GetQueryValue(c, ParentParam) + filterParam := framework.GetQueryValue(c, FilterParam) var request listOperationsRequest if parentParam != nil { unescaped, err := url.QueryUnescape(*parentParam) @@ -179,23 +186,30 @@ func (o OperationRouter) ListOperations(c *gin.Context) { return } - req, err := request.toServiceRequest() + var pageRequest pagination.PageRequest + if pagination.ParsePaginationQueryValues(c, &pageRequest) { + return + } + + req, err := request.toServiceRequest(pageRequest) if err != nil { framework.LoggingRespondErrWithMsg(c, err, invalidGetOperationsErr, http.StatusBadRequest) return } - - ops, err := o.service.ListOperations(c, req) + listOpsResp, err := o.service.ListOperations(c, req) if err != nil { errMsg := "getting operations from service" framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError) return } - resp := ListOperationsResponse{Operations: make([]Operation, 0, len(ops.Operations))} - for _, op := range ops.Operations { + resp := ListOperationsResponse{Operations: make([]Operation, 0, len(listOpsResp.Operations))} + for _, op := range listOpsResp.Operations { resp.Operations = append(resp.Operations, routerModel(op)) } + if pagination.MaybeSetNextPageToken(c, listOpsResp.NextPageToken, &resp.NextPageToken) { + return + } framework.Respond(c, resp, http.StatusOK) } diff --git a/pkg/service/operation/model.go b/pkg/service/operation/model.go index cce521d4e..fb4214139 100644 --- a/pkg/service/operation/model.go +++ b/pkg/service/operation/model.go @@ -2,6 +2,7 @@ package operation import ( "github.com/TBD54566975/ssi-sdk/util" + "github.com/tbd54566975/ssi-service/pkg/server/pagination" "go.einride.tech/aip/filtering" ) @@ -17,8 +18,9 @@ type Operation struct { } type ListOperationsRequest struct { - Parent string `validate:"required"` - Filter filtering.Filter + Parent string `validate:"required"` + Filter filtering.Filter + PageRequest *pagination.PageRequest } func (r ListOperationsRequest) Validate() error { @@ -26,7 +28,8 @@ func (r ListOperationsRequest) Validate() error { } type ListOperationsResponse struct { - Operations []Operation + Operations []Operation + NextPageToken string } type GetOperationRequest struct { diff --git a/pkg/service/operation/service.go b/pkg/service/operation/service.go index 656f273fb..69a48ad46 100644 --- a/pkg/service/operation/service.go +++ b/pkg/service/operation/service.go @@ -48,13 +48,16 @@ func (s Service) ListOperations(ctx context.Context, request ListOperationsReque return nil, errors.Wrap(err, "invalid request") } - ops, err := s.storage.ListOperations(ctx, request.Parent, request.Filter) + ops, err := s.storage.ListOperations(ctx, request.Parent, request.Filter, request.PageRequest.ToServicePage()) if err != nil { return nil, errors.Wrap(err, "fetching ops from storage") } - resp := ListOperationsResponse{Operations: make([]Operation, len(ops))} - for i, op := range ops { + resp := ListOperationsResponse{ + Operations: make([]Operation, len(ops.StoredOperations)), + NextPageToken: ops.NextPageToken, + } + for i, op := range ops.StoredOperations { op := op newOp, err := ServiceModel(op) if err != nil { diff --git a/pkg/service/operation/storage.go b/pkg/service/operation/storage.go index c9319eaa7..2c939bc6c 100644 --- a/pkg/service/operation/storage.go +++ b/pkg/service/operation/storage.go @@ -8,6 +8,7 @@ import ( "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/pkg/service/common" "go.einride.tech/aip/filtering" "github.com/tbd54566975/ssi-service/pkg/service/operation/credential" @@ -101,8 +102,10 @@ func (s Storage) GetOperation(ctx context.Context, id string) (opstorage.StoredO return stored, nil } -func (s Storage) ListOperations(ctx context.Context, parent string, filter filtering.Filter) ([]opstorage.StoredOperation, error) { - operations, err := s.db.ReadAll(ctx, namespace.FromParent(parent)) +func (s Storage) ListOperations(ctx context.Context, parent string, filter filtering.Filter, page *common.Page) (*opstorage.StoredOperations, error) { + token, size := page.ToStorageArgs() + + operations, nextPageToken, err := s.db.ReadPage(ctx, namespace.FromParent(parent), token, size) if err != nil { return nil, sdkutil.LoggingErrorMsgf(err, "could not get all operations") } @@ -123,7 +126,10 @@ func (s Storage) ListOperations(ctx context.Context, parent string, filter filte stored = append(stored, nextOp) } } - return stored, nil + return &opstorage.StoredOperations{ + StoredOperations: stored, + NextPageToken: nextPageToken, + }, nil } func (s Storage) DeleteOperation(ctx context.Context, id string) error { diff --git a/pkg/service/operation/storage/storage.go b/pkg/service/operation/storage/storage.go index 1ed006e49..1e2f92692 100644 --- a/pkg/service/operation/storage/storage.go +++ b/pkg/service/operation/storage/storage.go @@ -6,6 +6,11 @@ import ( "go.einride.tech/aip/filtering" ) +type StoredOperations struct { + StoredOperations []StoredOperation + NextPageToken string +} + type StoredOperation struct { ID string `json:"id"`