From 556edf79829ba734622abd4ba398898151695d54 Mon Sep 17 00:00:00 2001 From: Gabe Date: Mon, 10 Oct 2022 12:38:10 -0700 Subject: [PATCH] Credential Verification & DID Resolution (#121) * init * temp * update sdk * add did resolver * cred verification * working but untested * did resolution api and tests * bug * update ci * verification debugging * working * pr comments --- .github/workflows/ci.yml | 4 +- config/compose.toml | 1 + config/config.go | 4 +- config/config.toml | 1 + go.mod | 6 +- go.sum | 10 ++ internal/credential/verification.go | 188 +++++++++++++++++++++++++++ internal/did/resolver.go | 42 ++++++ internal/did/resolver_test.go | 32 +++++ internal/keyaccess/jwt.go | 48 +++++-- internal/keyaccess/jwt_test.go | 13 ++ pkg/server/router/credential.go | 65 ++++++++- pkg/server/router/credential_test.go | 5 +- pkg/server/router/did.go | 44 ++++++- pkg/server/router/did_test.go | 16 ++- pkg/server/router/dwn_test.go | 3 +- pkg/server/router/manifest_test.go | 7 +- pkg/server/router/router_test.go | 5 +- pkg/server/router/testutils_test.go | 12 +- pkg/server/server_credential_test.go | 110 +++++++++++++--- pkg/server/server_did_test.go | 63 ++++++++- pkg/server/server_dwn_test.go | 7 +- pkg/server/server_keystore_test.go | 3 + pkg/server/server_manifest_test.go | 37 ++++-- pkg/server/server_schema_test.go | 3 + pkg/server/server_test.go | 9 +- pkg/service/credential/credential.go | 80 ++++++++++-- pkg/service/did/did.go | 60 ++++++--- pkg/service/did/key.go | 2 +- pkg/service/did/model.go | 20 ++- pkg/service/service.go | 2 +- 31 files changed, 784 insertions(+), 118 deletions(-) create mode 100644 internal/credential/verification.go create mode 100644 internal/did/resolver.go create mode 100644 internal/did/resolver_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbdd5b2d4..6cd5ee507 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.19 + go-version: 1.19.2 - name: Install Mage run: go install github.com/magefile/mage @@ -35,7 +35,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: 1.19.2 - name: Install mage run: go install github.com/magefile/mage diff --git a/config/compose.toml b/config/compose.toml index 332648e90..fd2901dde 100644 --- a/config/compose.toml +++ b/config/compose.toml @@ -30,6 +30,7 @@ password = "default-password" [services.did] name = "did" methods = ["key"] +resolution_methods = ["key", "web", "pkh", "peer"] [services.schema] name = "schema" diff --git a/config/config.go b/config/config.go index 09bd3e219..6cb30c24f 100644 --- a/config/config.go +++ b/config/config.go @@ -78,7 +78,8 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool { type DIDServiceConfig struct { *BaseServiceConfig - Methods []string `toml:"methods"` + Methods []string `toml:"methods"` + ResolutionMethods []string `toml:"resolution_methods"` } func (d *DIDServiceConfig) IsEmpty() bool { @@ -184,6 +185,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) { DIDConfig: DIDServiceConfig{ BaseServiceConfig: &BaseServiceConfig{Name: "did"}, Methods: []string{"key"}, + ResolutionMethods: []string{"key", "peer", "web", "pkh"}, }, SchemaConfig: SchemaServiceConfig{ BaseServiceConfig: &BaseServiceConfig{Name: "schema"}, diff --git a/config/config.toml b/config/config.toml index afd2ff38d..f89ab566f 100644 --- a/config/config.toml +++ b/config/config.toml @@ -28,6 +28,7 @@ password = "default-password" [services.did] name = "did" methods = ["key"] +resolution_methods = ["key", "web", "pkh", "peer"] [services.schema] name = "schema" diff --git a/go.mod b/go.mod index bc899e1c1..497ce0f1a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/BurntSushi/toml v1.2.0 - github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221001002612-ec8641015c21 + github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007180051-d48966db0f15 github.com/ardanlabs/conf v1.5.0 github.com/dimfeld/httptreemux/v5 v5.4.0 github.com/go-playground/locales v0.14.0 @@ -21,7 +21,7 @@ require ( go.opentelemetry.io/otel/exporters/jaeger v1.10.0 go.opentelemetry.io/otel/sdk v1.10.0 go.opentelemetry.io/otel/trace v1.10.0 - golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 + golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b gopkg.in/go-playground/validator.v9 v9.31.0 ) @@ -51,7 +51,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect + golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect diff --git a/go.sum b/go.sum index 36431af61..9cf6e77ec 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,12 @@ github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220921215642-f749c4d89b8b h1:0IJ github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220921215642-f749c4d89b8b/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA= github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221001002612-ec8641015c21 h1:tsvfXXVW2rqdPIYJ5uLSUhTXG4C6niB9FmjpgQE/f9w= github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221001002612-ec8641015c21/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA= +github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221006024829-0be992d222cd h1:TfikEZGgOY0nSlJ3GlIBLisMu9zkV0cIXg+dbXX4poI= +github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221006024829-0be992d222cd/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA= +github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007010336-560f7ccb3b93 h1:paLRTB3VnfKfcJgx43faOgVM2ypKFJDrvovq5/BykL0= +github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007010336-560f7ccb3b93/go.mod h1:oi28ndJG+ksI/NvGI+x7t6xHiDXYGOYxORTjuVynklQ= +github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007180051-d48966db0f15 h1:tg8HA0K2/a7X6K/K5ayToC7Yrout5+v2CZH5sGpX2mc= +github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221007180051-d48966db0f15/go.mod h1:oi28ndJG+ksI/NvGI+x7t6xHiDXYGOYxORTjuVynklQ= github.com/ardanlabs/conf v1.5.0 h1:5TwP6Wu9Xi07eLFEpiCUF3oQXh9UzHMDVnD3u/I5d5c= github.com/ardanlabs/conf v1.5.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -130,6 +136,8 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY= golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= +golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -141,6 +149,8 @@ golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3B golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM= +golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/credential/verification.go b/internal/credential/verification.go new file mode 100644 index 000000000..ddc4813ed --- /dev/null +++ b/internal/credential/verification.go @@ -0,0 +1,188 @@ +package credential + +import ( + "crypto" + "fmt" + + credsdk "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/signing" + "github.com/TBD54566975/ssi-sdk/credential/verification" + "github.com/TBD54566975/ssi-sdk/cryptosuite" + didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/goccy/go-json" + "github.com/lestrrat-go/jwx/jwk" + "github.com/mr-tron/base58" + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-varint" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/tbd54566975/ssi-service/internal/keyaccess" + "github.com/tbd54566975/ssi-service/internal/util" +) + +type CredentialVerifier struct { + verifier *verification.CredentialVerifier + resolver *didsdk.Resolver +} + +// NewCredentialVerifier creates a new credential verifier which executes both signature and static verification checks. +// In the future the set of verification checks will be configurable. +func NewCredentialVerifier(resolver *didsdk.Resolver) (*CredentialVerifier, error) { + if resolver == nil { + return nil, errors.New("resolver is nil") + } + // TODO(gabe): consider making this configurable + verifiers := verification.KnownVerifiers + verifier, err := verification.NewCredentialVerifier(verifiers) + if err != nil { + return nil, errors.Wrap(err, "failed to create static credential verifier") + } + return &CredentialVerifier{ + verifier: verifier, + resolver: resolver, + }, nil +} + +// TODO(gabe) consider moving this verification logic to the sdk https://github.com/TBD54566975/ssi-service/issues/122 + +// 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 CredentialVerifier) VerifyJWTCredential(token string) error { + cred, err := signing.ParseVerifiableCredentialFromJWT(token) + if err != nil { + return util.LoggingErrorMsg(err, "could not parse credential from JWT") + } + kid, pubKey, err := v.getVerificationInformation(cred.Issuer.(string), "") + if err != nil { + return util.LoggingErrorMsg(err, "could not get verification from credential") + } + verifier, err := keyaccess.NewJWKKeyAccessVerifier(kid, pubKey) + if err != nil { + return util.LoggingErrorMsg(err, "could not create verifier") + } + if err := verifier.Verify(keyaccess.JWKToken{Token: token}); err != nil { + return util.LoggingErrorMsg(err, "could not verify credential's signature") + } + if err = v.verifier.VerifyCredential(*cred); err != nil { + return util.LoggingErrorMsg(err, "static credential verification failed") + } + return err +} + +// 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 CredentialVerifier) VerifyDataIntegrityCredential(credential credsdk.VerifiableCredential) error { + // TODO(gabe): perhaps this should be a verification method referenced on the proof object, not the issuer + kid, pubKey, err := v.getVerificationInformation(credential.Issuer.(string), "") + if err != nil { + return util.LoggingErrorMsg(err, "could not get verification from credential") + } + verifier, err := keyaccess.NewDataIntegrityKeyAccess(kid, pubKey) + if err != nil { + return util.LoggingErrorMsg(err, "could not create verifier") + } + if err := verifier.Verify(&credential); err != nil { + return util.LoggingErrorMsg(err, "could not verify credential's signature") + } + if err = v.verifier.VerifyCredential(credential); err != nil { + return util.LoggingErrorMsg(err, "static credential verification failed") + } + return err +} + +// getVerificationInformation resolves a DID and provides a kid and public key needed for credential verification +// it is possible that a DID has multiple verification methods, in which case a kid must be provided, otherwise +// resolution will fail. +func (v CredentialVerifier) getVerificationInformation(did, maybeKID string) (kid string, pubKey crypto.PublicKey, err error) { + resolved, err := v.resolver.Resolve(did) + if err != nil { + return "", nil, errors.Wrapf(err, "failed to resolve did: %s", did) + } + if resolved.DIDDocument.IsEmpty() { + return "", nil, errors.Errorf("did doc: %s is empty", did) + } + verificationMethods := resolved.DIDDocument.VerificationMethod + if len(verificationMethods) == 0 { + return "", nil, errors.Errorf("did doc: %s has no verification methods", did) + } + + // handle the case where a kid is provided && there are multiple verification methods + if len(verificationMethods) > 1 { + if kid == "" { + return "", nil, errors.Errorf("kid is required for did: %s, which has multiple verification methods", did) + } + for _, method := range verificationMethods { + if method.ID == kid { + kid = did + pubKey, err = extractKeyFromVerificationMethod(verificationMethods[0]) + return + } + } + } + // TODO(gabe): some DIDs, like did:key have KIDs that aren't used, so we need to know when to use a kid vs the DID + kid = did + pubKey, err = extractKeyFromVerificationMethod(verificationMethods[0]) + return +} + +func extractKeyFromVerificationMethod(method didsdk.VerificationMethod) (pubKey crypto.PublicKey, err error) { + if method.PublicKeyMultibase != "" { + pubKeyBytes, multiBaseErr := multibaseToPubKeyBytes(method.PublicKeyMultibase) + if multiBaseErr != nil { + err = multiBaseErr + return + } + pubKey, err = cryptosuite.PubKeyBytesToTypedKey(pubKeyBytes, method.Type) + return + } else if method.PublicKeyBase58 != "" { + pubKeyDecoded, b58Err := base58.Decode(method.PublicKeyBase58) + if b58Err != nil { + err = b58Err + return + } + pubKey, err = cryptosuite.PubKeyBytesToTypedKey(pubKeyDecoded, method.Type) + return + } else if method.PublicKeyJWK != nil { + jwkBytes, jwkErr := json.Marshal(method.PublicKeyJWK) + if err != nil { + err = jwkErr + return + } + pubKey, err = jwk.ParseKey(jwkBytes) + return + } + err = errors.New("no public key found in verification method") + return +} + +// multibaseToPubKey converts a multibase encoded public key to public key bytes for known multibase encodings +func multibaseToPubKeyBytes(mb string) ([]byte, error) { + if mb == "" { + err := fmt.Errorf("could not decode value: %s", mb) + logrus.WithError(err).Error() + return nil, err + } + + encoding, decoded, err := multibase.Decode(mb) + if err != nil { + logrus.WithError(err).Error("could not decode did:key") + return nil, err + } + if encoding != didsdk.Base58BTCMultiBase { + err := fmt.Errorf("expected %d encoding but found %d", didsdk.Base58BTCMultiBase, encoding) + logrus.WithError(err).Error() + return nil, err + } + + // n = # bytes for the int, which we expect to be two from our multicodec + _, n, err := varint.FromUvarint(decoded) + if err != nil { + return nil, err + } + if n != 2 { + return nil, errors.New("error parsing did:key varint") + } + pubKeyBytes := decoded[n:] + return pubKeyBytes, nil +} diff --git a/internal/did/resolver.go b/internal/did/resolver.go new file mode 100644 index 000000000..8622d3b93 --- /dev/null +++ b/internal/did/resolver.go @@ -0,0 +1,42 @@ +package did + +import ( + "fmt" + + didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/pkg/errors" +) + +// BuildResolver builds a DID resolver from a list of methods to support resolution for +func BuildResolver(methods []string) (*didsdk.Resolver, error) { + if len(methods) == 0 { + return nil, errors.New("no methods provided") + } + var resolvers []didsdk.Resolution + for _, method := range methods { + resolver, err := getKnownResolver(method) + if err != nil { + return nil, err + } + resolvers = append(resolvers, resolver) + } + if len(resolvers) == 0 { + return nil, errors.New("no resolvers created") + } + return didsdk.NewResolver(resolvers...) +} + +// all possible resolvers for the DID service +func getKnownResolver(method string) (didsdk.Resolution, error) { + switch didsdk.Method(method) { + case didsdk.KeyMethod: + return new(didsdk.KeyResolver), nil + case didsdk.WebMethod: + return new(didsdk.WebResolver), nil + case didsdk.PKHMethod: + return new(didsdk.PKHResolver), nil + case didsdk.PeerMethod: + return new(didsdk.PeerResolver), nil + } + return nil, fmt.Errorf("unsupported method: %s", method) +} diff --git a/internal/did/resolver_test.go b/internal/did/resolver_test.go new file mode 100644 index 000000000..4f6c4a475 --- /dev/null +++ b/internal/did/resolver_test.go @@ -0,0 +1,32 @@ +package did + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolver(t *testing.T) { + // empty resolver + _, err := BuildResolver(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no methods provided") + + // unsupported method + _, err = BuildResolver([]string{"unsupported"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported method: unsupported") + + // valid method + resolver, err := BuildResolver([]string{"key"}) + assert.NoError(t, err) + assert.NotEmpty(t, resolver) + resolved, err := resolver.Resolve("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp") + assert.NoError(t, err) + assert.NotEmpty(t, resolved) + + // resolve for a method that is not supported + _, err = resolver.Resolve("did:web:example.com") + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported method: web") +} diff --git a/internal/keyaccess/jwt.go b/internal/keyaccess/jwt.go index bdbaa2ec5..c5a4fa4ca 100644 --- a/internal/keyaccess/jwt.go +++ b/internal/keyaccess/jwt.go @@ -11,11 +11,11 @@ import ( ) type JWKKeyAccess struct { - crypto.JWTSigner - crypto.JWTVerifier + *crypto.JWTSigner + *crypto.JWTVerifier } -// NewJWKKeyAccess creates a new JWKKeyAccess object from a key id and private key, generating both +// NewJWKKeyAccess creates a JWKKeyAccess object from a key id and private key, generating both // JWT Signer and Verifier objects. func NewJWKKeyAccess(kid string, key gocrypto.PrivateKey) (*JWKKeyAccess, error) { if kid == "" { @@ -37,8 +37,29 @@ func NewJWKKeyAccess(kid string, key gocrypto.PrivateKey) (*JWKKeyAccess, error) return nil, errors.Wrapf(err, "could not create JWK Key Access object for kid: %s, error creating verifier", kid) } return &JWKKeyAccess{ - JWTSigner: *signer, - JWTVerifier: *verifier, + JWTSigner: signer, + JWTVerifier: verifier, + }, nil +} + +// NewJWKKeyAccessVerifier creates JWKKeyAccess object from a key id and public key, generating a JWT Verifier object. +func NewJWKKeyAccessVerifier(kid string, key gocrypto.PublicKey) (*JWKKeyAccess, error) { + if kid == "" { + return nil, errors.New("kid cannot be empty") + } + if key == nil { + return nil, errors.New("key cannot be nil") + } + gotJWK, err := jwk.New(key) + if err != nil { + return nil, errors.Wrap(err, "could not create jwk from public key") + } + verifier, err := crypto.NewJWTVerifier(kid, gotJWK) + if err != nil { + return nil, errors.Wrapf(err, "could not create JWK Key Access object for kid: %s, error creating verifier", kid) + } + return &JWKKeyAccess{ + JWTVerifier: verifier, }, nil } @@ -48,6 +69,9 @@ type JWKToken struct { } func (ka JWKKeyAccess) Sign(payload map[string]interface{}) (*JWKToken, error) { + if ka.JWTSigner == nil { + return nil, errors.New("cannot sign with nil signer") + } if payload == nil { return nil, errors.New("payload cannot be nil") } @@ -66,10 +90,13 @@ func (ka JWKKeyAccess) Verify(token JWKToken) error { } func (ka JWKKeyAccess) SignVerifiableCredential(credential credential.VerifiableCredential) (*JWKToken, error) { + if ka.JWTSigner == nil { + return nil, errors.New("cannot sign with nil signer") + } if err := credential.IsValid(); err != nil { return nil, errors.New("cannot sign invalid credential") } - tokenBytes, err := signing.SignVerifiableCredentialJWT(ka.JWTSigner, credential) + tokenBytes, err := signing.SignVerifiableCredentialJWT(*ka.JWTSigner, credential) if err != nil { return nil, errors.Wrap(err, "could not sign credential") } @@ -80,14 +107,17 @@ func (ka JWKKeyAccess) VerifyVerifiableCredential(token JWKToken) (*credential.V if token.Token == "" { return nil, errors.New("token cannot be empty") } - return signing.VerifyVerifiableCredentialJWT(ka.JWTVerifier, token.Token) + return signing.VerifyVerifiableCredentialJWT(*ka.JWTVerifier, token.Token) } func (ka JWKKeyAccess) SignVerifiablePresentation(presentation credential.VerifiablePresentation) (*JWKToken, error) { + if ka.JWTSigner == nil { + return nil, errors.New("cannot sign with nil signer") + } if err := presentation.IsValid(); err != nil { return nil, errors.New("cannot sign invalid presentation") } - tokenBytes, err := signing.SignVerifiablePresentationJWT(ka.JWTSigner, presentation) + tokenBytes, err := signing.SignVerifiablePresentationJWT(*ka.JWTSigner, presentation) if err != nil { return nil, errors.Wrap(err, "could not sign presentation") } @@ -98,5 +128,5 @@ func (ka JWKKeyAccess) VerifyVerifiablePresentation(token JWKToken) (*credential if token.Token == "" { return nil, errors.New("token cannot be empty") } - return signing.VerifyVerifiablePresentationJWT(ka.JWTVerifier, token.Token) + return signing.VerifyVerifiablePresentationJWT(*ka.JWTVerifier, token.Token) } diff --git a/internal/keyaccess/jwt_test.go b/internal/keyaccess/jwt_test.go index c1a00730a..ef5e67e50 100644 --- a/internal/keyaccess/jwt_test.go +++ b/internal/keyaccess/jwt_test.go @@ -56,6 +56,19 @@ func TestJWKKeyAccessSignVerify(t *testing.T) { err = ka.Verify(*token) assert.NoError(tt, err) + + // Create just a verifier and check that it can verify the token + verifier, err := NewJWKKeyAccessVerifier(kid, privKey.Public()) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifier) + + err = verifier.Verify(*token) + assert.NoError(tt, err) + + // Make sure the verifier can't sign + _, err = verifier.Sign(data) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "cannot sign with nil signer") }) t.Run("Sign and Verify - Bad Data", func(tt *testing.T) { diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go index 824459e6b..67e7473f0 100644 --- a/pkg/server/router/credential.go +++ b/pkg/server/router/credential.go @@ -69,7 +69,7 @@ type CreateCredentialResponse struct { // CreateCredential godoc // @Summary Create Credential -// @Description Create credential +// @Description Create a credential // @Tags CredentialAPI // @Accept json // @Produce json @@ -114,7 +114,7 @@ type GetCredentialResponse struct { // @Success 200 {object} GetCredentialResponse // @Failure 400 {string} string "Bad request" // @Router /v1/credentials/{id} [get] -func (cr CredentialRouter) GetCredential(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (cr CredentialRouter) GetCredential(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { id := framework.GetParam(ctx, IDParam) if id == nil { errMsg := "cannot get credential without ID parameter" @@ -137,6 +137,59 @@ func (cr CredentialRouter) GetCredential(ctx context.Context, w http.ResponseWri return framework.Respond(ctx, w, resp, http.StatusOK) } +type VerifyCredentialRequest struct { + DataIntegrityCredential *credsdk.VerifiableCredential `json:"credential,omitempty"` + CredentialJWT *string `json:"credentialJwt,omitempty"` +} + +func (vcr VerifyCredentialRequest) IsValid() bool { + return (vcr.DataIntegrityCredential != nil && vcr.CredentialJWT == nil) || + (vcr.DataIntegrityCredential == nil && vcr.CredentialJWT != nil) +} + +type VerifyCredentialResponse struct { + Verified bool `json:"verified" json:"verified"` + Reason string `json:"reason,omitempty" json:"reason,omitempty"` +} + +// VerifyCredential godoc +// @Summary Verify Credential +// @Description Verify a given credential by its id +// @Tags CredentialAPI +// @Accept json +// @Produce json +// @Param request body VerifyCredentialRequest true "request body" +// @Success 200 {object} VerifyCredentialResponse +// @Failure 400 {string} string "Bad request" +// @Router /v1/credentials/verification [put] +func (cr CredentialRouter) VerifyCredential(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var request VerifyCredentialRequest + if err := framework.Decode(r, &request); err != nil { + errMsg := "invalid verify credential request" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + if !request.IsValid() { + err := errors.New("request must contain either a Data Integrity Credential or a JWT Credential") + logrus.WithError(err).Error() + return framework.NewRequestError(err, http.StatusBadRequest) + } + + verificationResult, err := cr.service.VerifyCredential(credential.VerifyCredentialRequest{ + DataIntegrityCredential: request.DataIntegrityCredential, + CredentialJWT: request.CredentialJWT, + }) + if err != nil { + errMsg := "could not verify credential" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError) + } + + resp := VerifyCredentialResponse{Verified: verificationResult.Verified, Reason: verificationResult.Reason} + return framework.Respond(ctx, w, resp, http.StatusOK) +} + type GetCredentialsResponse struct { Credentials []credmodel.CredentialContainer `json:"credentials"` } @@ -178,7 +231,7 @@ func (cr CredentialRouter) GetCredentials(ctx context.Context, w http.ResponseWr return err } -func (cr CredentialRouter) getCredentialsByIssuer(issuer string, ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (cr CredentialRouter) getCredentialsByIssuer(issuer string, ctx context.Context, w http.ResponseWriter, _ *http.Request) error { gotCredentials, err := cr.service.GetCredentialsByIssuer(credential.GetCredentialByIssuerRequest{Issuer: issuer}) if err != nil { errMsg := fmt.Sprintf("could not get credentials for issuer: %s", util.SanitizeLog(issuer)) @@ -190,7 +243,7 @@ func (cr CredentialRouter) getCredentialsByIssuer(issuer string, ctx context.Con return framework.Respond(ctx, w, resp, http.StatusOK) } -func (cr CredentialRouter) getCredentialsBySubject(subject string, ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (cr CredentialRouter) getCredentialsBySubject(subject string, ctx context.Context, w http.ResponseWriter, _ *http.Request) error { gotCredentials, err := cr.service.GetCredentialsBySubject(credential.GetCredentialBySubjectRequest{Subject: subject}) if err != nil { errMsg := fmt.Sprintf("could not get credentials for subject: %s", util.SanitizeLog(subject)) @@ -202,7 +255,7 @@ func (cr CredentialRouter) getCredentialsBySubject(subject string, ctx context.C return framework.Respond(ctx, w, resp, http.StatusOK) } -func (cr CredentialRouter) getCredentialsBySchema(schema string, ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (cr CredentialRouter) getCredentialsBySchema(schema string, ctx context.Context, w http.ResponseWriter, _ *http.Request) error { gotCredentials, err := cr.service.GetCredentialsBySchema(credential.GetCredentialBySchemaRequest{Schema: schema}) if err != nil { errMsg := fmt.Sprintf("could not get credentials for schema: %s", util.SanitizeLog(schema)) @@ -225,7 +278,7 @@ func (cr CredentialRouter) getCredentialsBySchema(schema string, ctx context.Con // @Failure 400 {string} string "Bad request" // @Failure 500 {string} string "Internal server error" // @Router /v1/credentials/{id} [delete] -func (cr CredentialRouter) DeleteCredential(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (cr CredentialRouter) DeleteCredential(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { id := framework.GetParam(ctx, IDParam) if id == nil { errMsg := "cannot delete credential without ID parameter" diff --git a/pkg/server/router/credential_test.go b/pkg/server/router/credential_test.go index 34a9cfeb2..980cee0e4 100644 --- a/pkg/server/router/credential_test.go +++ b/pkg/server/router/credential_test.go @@ -8,6 +8,7 @@ import ( credsdk "github.com/TBD54566975/ssi-sdk/credential" "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/stretchr/testify/assert" "github.com/tbd54566975/ssi-service/config" @@ -45,7 +46,7 @@ func TestCredentialRouter(t *testing.T) { serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} keyStoreService := testKeyStoreService(tt, bolt) didService := testDIDService(tt, bolt, keyStoreService) - credService, err := credential.NewCredentialService(serviceConfig, bolt, keyStoreService) + credService, err := credential.NewCredentialService(serviceConfig, bolt, keyStoreService, didService.GetResolver()) assert.NoError(tt, err) assert.NotEmpty(tt, credService) @@ -55,7 +56,7 @@ func TestCredentialRouter(t *testing.T) { // create a credential - issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519}) + issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) assert.NoError(tt, err) assert.NotEmpty(tt, issuerDID) diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index 2bc1702f0..a59061d00 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -40,7 +40,7 @@ func NewDIDRouter(s svcframework.Service) (*DIDRouter, error) { } type GetDIDMethodsResponse struct { - DIDMethods []did.Method `json:"didMethods,omitempty"` + DIDMethods []didsdk.Method `json:"methods,omitempty"` } // GetDIDMethods godoc @@ -94,7 +94,7 @@ func (dr DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter } // TODO(gabe) check if the key type is supported for the method, to tell whether this is a bad req or internal error - createDIDRequest := did.CreateDIDRequest{Method: did.Method(*method), KeyType: request.KeyType} + createDIDRequest := did.CreateDIDRequest{Method: didsdk.Method(*method), KeyType: request.KeyType} createDIDResponse, err := dr.service.CreateDIDByMethod(createDIDRequest) if err != nil { errMsg := fmt.Sprintf("could not create DID for method<%s> with key type: %s", *method, request.KeyType) @@ -141,7 +141,7 @@ func (dr DIDRouter) GetDIDByMethod(ctx context.Context, w http.ResponseWriter, _ // TODO(gabe) check if the method is supported, to tell whether this is a bad req or internal error // TODO(gabe) differentiate between internal errors and not found DIDs - getDIDRequest := did.GetDIDRequest{Method: did.Method(*method), ID: *id} + getDIDRequest := did.GetDIDRequest{Method: didsdk.Method(*method), ID: *id} gotDID, err := dr.service.GetDIDByMethod(getDIDRequest) if err != nil { errMsg := fmt.Sprintf("could not get DID for method<%s> with id: %s", *method, *id) @@ -177,7 +177,7 @@ func (dr DIDRouter) GetDIDsByMethod(ctx context.Context, w http.ResponseWriter, // TODO(gabe) check if the method is supported, to tell whether this is a bad req or internal error // TODO(gabe) differentiate between internal errors and not found DIDs - getDIDsRequest := did.GetDIDsRequest{Method: did.Method(*method)} + getDIDsRequest := did.GetDIDsRequest{Method: didsdk.Method(*method)} gotDIDs, err := dr.service.GetDIDsByMethod(getDIDsRequest) if err != nil { errMsg := fmt.Sprintf("could not get DIDs for method: %s", *method) @@ -188,3 +188,39 @@ func (dr DIDRouter) GetDIDsByMethod(ctx context.Context, w http.ResponseWriter, resp := GetDIDsByMethodResponse{DIDs: gotDIDs.DIDs} return framework.Respond(ctx, w, resp, http.StatusOK) } + +type ResolveDIDResponse struct { + ResolutionMetadata *didsdk.DIDResolutionMetadata `json:"didResolutionMetadata,omitempty"` + DIDDocument *didsdk.DIDDocument `json:"didDocument"` + DIDDocumentMetadata *didsdk.DIDDocumentMetadata `json:"didDocumentMetadata,omitempty"` +} + +// ResolveDID godoc +// @Summary Resolve a DID +// @Description Resolve a DID that may not be stored in this service +// @Tags DecentralizedIdentityAPI +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} ResolveDIDResponse +// @Failure 400 {string} string "Bad request" +// @Router /v1/dids/resolver/{id} [get] +func (dr DIDRouter) ResolveDID(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { + id := framework.GetParam(ctx, IDParam) + if id == nil { + errMsg := "get DID request missing id parameter" + logrus.Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + resolveDIDRequest := did.ResolveDIDRequest{DID: *id} + resolvedDID, err := dr.service.ResolveDID(resolveDIDRequest) + if err != nil { + errMsg := fmt.Sprintf("could not get DID with id: %s", *id) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + resp := ResolveDIDResponse{ResolutionMetadata: resolvedDID.ResolutionMetadata, DIDDocument: resolvedDID.DIDDocument, DIDDocumentMetadata: resolvedDID.DIDDocumentMetadata} + return framework.Respond(ctx, w, resp, http.StatusOK) +} diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 3799ed9b7..49bacef42 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/stretchr/testify/assert" "github.com/tbd54566975/ssi-service/config" @@ -40,7 +41,8 @@ func TestDIDRouter(t *testing.T) { assert.NotEmpty(tt, db) keyStoreService := testKeyStoreService(tt, db) - serviceConfig := config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}} + methods := []string{didsdk.KeyMethod.String()} + serviceConfig := config.DIDServiceConfig{Methods: methods, ResolutionMethods: methods} didService, err := did.NewDIDService(serviceConfig, db, keyStoreService) assert.NoError(tt, err) assert.NotEmpty(tt, didService) @@ -57,15 +59,15 @@ func TestDIDRouter(t *testing.T) { supported := didService.GetSupportedMethods() assert.NotEmpty(tt, supported) assert.Len(tt, supported.Methods, 1) - assert.Equal(tt, did.KeyMethod, supported.Methods[0]) + assert.Equal(tt, didsdk.KeyMethod, supported.Methods[0]) // bad key type - _, err = didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: "bad"}) + _, err = didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: "bad"}) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not create did:key") // good key type - createDIDResponse, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519}) + createDIDResponse, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) assert.NoError(tt, err) assert.NotEmpty(tt, createDIDResponse) @@ -73,7 +75,7 @@ func TestDIDRouter(t *testing.T) { assert.Contains(tt, createDIDResponse.DID.ID, "did:key") // get it back - getDIDResponse, err := didService.GetDIDByMethod(did.GetDIDRequest{Method: did.KeyMethod, ID: createDIDResponse.DID.ID}) + getDIDResponse, err := didService.GetDIDByMethod(did.GetDIDRequest{Method: didsdk.KeyMethod, ID: createDIDResponse.DID.ID}) assert.NoError(tt, err) assert.NotEmpty(tt, getDIDResponse) @@ -81,12 +83,12 @@ func TestDIDRouter(t *testing.T) { assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) // create a second DID - createDIDResponse2, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519}) + createDIDResponse2, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519}) assert.NoError(tt, err) assert.NotEmpty(tt, createDIDResponse2) // get all DIDs back - getDIDsResponse, err := didService.GetDIDsByMethod(did.GetDIDsRequest{Method: did.KeyMethod}) + getDIDsResponse, err := didService.GetDIDsByMethod(did.GetDIDsRequest{Method: didsdk.KeyMethod}) assert.NoError(tt, err) assert.NotEmpty(tt, getDIDsResponse) assert.Len(tt, getDIDsResponse.DIDs, 2) diff --git a/pkg/server/router/dwn_test.go b/pkg/server/router/dwn_test.go index 8f0b7f98a..486d9d759 100644 --- a/pkg/server/router/dwn_test.go +++ b/pkg/server/router/dwn_test.go @@ -39,7 +39,8 @@ func TestDWNRouter(t *testing.T) { serviceConfig := config.DWNServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "dwn"}} keyStore := testKeyStoreService(t, bolt) - credentialService := testCredentialService(t, bolt, keyStore) + didService := testDIDService(t, bolt, keyStore) + credentialService := testCredentialService(t, bolt, keyStore, didService) manifestService := testManifestService(t, bolt, keyStore, credentialService) dwnService, err := dwn.NewDWNService(serviceConfig, bolt, keyStore, manifestService) assert.NoError(tt, err) diff --git a/pkg/server/router/manifest_test.go b/pkg/server/router/manifest_test.go index 348f7664a..6002799aa 100644 --- a/pkg/server/router/manifest_test.go +++ b/pkg/server/router/manifest_test.go @@ -7,6 +7,7 @@ import ( "github.com/TBD54566975/ssi-sdk/credential/exchange" manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest" "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -44,7 +45,8 @@ func TestManifestRouter(t *testing.T) { serviceConfig := config.ManifestServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "manifest"}} keyStoreService := testKeyStoreService(tt, bolt) - testCredentialService := testCredentialService(tt, bolt, keyStoreService) + didService := testDIDService(tt, bolt, keyStoreService) + testCredentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestService, err := manifest.NewManifestService(serviceConfig, bolt, keyStoreService, testCredentialService) assert.NoError(tt, err) assert.NotEmpty(tt, manifestService) @@ -54,9 +56,8 @@ func TestManifestRouter(t *testing.T) { assert.Equal(tt, framework.StatusReady, manifestService.Status().Status) // create issuer and applicant DIDs - didService := testDIDService(tt, bolt, keyStoreService) createDIDRequest := did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, } issuerDID, err := didService.CreateDIDByMethod(createDIDRequest) diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 4bbf129a5..89ebd6b51 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -1,8 +1,9 @@ package router import ( + didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/tbd54566975/ssi-service/config" - "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/service/framework" ) @@ -22,7 +23,7 @@ func (s *testService) Config() config.ServicesConfig { return config.ServicesConfig{ StorageProvider: "bolt", KeyStoreConfig: config.KeyStoreServiceConfig{ServiceKeyPassword: "test-password"}, - DIDConfig: config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}}, + DIDConfig: config.DIDServiceConfig{Methods: []string{string(didsdk.KeyMethod)}}, SchemaConfig: config.SchemaServiceConfig{}, CredentialConfig: config.CredentialServiceConfig{}, ManifestConfig: config.ManifestServiceConfig{}, diff --git a/pkg/server/router/testutils_test.go b/pkg/server/router/testutils_test.go index 1a052a6e2..f94797160 100644 --- a/pkg/server/router/testutils_test.go +++ b/pkg/server/router/testutils_test.go @@ -23,7 +23,13 @@ func testKeyStoreService(t *testing.T, db *storage.BoltDB) *keystore.Service { } func testDIDService(t *testing.T, db *storage.BoltDB, keyStore *keystore.Service) *did.Service { - serviceConfig := config.DIDServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "did"}, Methods: []string{"key"}} + serviceConfig := config.DIDServiceConfig{ + BaseServiceConfig: &config.BaseServiceConfig{ + Name: "did", + }, + Methods: []string{"key"}, + ResolutionMethods: []string{"key"}, + } // create a did service didService, err := did.NewDIDService(serviceConfig, db, keyStore) require.NoError(t, err) @@ -31,10 +37,10 @@ func testDIDService(t *testing.T, db *storage.BoltDB, keyStore *keystore.Service return didService } -func testCredentialService(t *testing.T, db *storage.BoltDB, keyStore *keystore.Service) *credential.Service { +func testCredentialService(t *testing.T, db *storage.BoltDB, keyStore *keystore.Service, did *did.Service) *credential.Service { serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} // create a credential service - credentialService, err := credential.NewCredentialService(serviceConfig, db, keyStore) + credentialService, err := credential.NewCredentialService(serviceConfig, db, keyStore, did.GetResolver()) require.NoError(t, err) require.NotEmpty(t, credentialService) return credentialService diff --git a/pkg/server/server_credential_test.go b/pkg/server/server_credential_test.go index eb6538712..ef829b513 100644 --- a/pkg/server/server_credential_test.go +++ b/pkg/server/server_credential_test.go @@ -9,10 +9,12 @@ import ( "time" credsdk "github.com/TBD54566975/ssi-sdk/credential" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -22,6 +24,7 @@ import ( func TestCredentialAPI(t *testing.T) { t.Run("Test Create Credential", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -30,11 +33,11 @@ func TestCredentialAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credService := testCredentialRouter(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credService := testCredentialRouter(tt, bolt, keyStoreService, didService) issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -96,14 +99,13 @@ func TestCredentialAPI(t *testing.T) { assert.NoError(tt, err) assert.NotEmpty(tt, resp.CredentialJWT) - parsedCred, err := signing.ParseVerifiableCredentialFromJWT(*resp.CredentialJWT) assert.NoError(tt, err) - assert.NotEmpty(tt, parsedCred) - assert.Equal(tt, parsedCred.Issuer, issuerDID.DID.ID) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) }) t.Run("Test Get Credential By ID", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -112,8 +114,8 @@ func TestCredentialAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credService := testCredentialRouter(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credService := testCredentialRouter(tt, bolt, keyStoreService, didService) w := httptest.NewRecorder() @@ -136,7 +138,7 @@ func TestCredentialAPI(t *testing.T) { w.Flush() issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -178,6 +180,7 @@ func TestCredentialAPI(t *testing.T) { t.Run("Test Get Credential By Schema", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -186,13 +189,13 @@ func TestCredentialAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credService := testCredentialRouter(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credService := testCredentialRouter(tt, bolt, keyStoreService, didService) w := httptest.NewRecorder() issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -238,6 +241,7 @@ func TestCredentialAPI(t *testing.T) { t.Run("Test Get Credential By Issuer", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -246,13 +250,13 @@ func TestCredentialAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credService := testCredentialRouter(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credService := testCredentialRouter(tt, bolt, keyStoreService, didService) w := httptest.NewRecorder() issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -298,6 +302,7 @@ func TestCredentialAPI(t *testing.T) { t.Run("Test Get Credential By Subject", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -306,13 +311,13 @@ func TestCredentialAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credService := testCredentialRouter(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credService := testCredentialRouter(tt, bolt, keyStoreService, didService) w := httptest.NewRecorder() issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -359,6 +364,7 @@ func TestCredentialAPI(t *testing.T) { t.Run("Test Delete Credential", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -367,11 +373,11 @@ func TestCredentialAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credService := testCredentialRouter(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credService := testCredentialRouter(tt, bolt, keyStoreService, didService) issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -425,4 +431,76 @@ func TestCredentialAPI(t *testing.T) { assert.Error(tt, err) assert.Contains(tt, err.Error(), fmt.Sprintf("could not get credential with id: %s", credID)) }) + + t.Run("Test Verifying a Credential", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + require.NoError(tt, err) + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + credService := testCredentialRouter(tt, bolt, keyStoreService, didService) + + issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // good request + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + w := httptest.NewRecorder() + err = credService.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NoError(tt, err) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + w.Flush() + + // verify the credential + requestValue = newRequestValue(tt, router.VerifyCredentialRequest{CredentialJWT: resp.CredentialJWT}) + req = httptest.NewRequest(http.MethodPost, "https://ssi-service.com/v1/credentials/verification", requestValue) + err = credService.VerifyCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var verifyResp router.VerifyCredentialResponse + err = json.NewDecoder(w.Body).Decode(&verifyResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifyResp) + assert.True(tt, verifyResp.Verified) + + // bad credential + requestValue = newRequestValue(tt, router.VerifyCredentialRequest{CredentialJWT: util.StringPtr("badjwt")}) + req = httptest.NewRequest(http.MethodPost, "https://ssi-service.com/v1/credentials/verification", requestValue) + err = credService.VerifyCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + err = json.NewDecoder(w.Body).Decode(&verifyResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifyResp) + assert.False(tt, verifyResp.Verified) + assert.Contains(tt, verifyResp.Reason, "could not parse credential from JWT") + }) } diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go index f5bf3b50d..25a512040 100644 --- a/pkg/server/server_did_test.go +++ b/pkg/server/server_did_test.go @@ -8,17 +8,19 @@ import ( "testing" "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tbd54566975/ssi-service/pkg/server/router" - "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/storage" ) func TestDIDAPI(t *testing.T) { t.Run("Test Get DID Methods", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -42,11 +44,12 @@ func TestDIDAPI(t *testing.T) { assert.NoError(tt, err) assert.Len(tt, resp.DIDMethods, 1) - assert.Equal(tt, resp.DIDMethods[0], did.KeyMethod) + assert.Equal(tt, resp.DIDMethods[0], didsdk.KeyMethod) }) t.Run("Test Create DID By Method: Key", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -95,11 +98,12 @@ func TestDIDAPI(t *testing.T) { err = json.NewDecoder(w.Body).Decode(&resp) assert.NoError(tt, err) - assert.Contains(tt, resp.DID.ID, did.KeyMethod) + assert.Contains(tt, resp.DID.ID, didsdk.KeyMethod) }) t.Run("Test Get DID By Method", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -174,6 +178,7 @@ func TestDIDAPI(t *testing.T) { t.Run("Test Get DIDs By Method", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -259,4 +264,56 @@ func TestDIDAPI(t *testing.T) { } assert.Len(tt, knownDIDs, 0) }) + + t.Run("Test Resolve DIDs", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + require.NoError(tt, err) + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + _, keyStore := testKeyStore(tt, bolt) + didService := testDIDRouter(tt, bolt, keyStore) + + // bad resolution request + req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/bad", nil) + w := httptest.NewRecorder() + + badParams := map[string]string{ + "id": "bad", + } + err = didService.ResolveDID(newRequestContextWithParams(badParams), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "not a valid did: bad") + + w.Flush() + + // known method, bad did + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/did:key:abcd", nil) + badParams = map[string]string{ + "id": "did:key:abcd", + } + err = didService.ResolveDID(newRequestContextWithParams(badParams), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "did:key:abcd: selected encoding not supported") + + w.Flush() + + // known method, good did + req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/resolver/did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", nil) + goodParams := map[string]string{ + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + } + err = didService.ResolveDID(newRequestContextWithParams(goodParams), w, req) + assert.NoError(tt, err) + + var resolutionResponse router.ResolveDIDResponse + err = json.NewDecoder(w.Body).Decode(&resolutionResponse) + assert.NoError(tt, err) + assert.NotEmpty(tt, resolutionResponse.DIDDocument) + assert.Equal(tt, "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", resolutionResponse.DIDDocument.ID) + }) } diff --git a/pkg/server/server_dwn_test.go b/pkg/server/server_dwn_test.go index 7b7cfde42..a30765001 100644 --- a/pkg/server/server_dwn_test.go +++ b/pkg/server/server_dwn_test.go @@ -7,8 +7,10 @@ import ( "testing" "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -18,6 +20,7 @@ import ( func TestDWNAPI(t *testing.T) { t.Run("Test DWN Publish Manifest", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -26,8 +29,8 @@ func TestDWNAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, manifestService := testManifest(tt, bolt, keyStoreService, credentialService) dwnService := testDWNRouter(tt, bolt, keyStoreService, manifestService) @@ -35,7 +38,7 @@ func TestDWNAPI(t *testing.T) { // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) diff --git a/pkg/server/server_keystore_test.go b/pkg/server/server_keystore_test.go index bfb5d64c4..6357042a3 100644 --- a/pkg/server/server_keystore_test.go +++ b/pkg/server/server_keystore_test.go @@ -11,6 +11,7 @@ import ( "github.com/goccy/go-json" "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/storage" @@ -19,6 +20,7 @@ import ( func TestKeyStoreAPI(t *testing.T) { t.Run("Test Store Key", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -68,6 +70,7 @@ func TestKeyStoreAPI(t *testing.T) { t.Run("Test Get Key Details", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { diff --git a/pkg/server/server_manifest_test.go b/pkg/server/server_manifest_test.go index c85387ca0..9f212425a 100644 --- a/pkg/server/server_manifest_test.go +++ b/pkg/server/server_manifest_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -19,6 +21,7 @@ import ( func TestManifestAPI(t *testing.T) { t.Run("Test Create Manifest", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -27,8 +30,8 @@ func TestManifestAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, _ := testManifest(tt, bolt, keyStoreService, credentialService) // missing required field: Manifest @@ -47,7 +50,7 @@ func TestManifestAPI(t *testing.T) { // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -73,6 +76,7 @@ func TestManifestAPI(t *testing.T) { t.Run("Test Get Manifest By ID", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -81,8 +85,8 @@ func TestManifestAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, _ := testManifest(tt, bolt, keyStoreService, credentialService) w := httptest.NewRecorder() @@ -107,7 +111,7 @@ func TestManifestAPI(t *testing.T) { // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -139,6 +143,7 @@ func TestManifestAPI(t *testing.T) { t.Run("Test Get Manifests", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -147,15 +152,15 @@ func TestManifestAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, _ := testManifest(tt, bolt, keyStoreService, credentialService) w := httptest.NewRecorder() // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -188,6 +193,7 @@ func TestManifestAPI(t *testing.T) { t.Run("Test Delete Manifest", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -196,13 +202,13 @@ func TestManifestAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, _ := testManifest(tt, bolt, keyStoreService, credentialService) // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -253,6 +259,7 @@ func TestManifestAPI(t *testing.T) { t.Run("Test Submit Application", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -261,8 +268,8 @@ func TestManifestAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, _ := testManifest(tt, bolt, keyStoreService, credentialService) @@ -284,7 +291,7 @@ func TestManifestAPI(t *testing.T) { // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -326,6 +333,7 @@ func TestManifestAPI(t *testing.T) { t.Run("Test Get Application By ID and Get Applications", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -334,8 +342,8 @@ func TestManifestAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, _ := testManifest(tt, bolt, keyStoreService, credentialService) w := httptest.NewRecorder() @@ -350,7 +358,7 @@ func TestManifestAPI(t *testing.T) { // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) @@ -429,6 +437,7 @@ func TestManifestAPI(t *testing.T) { t.Run("Test Delete Application", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -437,13 +446,13 @@ func TestManifestAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - credentialService := testCredentialService(tt, bolt, keyStoreService) didService := testDIDService(tt, bolt, keyStoreService) + credentialService := testCredentialService(tt, bolt, keyStoreService, didService) manifestRouter, _ := testManifest(tt, bolt, keyStoreService, credentialService) // create an issuer issuerDIDDoc, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ - Method: did.KeyMethod, + Method: didsdk.KeyMethod, KeyType: crypto.Ed25519, }) assert.NoError(tt, err) diff --git a/pkg/server/server_schema_test.go b/pkg/server/server_schema_test.go index deb0d00d1..60c4adb65 100644 --- a/pkg/server/server_schema_test.go +++ b/pkg/server/server_schema_test.go @@ -9,6 +9,7 @@ import ( "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/storage" @@ -17,6 +18,7 @@ import ( func TestSchemaAPI(t *testing.T) { t.Run("Test Create Schema", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { @@ -65,6 +67,7 @@ func TestSchemaAPI(t *testing.T) { t.Run("Test Get Schemas", func(tt *testing.T) { bolt, err := storage.NewBoltDB() + require.NoError(tt, err) // remove the db file after the test tt.Cleanup(func() { diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index e42eae789..95a8a21c0 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -217,6 +217,7 @@ func testDIDService(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Servi serviceConfig := config.DIDServiceConfig{ BaseServiceConfig: &config.BaseServiceConfig{Name: "test-did"}, Methods: []string{"key"}, + ResolutionMethods: []string{"key"}, } // create a did service @@ -253,18 +254,18 @@ func testSchemaRouter(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Ser return schemaRouter } -func testCredentialService(t *testing.T, db *storage.BoltDB, keyStore *keystore.Service) *credential.Service { +func testCredentialService(t *testing.T, db *storage.BoltDB, keyStore *keystore.Service, did *did.Service) *credential.Service { serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}} // create a credential service - credentialService, err := credential.NewCredentialService(serviceConfig, db, keyStore) + credentialService, err := credential.NewCredentialService(serviceConfig, db, keyStore, did.GetResolver()) require.NoError(t, err) require.NotEmpty(t, credentialService) return credentialService } -func testCredentialRouter(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Service) *router.CredentialRouter { - credentialService := testCredentialService(t, bolt, keyStore) +func testCredentialRouter(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Service, did *did.Service) *router.CredentialRouter { + credentialService := testCredentialService(t, bolt, keyStore, did) // create router for service credentialRouter, err := router.NewCredentialRouter(credentialService) diff --git a/pkg/service/credential/credential.go b/pkg/service/credential/credential.go index 2e6071d1b..11aab8cfe 100644 --- a/pkg/service/credential/credential.go +++ b/pkg/service/credential/credential.go @@ -5,11 +5,12 @@ import ( "time" "github.com/TBD54566975/ssi-sdk/credential" + didint "github.com/TBD54566975/ssi-sdk/did" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/tbd54566975/ssi-service/config" - credmodel "github.com/tbd54566975/ssi-service/internal/credential" + credint "github.com/tbd54566975/ssi-service/internal/credential" "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" credstorage "github.com/tbd54566975/ssi-service/pkg/service/credential/storage" @@ -19,8 +20,9 @@ import ( ) type Service struct { - storage credstorage.Storage - config config.CredentialServiceConfig + storage credstorage.Storage + config config.CredentialServiceConfig + verifier *credint.CredentialVerifier // external dependencies keyStore *keystore.Service @@ -44,15 +46,20 @@ func (s Service) Config() config.CredentialServiceConfig { return s.config } -func NewCredentialService(config config.CredentialServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) { +func NewCredentialService(config config.CredentialServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, resolver *didint.Resolver) (*Service, error) { credentialStorage, err := credstorage.NewCredentialStorage(s) if err != nil { errMsg := "could not instantiate storage for the credential service" return nil, util.LoggingErrorMsg(err, errMsg) } + verifier, err := credint.NewCredentialVerifier(resolver) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate verifier for the credential service") + } return &Service{ storage: credentialStorage, config: config, + verifier: verifier, keyStore: keyStore, }, nil } @@ -130,7 +137,7 @@ func (s Service) CreateCredential(request CreateCredentialRequest) (*CreateCrede } // store the credential - container := credmodel.CredentialContainer{ + container := credint.CredentialContainer{ ID: cred.ID, Credential: cred, CredentialJWT: credJWT, @@ -168,6 +175,55 @@ func (s Service) signCredentialJWT(issuer string, cred credential.VerifiableCred return &credToken.Token, nil } +type VerifyCredentialRequest struct { + DataIntegrityCredential *credential.VerifiableCredential `json:"credential,omitempty"` + CredentialJWT *string `json:"credentialJwt,omitempty"` +} + +// IsValid checks if the request is valid, meaning there is at least one data integrity OR jwt credential, but not both +func (vcr VerifyCredentialRequest) IsValid() error { + if vcr.DataIntegrityCredential == nil && vcr.CredentialJWT == nil { + return errors.New("either a credential or a credential JWT must be provided") + } + if vcr.DataIntegrityCredential != nil && vcr.CredentialJWT != nil { + return errors.New("only one of credential or credential JWT can be provided") + } + return nil +} + +type VerifyCredentialResponse struct { + Verified bool `json:"verified" json:"verified"` + Reason string `json:"reason,omitempty" json:"reason,omitempty"` +} + +// VerifyCredential does three levels of verification on a credential: +// 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 +// 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(request VerifyCredentialRequest) (*VerifyCredentialResponse, error) { + + logrus.Debugf("verifying credential: %+v", request) + + if err := request.IsValid(); err != nil { + return nil, util.LoggingErrorMsg(err, "invalid verify credential request") + } + + if request.CredentialJWT != nil { + if err := s.verifier.VerifyJWTCredential(*request.CredentialJWT); err != nil { + return &VerifyCredentialResponse{Verified: false, Reason: err.Error()}, nil + } + } else { + if err := s.verifier.VerifyDataIntegrityCredential(*request.DataIntegrityCredential); err != nil { + return &VerifyCredentialResponse{Verified: false, Reason: err.Error()}, nil + } + } + + return &VerifyCredentialResponse{Verified: true}, nil +} + func (s Service) GetCredential(request GetCredentialRequest) (*GetCredentialResponse, error) { logrus.Debugf("getting credential: %s", request.ID) @@ -182,7 +238,7 @@ func (s Service) GetCredential(request GetCredentialRequest) (*GetCredentialResp return nil, util.LoggingNewError(errMsg) } response := GetCredentialResponse{ - credmodel.CredentialContainer{ + credint.CredentialContainer{ ID: gotCred.CredentialID, Credential: gotCred.Credential, CredentialJWT: gotCred.CredentialJWT, @@ -201,9 +257,9 @@ func (s Service) GetCredentialsByIssuer(request GetCredentialByIssuerRequest) (* return nil, util.LoggingErrorMsg(err, errMsg) } - var creds []credmodel.CredentialContainer + var creds []credint.CredentialContainer for _, cred := range gotCreds { - container := credmodel.CredentialContainer{ + container := credint.CredentialContainer{ ID: cred.CredentialID, Credential: cred.Credential, CredentialJWT: cred.CredentialJWT, @@ -225,9 +281,9 @@ func (s Service) GetCredentialsBySubject(request GetCredentialBySubjectRequest) return nil, util.LoggingErrorMsg(err, errMsg) } - var creds []credmodel.CredentialContainer + var creds []credint.CredentialContainer for _, cred := range gotCreds { - container := credmodel.CredentialContainer{ + container := credint.CredentialContainer{ ID: cred.CredentialID, Credential: cred.Credential, CredentialJWT: cred.CredentialJWT, @@ -248,9 +304,9 @@ func (s Service) GetCredentialsBySchema(request GetCredentialBySchemaRequest) (* return nil, util.LoggingErrorMsg(err, errMsg) } - var creds []credmodel.CredentialContainer + var creds []credint.CredentialContainer for _, cred := range gotCreds { - container := credmodel.CredentialContainer{ + container := credint.CredentialContainer{ ID: cred.CredentialID, Credential: cred.Credential, CredentialJWT: cred.CredentialJWT, diff --git a/pkg/service/did/did.go b/pkg/service/did/did.go index 3571c7d31..11daeb9fe 100644 --- a/pkg/service/did/did.go +++ b/pkg/service/did/did.go @@ -3,9 +3,11 @@ package did import ( "fmt" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/pkg/errors" "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/internal/did" "github.com/tbd54566975/ssi-service/internal/util" didstorage "github.com/tbd54566975/ssi-service/pkg/service/did/storage" "github.com/tbd54566975/ssi-service/pkg/service/framework" @@ -13,17 +15,13 @@ import ( "github.com/tbd54566975/ssi-service/pkg/storage" ) -type Method string - -const ( - KeyMethod Method = "key" -) - type Service struct { - // supported DID methods - handlers map[Method]MethodHandler - storage didstorage.Storage config config.DIDServiceConfig + storage didstorage.Storage + resolver *didsdk.Resolver + + // supported DID methods + handlers map[didsdk.Method]MethodHandler // external dependencies keyStore *keystore.Service @@ -33,7 +31,7 @@ type Service struct { type MethodHandler interface { CreateDID(request CreateDIDRequest) (*CreateDIDResponse, error) GetDID(request GetDIDRequest) (*GetDIDResponse, error) - GetDIDs(method Method) (*GetDIDsResponse, error) + GetDIDs(method didsdk.Method) (*GetDIDsResponse, error) } func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) { @@ -41,27 +39,36 @@ func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, key if err != nil { return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service") } + + // instantiate DID resolver + resolver, err := did.BuildResolver(config.ResolutionMethods) + if err != nil { + return nil, errors.Wrap(err, "could not instantiate DID resolver") + } + svc := Service{ storage: didStorage, - handlers: make(map[Method]MethodHandler), + handlers: make(map[didsdk.Method]MethodHandler), keyStore: keyStore, + resolver: resolver, } // instantiate all handlers for DID methods for _, m := range config.Methods { - if err := svc.instantiateHandlerForMethod(Method(m)); err != nil { + if err := svc.instantiateHandlerForMethod(didsdk.Method(m)); err != nil { return nil, errors.Wrap(err, "could not instantiate DID service") } } + return &svc, nil } -func (s *Service) instantiateHandlerForMethod(method Method) error { +func (s *Service) instantiateHandlerForMethod(method didsdk.Method) error { switch method { - case KeyMethod: + case didsdk.KeyMethod: handler, err := newKeyDIDHandler(s.storage, s.keyStore) if err != nil { - err := fmt.Errorf("could not instnatiate did:%s handler", KeyMethod) + err := fmt.Errorf("could not instnatiate did:%s handler", didsdk.KeyMethod) return util.LoggingError(err) } s.handlers[method] = handler @@ -91,8 +98,27 @@ func (s *Service) Config() config.DIDServiceConfig { return s.config } +func (s *Service) GetResolver() *didsdk.Resolver { + return s.resolver +} + +func (s *Service) ResolveDID(request ResolveDIDRequest) (*ResolveDIDResponse, error) { + if request.DID == "" { + return nil, util.LoggingNewError("cannot resolve empty DID") + } + resolved, err := s.resolver.Resolve(request.DID) + if err != nil { + return nil, err + } + return &ResolveDIDResponse{ + ResolutionMetadata: &resolved.DIDResolutionMetadata, + DIDDocument: &resolved.DIDDocument, + DIDDocumentMetadata: &resolved.DIDDocumentMetadata, + }, nil +} + func (s *Service) GetSupportedMethods() GetSupportedMethodsResponse { - var methods []Method + var methods []didsdk.Method for method := range s.handlers { methods = append(methods, method) } @@ -127,7 +153,7 @@ func (s *Service) GetDIDsByMethod(request GetDIDsRequest) (*GetDIDsResponse, err return handler.GetDIDs(method) } -func (s *Service) getHandler(method Method) (MethodHandler, error) { +func (s *Service) getHandler(method didsdk.Method) (MethodHandler, error) { handler, ok := s.handlers[method] if !ok { err := fmt.Errorf("could not get handler for DID method: %s", method) diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go index e56de59b5..15da7fb93 100644 --- a/pkg/service/did/key.go +++ b/pkg/service/did/key.go @@ -87,7 +87,7 @@ func (h *keyDIDHandler) GetDID(request GetDIDRequest) (*GetDIDResponse, error) { return &GetDIDResponse{DID: gotDID.DID}, nil } -func (h *keyDIDHandler) GetDIDs(method Method) (*GetDIDsResponse, error) { +func (h *keyDIDHandler) GetDIDs(method did.Method) (*GetDIDsResponse, error) { logrus.Debugf("getting DIDs for method: %s", method) diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go index ea49ec1b0..1bbbda25c 100644 --- a/pkg/service/did/model.go +++ b/pkg/service/did/model.go @@ -6,12 +6,22 @@ import ( ) type GetSupportedMethodsResponse struct { - Methods []Method `json:"methods"` + Methods []didsdk.Method `json:"methods"` +} + +type ResolveDIDRequest struct { + DID string `json:"did" validate:"required"` +} + +type ResolveDIDResponse struct { + ResolutionMetadata *didsdk.DIDResolutionMetadata `json:"didResolutionMetadata,omitempty"` + DIDDocument *didsdk.DIDDocument `json:"didDocument"` + DIDDocumentMetadata *didsdk.DIDDocumentMetadata `json:"didDocumentMetadata,omitempty"` } // CreateDIDRequest is the JSON-serializable request for creating a DID across DID methods type CreateDIDRequest struct { - Method Method `json:"method" validate:"required"` + Method didsdk.Method `json:"method" validate:"required"` KeyType crypto.KeyType `validate:"required"` } @@ -23,8 +33,8 @@ type CreateDIDResponse struct { } type GetDIDRequest struct { - Method Method `json:"method" validate:"required"` - ID string `json:"id" validate:"required"` + Method didsdk.Method `json:"method" validate:"required"` + ID string `json:"id" validate:"required"` } // GetDIDResponse is the JSON-serializable response for getting a DID @@ -33,7 +43,7 @@ type GetDIDResponse struct { } type GetDIDsRequest struct { - Method Method `json:"method" validate:"required"` + Method didsdk.Method `json:"method" validate:"required"` } // GetDIDsResponse is the JSON-serializable response for getting all DIDs for a given method diff --git a/pkg/service/service.go b/pkg/service/service.go index 18724c164..991d174cb 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -89,7 +89,7 @@ func instantiateServices(config config.ServicesConfig) ([]framework.Service, err return nil, util.LoggingErrorMsg(err, "could not instantiate the schema service") } - credentialService, err := credential.NewCredentialService(config.CredentialConfig, storageProvider, keyStoreService) + credentialService, err := credential.NewCredentialService(config.CredentialConfig, storageProvider, keyStoreService, didService.GetResolver()) if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate the credential service") }