From cbc49af9caec60585274b105b2224914fb4031e4 Mon Sep 17 00:00:00 2001 From: Gabe Date: Tue, 11 Oct 2022 11:52:54 -0700 Subject: [PATCH] Schema Signing & Verification (#123) * light request validation * Scheme delete api * optional schema signing * schema signing testing * verification not working yet * verification api and test * tests * generate new swagger * create one DB per test --- doc/swagger.yaml | 295 ++++++++++++++++++++++---- internal/credential/verification.go | 124 ++--------- internal/keyaccess/verification.go | 108 ++++++++++ pkg/server/framework/request.go | 5 + pkg/server/router/credential.go | 9 +- pkg/server/router/did.go | 9 +- pkg/server/router/manifest_test.go | 2 - pkg/server/router/schema.go | 128 +++++++++-- pkg/server/router/schema_test.go | 96 ++++++++- pkg/server/server.go | 6 +- pkg/server/server_schema_test.go | 165 +++++++++++++- pkg/server/server_test.go | 8 +- pkg/service/credential/credential.go | 2 +- pkg/service/schema/model.go | 25 ++- pkg/service/schema/schema.go | 144 ++++++++++++- pkg/service/schema/storage/bolt.go | 4 +- pkg/service/schema/storage/storage.go | 4 +- pkg/service/service.go | 5 +- 18 files changed, 931 insertions(+), 208 deletions(-) create mode 100644 internal/keyaccess/verification.go diff --git a/doc/swagger.yaml b/doc/swagger.yaml index 259c60737..1fde00129 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -160,28 +160,44 @@ definitions: $ref: '#/definitions/did.VerificationMethod' type: array type: object - did.Service: + did.DIDDocumentMetadata: properties: - accept: - items: - type: string - type: array - id: + canonicalId: type: string - routingKeys: - items: - type: string - type: array - serviceEndpoint: - description: |- - A string, map, or set composed of one or more strings and/or maps - All string values must be valid URIs - type: + created: + type: string + deactivated: + type: boolean + equivalentId: + type: string + nextUpdate: + type: string + nextVersionId: + type: string + updated: + type: string + versionId: type: string - required: - - id - - serviceEndpoint - - type + type: object + did.DIDResolutionMetadata: + properties: + contentType: + type: string + error: + $ref: '#/definitions/did.ResolutionError' + type: object + did.ResolutionError: + properties: + code: + type: string + invalidDid: + type: boolean + notFound: + type: boolean + representationNotSupported: + type: boolean + type: object + did.Service: type: object did.VerificationMethod: properties: @@ -519,6 +535,10 @@ definitions: type: string schema: $ref: '#/definitions/schema.JSONSchema' + sign: + description: Sign represents whether the schema should be signed by the author. + Default is false. + type: boolean required: - author - name @@ -530,6 +550,8 @@ definitions: type: string schema: $ref: '#/definitions/schema.VCJSONSchema' + schemaJwt: + type: string type: object github.com_tbd54566975_ssi-service_pkg_server_router.GetApplicationResponse: properties: @@ -568,7 +590,7 @@ definitions: type: object github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDMethodsResponse: properties: - didMethods: + methods: items: type: string type: array @@ -628,12 +650,14 @@ definitions: properties: schema: $ref: '#/definitions/schema.VCJSONSchema' + schemaJwt: + type: string type: object github.com_tbd54566975_ssi-service_pkg_server_router.GetSchemasResponse: properties: schemas: items: - $ref: '#/definitions/schema.VCJSONSchema' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetSchemaResponse' type: array type: object github.com_tbd54566975_ssi-service_pkg_server_router.PublishManifestRequest: @@ -653,6 +677,15 @@ definitions: - dwnResponse - manifest type: object + github.com_tbd54566975_ssi-service_pkg_server_router.ResolveDIDResponse: + properties: + didDocument: + $ref: '#/definitions/did.DIDDocument' + didDocumentMetadata: + $ref: '#/definitions/did.DIDDocumentMetadata' + didResolutionMetadata: + $ref: '#/definitions/did.DIDResolutionMetadata' + type: object github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest: properties: base58PrivateKey: @@ -691,6 +724,32 @@ definitions: items: {} type: array type: object + github.com_tbd54566975_ssi-service_pkg_server_router.VerifyCredentialRequest: + properties: + credential: + $ref: '#/definitions/credential.VerifiableCredential' + credentialJwt: + type: string + type: object + github.com_tbd54566975_ssi-service_pkg_server_router.VerifyCredentialResponse: + properties: + reason: + type: string + verified: + type: boolean + type: object + github.com_tbd54566975_ssi-service_pkg_server_router.VerifySchemaRequest: + properties: + schemaJwt: + type: string + type: object + github.com_tbd54566975_ssi-service_pkg_server_router.VerifySchemaResponse: + properties: + reason: + type: string + verified: + type: boolean + type: object manifest.CredentialApplication: properties: format: @@ -867,6 +926,10 @@ definitions: type: string schema: $ref: '#/definitions/schema.JSONSchema' + sign: + description: Sign represents whether the schema should be signed by the author. + Default is false. + type: boolean required: - author - name @@ -878,6 +941,8 @@ definitions: type: string schema: $ref: '#/definitions/schema.VCJSONSchema' + schemaJwt: + type: string type: object pkg_server_router.GetApplicationResponse: properties: @@ -916,7 +981,7 @@ definitions: type: object pkg_server_router.GetDIDMethodsResponse: properties: - didMethods: + methods: items: type: string type: array @@ -976,12 +1041,14 @@ definitions: properties: schema: $ref: '#/definitions/schema.VCJSONSchema' + schemaJwt: + type: string type: object pkg_server_router.GetSchemasResponse: properties: schemas: items: - $ref: '#/definitions/schema.VCJSONSchema' + $ref: '#/definitions/pkg_server_router.GetSchemaResponse' type: array type: object pkg_server_router.PublishManifestRequest: @@ -1001,6 +1068,15 @@ definitions: - dwnResponse - manifest type: object + pkg_server_router.ResolveDIDResponse: + properties: + didDocument: + $ref: '#/definitions/did.DIDDocument' + didDocumentMetadata: + $ref: '#/definitions/did.DIDDocumentMetadata' + didResolutionMetadata: + $ref: '#/definitions/did.DIDResolutionMetadata' + type: object pkg_server_router.StoreKeyRequest: properties: base58PrivateKey: @@ -1039,6 +1115,32 @@ definitions: items: {} type: array type: object + pkg_server_router.VerifyCredentialRequest: + properties: + credential: + $ref: '#/definitions/credential.VerifiableCredential' + credentialJwt: + type: string + type: object + pkg_server_router.VerifyCredentialResponse: + properties: + reason: + type: string + verified: + type: boolean + type: object + pkg_server_router.VerifySchemaRequest: + properties: + schemaJwt: + type: string + type: object + pkg_server_router.VerifySchemaResponse: + properties: + reason: + type: string + verified: + type: boolean + type: object rendering.ColorResource: properties: color: @@ -1174,7 +1276,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetHealthCheckResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetHealthCheckResponse' summary: Health Check tags: - HealthCheck @@ -1220,7 +1322,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetCredentialsResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetCredentialsResponse' "400": description: Bad request schema: @@ -1235,21 +1337,21 @@ paths: put: consumes: - application/json - description: Create credential + description: Create a credential parameters: - description: request body in: body name: request required: true schema: - $ref: '#/definitions/pkg_server_router.CreateCredentialRequest' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateCredentialRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/pkg_server_router.CreateCredentialResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateCredentialResponse' "400": description: Bad request schema: @@ -1306,7 +1408,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetCredentialResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetCredentialResponse' "400": description: Bad request schema: @@ -1314,6 +1416,32 @@ paths: summary: Get Credential tags: - CredentialAPI + /v1/credentials/verification: + put: + consumes: + - application/json + description: Verify a given credential by its id + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.VerifyCredentialRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.VerifyCredentialResponse' + "400": + description: Bad request + schema: + type: string + summary: Verify Credential + tags: + - CredentialAPI /v1/dids: get: consumes: @@ -1424,6 +1552,31 @@ paths: summary: Get DID tags: - DecentralizedIdentityAPI + /v1/dids/resolver/{id}: + get: + consumes: + - application/json + description: Resolve a DID that may not be stored in this service + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_server_router.ResolveDIDResponse' + "400": + description: Bad request + schema: + type: string + summary: Resolve a DID + tags: + - DecentralizedIdentityAPI /v1/dwn/manifest: put: consumes: @@ -1435,14 +1588,14 @@ paths: name: request required: true schema: - $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.PublishManifestRequest' + $ref: '#/definitions/pkg_server_router.PublishManifestRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.PublishManifestResponse' + $ref: '#/definitions/pkg_server_router.PublishManifestResponse' "400": description: Bad request schema: @@ -1465,7 +1618,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/pkg_server_router.StoreKeyRequest' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest' produces: - application/json responses: @@ -1499,7 +1652,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetKeyDetailsResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetKeyDetailsResponse' "400": description: Bad request schema: @@ -1532,7 +1685,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetManifestsResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetManifestsResponse' "400": description: Bad request schema: @@ -1554,14 +1707,14 @@ paths: name: request required: true schema: - $ref: '#/definitions/pkg_server_router.CreateManifestRequest' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateManifestRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/pkg_server_router.CreateManifestResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateManifestResponse' "400": description: Bad request schema: @@ -1618,7 +1771,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetManifestResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetManifestResponse' "400": description: Bad request schema: @@ -1638,7 +1791,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetApplicationsResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetApplicationsResponse' "400": description: Bad request schema: @@ -1660,14 +1813,14 @@ paths: name: request required: true schema: - $ref: '#/definitions/pkg_server_router.SubmitApplicationRequest' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.SubmitApplicationRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/pkg_server_router.SubmitApplicationResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.SubmitApplicationResponse' "400": description: Bad request schema: @@ -1724,7 +1877,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetApplicationResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetApplicationResponse' "400": description: Bad request schema: @@ -1744,7 +1897,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetResponsesResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetResponsesResponse' "400": description: Bad request schema: @@ -1801,7 +1954,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetResponseResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetResponseResponse' "400": description: Bad request schema: @@ -1858,10 +2011,38 @@ paths: tags: - SchemaAPI /v1/schemas/{id}: + delete: + consumes: + - application/json + description: Delete a schema by its ID + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + "400": + description: Bad request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Delete Schema + tags: + - SchemaAPI get: consumes: - application/json - description: Get schema by ID + description: Get a schema by its ID parameters: - description: ID in: path @@ -1882,4 +2063,30 @@ paths: summary: Get Schema tags: - SchemaAPI + /v1/schemas/verification: + put: + consumes: + - application/json + description: Verify a given schema by its id + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/pkg_server_router.VerifySchemaRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/pkg_server_router.VerifySchemaResponse' + "400": + description: Bad request + schema: + type: string + summary: Verify Schema + tags: + - SchemaAPI swagger: "2.0" diff --git a/internal/credential/verification.go b/internal/credential/verification.go index ddc4813ed..baef87ca2 100644 --- a/internal/credential/verification.go +++ b/internal/credential/verification.go @@ -1,21 +1,11 @@ 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" @@ -53,7 +43,12 @@ func (v CredentialVerifier) VerifyJWTCredential(token string) error { if err != nil { return util.LoggingErrorMsg(err, "could not parse credential from JWT") } - kid, pubKey, err := v.getVerificationInformation(cred.Issuer.(string), "") + issuerDID := cred.Issuer.(string) + resolved, err := v.resolver.Resolve(issuerDID) + if err != nil { + return errors.Wrapf(err, "failed to resolve did: %s", issuerDID) + } + kid, pubKey, err := keyaccess.GetVerificationInformation(resolved.DIDDocument, "") if err != nil { return util.LoggingErrorMsg(err, "could not get verification from credential") } @@ -74,115 +69,24 @@ func (v CredentialVerifier) VerifyJWTCredential(token string) error { // 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), "") + issuerDID := credential.Issuer.(string) + resolved, err := v.resolver.Resolve(issuerDID) if err != nil { - return util.LoggingErrorMsg(err, "could not get verification from credential") + return errors.Wrapf(err, "failed to resolve did: %s", issuerDID) + } + kid, pubKey, err := keyaccess.GetVerificationInformation(resolved.DIDDocument, "") + if err != nil { + return util.LoggingErrorMsg(err, "could not get verification information 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") + return util.LoggingErrorMsg(err, "could not verify the 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/keyaccess/verification.go b/internal/keyaccess/verification.go new file mode 100644 index 000000000..dd38e5fd9 --- /dev/null +++ b/internal/keyaccess/verification.go @@ -0,0 +1,108 @@ +package keyaccess + +import ( + "crypto" + "fmt" + + "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" +) + +// GetVerificationInformation resolves a DID and provides a kid and public key needed for data verification +// it is possible that a DID has multiple verification methods, in which case a kid must be provided, otherwise +// resolution will fail. +func GetVerificationInformation(did didsdk.DIDDocument, maybeKID string) (kid string, pubKey crypto.PublicKey, err error) { + if did.IsEmpty() { + return "", nil, errors.Errorf("did doc: %+v is empty", did) + } + verificationMethods := did.VerificationMethod + if len(verificationMethods) == 0 { + return "", nil, errors.Errorf("did doc: %s has no verification methods", did.ID) + } + + // 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.ID) + } + for _, method := range verificationMethods { + if method.ID == kid { + kid = did.ID + 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.ID + 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/pkg/server/framework/request.go b/pkg/server/framework/request.go index 282148e60..9c1273f18 100644 --- a/pkg/server/framework/request.go +++ b/pkg/server/framework/request.go @@ -6,6 +6,7 @@ import ( "reflect" "strings" + "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" "github.com/dimfeld/httptreemux/v5" @@ -95,3 +96,7 @@ func Decode(r *http.Request, val interface{}) error { return nil } + +func ValidateRequest(request interface{}) error { + return util.IsValidStruct(request) +} diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go index 67e7473f0..fe319a2c0 100644 --- a/pkg/server/router/credential.go +++ b/pkg/server/router/credential.go @@ -80,8 +80,15 @@ type CreateCredentialResponse struct { // @Router /v1/credentials [put] func (cr CredentialRouter) CreateCredential(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var request CreateCredentialRequest + invalidCreateCredentialRequest := "invalid create credential request" if err := framework.Decode(r, &request); err != nil { - errMsg := "invalid create credential request" + errMsg := invalidCreateCredentialRequest + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + if err := framework.ValidateRequest(request); err != nil { + errMsg := invalidCreateCredentialRequest logrus.WithError(err).Error(errMsg) return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) } diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go index a59061d00..dafbf0931 100644 --- a/pkg/server/router/did.go +++ b/pkg/server/router/did.go @@ -87,8 +87,15 @@ func (dr DIDRouter) CreateDIDByMethod(ctx context.Context, w http.ResponseWriter } var request CreateDIDByMethodRequest + invalidCreateDIDRequest := "invalid create DID request" if err := framework.Decode(r, &request); err != nil { - errMsg := "invalid create DID request" + errMsg := invalidCreateDIDRequest + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + if err := framework.ValidateRequest(request); err != nil { + errMsg := invalidCreateDIDRequest logrus.WithError(err).Error(errMsg) return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) } diff --git a/pkg/server/router/manifest_test.go b/pkg/server/router/manifest_test.go index 6002799aa..05a53d8b4 100644 --- a/pkg/server/router/manifest_test.go +++ b/pkg/server/router/manifest_test.go @@ -80,13 +80,11 @@ func TestManifestRouter(t *testing.T) { createApplicationRequest := getValidApplicationRequest(applicantDID.DID.ID, createdManifest.Manifest.ID, createManifestRequest.Manifest.PresentationDefinition.InputDescriptors[0].ID) createdApplicationResponse, err := manifestService.ProcessApplicationSubmission(createApplicationRequest) - assert.NoError(tt, err) assert.NotEmpty(tt, createdManifest) assert.NotEmpty(tt, createdApplicationResponse.Response.ID) assert.Equal(tt, len(createManifestRequest.Manifest.OutputDescriptors), len(createdApplicationResponse.Credential)) }) - } func getValidManifestRequest(issuerDID string) manifest.CreateManifestRequest { diff --git a/pkg/server/router/schema.go b/pkg/server/router/schema.go index 29020bdbf..0f84accd6 100644 --- a/pkg/server/router/schema.go +++ b/pkg/server/router/schema.go @@ -8,6 +8,7 @@ import ( schemalib "github.com/TBD54566975/ssi-sdk/credential/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/pkg/server/framework" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/schema" @@ -34,11 +35,14 @@ type CreateSchemaRequest struct { Author string `json:"author" validate:"required"` Name string `json:"name" validate:"required"` Schema schemalib.JSONSchema `json:"schema" validate:"required"` + // Sign represents whether the schema should be signed by the author. Default is false. + Sign bool `json:"sign"` } type CreateSchemaResponse struct { - ID string `json:"id"` - Schema schemalib.VCJSONSchema `json:"schema"` + ID string `json:"id"` + Schema schemalib.VCJSONSchema `json:"schema"` + SchemaJWT *string `json:"schemaJwt,omitempty"` } // CreateSchema godoc @@ -54,13 +58,20 @@ type CreateSchemaResponse struct { // @Router /v1/schemas [put] func (sr SchemaRouter) CreateSchema(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var request CreateSchemaRequest + invalidCreateSchemaRequest := "invalid create schema request" if err := framework.Decode(r, &request); err != nil { - errMsg := "invalid create schema request" + errMsg := invalidCreateSchemaRequest + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + if err := framework.ValidateRequest(request); err != nil { + errMsg := invalidCreateSchemaRequest logrus.WithError(err).Error(errMsg) return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) } - req := schema.CreateSchemaRequest{Author: request.Author, Name: request.Name, Schema: request.Schema} + req := schema.CreateSchemaRequest{Author: request.Author, Name: request.Name, Schema: request.Schema, Sign: request.Sign} createSchemaResponse, err := sr.service.CreateSchema(req) if err != nil { errMsg := fmt.Sprintf("could not create schema with authoring DID: %s", request.Author) @@ -68,12 +79,42 @@ func (sr SchemaRouter) CreateSchema(ctx context.Context, w http.ResponseWriter, return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError) } - resp := CreateSchemaResponse{ID: createSchemaResponse.ID, Schema: createSchemaResponse.Schema} + resp := CreateSchemaResponse{ID: createSchemaResponse.ID, Schema: createSchemaResponse.Schema, SchemaJWT: createSchemaResponse.SchemaJWT} return framework.Respond(ctx, w, resp, http.StatusCreated) } +// GetSchema godoc +// @Summary Get Schema +// @Description Get a schema by its ID +// @Tags SchemaAPI +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} GetSchemaResponse +// @Failure 400 {string} string "Bad request" +// @Router /v1/schemas/{id} [get] +func (sr SchemaRouter) GetSchema(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + id := framework.GetParam(ctx, IDParam) + if id == nil { + errMsg := "cannot get schema without ID parameter" + logrus.Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + // TODO(gabe) differentiate between internal errors and not found schemas + gotSchema, err := sr.service.GetSchema(schema.GetSchemaRequest{ID: *id}) + if err != nil { + errMsg := fmt.Sprintf("could not get schema with id: %s", *id) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + resp := GetSchemaResponse{Schema: gotSchema.Schema, SchemaJWT: gotSchema.SchemaJWT} + return framework.Respond(ctx, w, resp, http.StatusOK) +} + type GetSchemasResponse struct { - Schemas []schemalib.VCJSONSchema `json:"schemas,omitempty"` + Schemas []GetSchemaResponse `json:"schemas,omitempty"` } // GetSchemas godoc @@ -92,40 +133,83 @@ func (sr SchemaRouter) GetSchemas(ctx context.Context, w http.ResponseWriter, r logrus.WithError(err).Error(errMsg) return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError) } - resp := GetSchemasResponse{Schemas: gotSchemas.Schemas} + + var schemas []GetSchemaResponse + for _, s := range gotSchemas.Schemas { + schemas = append(schemas, GetSchemaResponse{Schema: s.Schema}) + } + + resp := GetSchemasResponse{Schemas: schemas} return framework.Respond(ctx, w, resp, http.StatusOK) } type GetSchemaResponse struct { - Schema schemalib.VCJSONSchema `json:"schema,omitempty"` + Schema schemalib.VCJSONSchema `json:"schema,omitempty"` + SchemaJWT *string `json:"schemaJwt,omitempty"` } -// GetSchema godoc -// @Summary Get Schema -// @Description Get schema by ID +type VerifySchemaRequest struct { + SchemaJWT string `json:"schemaJwt"` +} + +type VerifySchemaResponse struct { + Verified bool `json:"verified" json:"verified"` + Reason string `json:"reason,omitempty" json:"reason,omitempty"` +} + +// VerifySchema godoc +// @Summary Verify Schema +// @Description Verify a given schema by its id +// @Tags SchemaAPI +// @Accept json +// @Produce json +// @Param request body VerifySchemaRequest true "request body" +// @Success 200 {object} VerifySchemaResponse +// @Failure 400 {string} string "Bad request" +// @Router /v1/schemas/verification [put] +func (sr SchemaRouter) VerifySchema(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var request VerifySchemaRequest + if err := framework.Decode(r, &request); err != nil { + errMsg := "invalid verify schema request" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + } + + verificationResult, err := sr.service.VerifySchema(schema.VerifySchemaRequest{SchemaJWT: request.SchemaJWT}) + if err != nil { + errMsg := "could not verify schema" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError) + } + + resp := VerifySchemaResponse{Verified: verificationResult.Verified, Reason: verificationResult.Reason} + return framework.Respond(ctx, w, resp, http.StatusOK) +} + +// DeleteSchema godoc +// @Summary Delete Schema +// @Description Delete a schema by its ID // @Tags SchemaAPI // @Accept json // @Produce json // @Param id path string true "ID" -// @Success 200 {object} GetSchemaResponse +// @Success 200 {string} string "OK" // @Failure 400 {string} string "Bad request" -// @Router /v1/schemas/{id} [get] -func (sr SchemaRouter) GetSchema(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +// @Failure 500 {string} string "Internal server error" +// @Router /v1/schemas/{id} [delete] +func (sr SchemaRouter) DeleteSchema(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { id := framework.GetParam(ctx, IDParam) if id == nil { - errMsg := "cannot get schema without ID parameter" + errMsg := "cannot delete a schema without an ID parameter" logrus.Error(errMsg) return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) } - // TODO(gabe) differentiate between internal errors and not found schemas - gotSchema, err := sr.service.GetSchema(schema.GetSchemaRequest{ID: *id}) - if err != nil { - errMsg := fmt.Sprintf("could not get schema with id: %s", *id) + if err := sr.service.DeleteSchema(schema.DeleteSchemaRequest{ID: *id}); err != nil { + errMsg := fmt.Sprintf("could not delete schema with id: %s", *id) logrus.WithError(err).Error(errMsg) - return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest) + return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError) } - resp := GetSchemaResponse{Schema: gotSchema.Schema} - return framework.Respond(ctx, w, resp, http.StatusOK) + return framework.Respond(ctx, w, nil, http.StatusOK) } diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go index 4a199492e..f7c3fb45c 100644 --- a/pkg/server/router/schema_test.go +++ b/pkg/server/router/schema_test.go @@ -4,9 +4,13 @@ import ( "os" "testing" + "github.com/TBD54566975/ssi-sdk/crypto" + didsdk "github.com/TBD54566975/ssi-sdk/did" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "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/schema" "github.com/tbd54566975/ssi-service/pkg/storage" @@ -35,12 +39,13 @@ func TestSchemaRouter(t *testing.T) { t.Run("Schema Service Test", func(tt *testing.T) { bolt, err := storage.NewBoltDB() - assert.NoError(tt, err) - assert.NotEmpty(tt, bolt) + require.NoError(tt, err) + require.NotEmpty(tt, bolt) serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} keyStoreService := testKeyStoreService(tt, bolt) - schemaService, err := schema.NewSchemaService(serviceConfig, bolt, keyStoreService) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService, err := schema.NewSchemaService(serviceConfig, bolt, keyStoreService, didService.GetResolver()) assert.NoError(tt, err) assert.NotEmpty(tt, schemaService) @@ -105,5 +110,90 @@ func TestSchemaRouter(t *testing.T) { // make sure their IDs are different assert.True(tt, gotSchemas.Schemas[0].ID != gotSchemas.Schemas[1].ID) + + // delete the first schema + err = schemaService.DeleteSchema(schema.DeleteSchemaRequest{ID: gotSchemas.Schemas[0].ID}) + assert.NoError(tt, err) + + // get all schemas, expect one + gotSchemas, err = schemaService.GetSchemas() + assert.NoError(tt, err) + assert.NotEmpty(tt, gotSchemas.Schemas) + assert.Len(tt, gotSchemas.Schemas, 1) + }) +} + +func TestSchemaSigning(t *testing.T) { + // remove the db file after the test + t.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + + t.Run("Unsigned Schema Test", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + require.NoError(tt, err) + require.NotEmpty(tt, bolt) + + serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}} + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService, err := schema.NewSchemaService(serviceConfig, bolt, keyStoreService, didService.GetResolver()) + assert.NoError(tt, err) + assert.NotEmpty(tt, schemaService) + + // check type and status + assert.Equal(tt, framework.Schema, schemaService.Type()) + assert.Equal(tt, framework.StatusReady, schemaService.Status().Status) + + // create a schema and don't sign it + simpleSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + }, + }, + "required": []interface{}{"foo"}, + "additionalProperties": false, + } + createdSchema, err := schemaService.CreateSchema(schema.CreateSchemaRequest{Author: "me", Name: "simple schema", Schema: simpleSchema}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + assert.NotEmpty(tt, createdSchema.ID) + assert.Empty(tt, createdSchema.SchemaJWT) + assert.Equal(tt, "me", createdSchema.Schema.Author) + assert.Equal(tt, "simple schema", createdSchema.Schema.Name) + + // missing DID + createdSchema, err = schemaService.CreateSchema(schema.CreateSchemaRequest{Author: "me", Name: "simple schema", Schema: simpleSchema, Sign: true}) + assert.Error(tt, err) + assert.Empty(tt, createdSchema) + assert.Contains(tt, err.Error(), "could not get key for signing schema for author") + + // create an author DID + authorDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, authorDID) + + createdSchema, err = schemaService.CreateSchema(schema.CreateSchemaRequest{Author: authorDID.DID.ID, Name: "simple schema", Schema: simpleSchema, Sign: true}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createdSchema) + assert.NotEmpty(tt, createdSchema.SchemaJWT) + + // verify the schema + verifiedSchema, err := schemaService.VerifySchema(schema.VerifySchemaRequest{SchemaJWT: *createdSchema.SchemaJWT}) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifiedSchema) + assert.True(tt, verifiedSchema.Verified) + + // verify a bad schema + verifiedSchema, err = schemaService.VerifySchema(schema.VerifySchemaRequest{SchemaJWT: "bad"}) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifiedSchema) + assert.False(tt, verifiedSchema.Verified) + assert.Contains(tt, verifiedSchema.Reason, "could not verify schema") }) } diff --git a/pkg/server/server.go b/pkg/server/server.go index 60267f29f..222d0a3ab 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -31,6 +31,7 @@ const ( ResponsesPrefix = "/responses" KeyStorePrefix = "/keys" DWNPrefix = "/dwn" + VerificationPath = "/verification" ) // SSIServer exposes all dependencies needed to run a http server and all its services @@ -130,8 +131,10 @@ func (s *SSIServer) SchemaAPI(service svcframework.Service) (err error) { handlerPath := V1Prefix + SchemasPrefix s.Handle(http.MethodPut, handlerPath, schemaRouter.CreateSchema) - s.Handle(http.MethodGet, handlerPath, schemaRouter.GetSchemas) s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), schemaRouter.GetSchema) + s.Handle(http.MethodGet, handlerPath, schemaRouter.GetSchemas) + s.Handle(http.MethodPut, path.Join(handlerPath, VerificationPath), schemaRouter.VerifySchema) + s.Handle(http.MethodDelete, path.Join(handlerPath, "/:id"), schemaRouter.DeleteSchema) return } @@ -146,6 +149,7 @@ func (s *SSIServer) CredentialAPI(service svcframework.Service) (err error) { s.Handle(http.MethodPut, handlerPath, credRouter.CreateCredential) s.Handle(http.MethodGet, handlerPath, credRouter.GetCredentials) s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), credRouter.GetCredential) + s.Handle(http.MethodPut, path.Join(handlerPath, VerificationPath), credRouter.VerifyCredential) s.Handle(http.MethodDelete, path.Join(handlerPath, "/:id"), credRouter.DeleteCredential) return } diff --git a/pkg/server/server_schema_test.go b/pkg/server/server_schema_test.go index 60c4adb65..1f0e3c607 100644 --- a/pkg/server/server_schema_test.go +++ b/pkg/server/server_schema_test.go @@ -7,11 +7,14 @@ import ( "os" "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" ) @@ -27,7 +30,8 @@ func TestSchemaAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - schemaService := testSchemaRouter(tt, bolt, keyStoreService) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) simpleSchema := map[string]interface{}{ "type": "object", @@ -65,7 +69,7 @@ func TestSchemaAPI(t *testing.T) { assert.EqualValues(tt, schemaRequest.Schema, resp.Schema.Schema) }) - t.Run("Test Get Schemas", func(tt *testing.T) { + t.Run("Test Sign & Verify Schema", func(tt *testing.T) { bolt, err := storage.NewBoltDB() require.NoError(tt, err) @@ -76,7 +80,92 @@ func TestSchemaAPI(t *testing.T) { }) keyStoreService := testKeyStoreService(tt, bolt) - schemaService := testSchemaRouter(tt, bolt, keyStoreService) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) + + simpleSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + }, + }, + "required": []interface{}{"foo"}, + "additionalProperties": false, + } + + w := httptest.NewRecorder() + + // sign request with unknown DID + schemaRequest := router.CreateSchemaRequest{Author: "did:test", Name: "test schema", Schema: simpleSchema, Sign: true} + schemaRequestValue := newRequestValue(tt, schemaRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + err = schemaService.CreateSchema(newRequestContext(), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not create schema with authoring DID: did:test") + + // create a DID + issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + // sign with known DID + schemaRequest = router.CreateSchemaRequest{Author: issuerDID.DID.ID, Name: "test schema", Schema: simpleSchema, Sign: true} + schemaRequestValue = newRequestValue(tt, schemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + err = schemaService.CreateSchema(newRequestContext(), w, req) + + var resp router.CreateSchemaResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.SchemaJWT) + assert.NotEmpty(tt, resp.ID) + assert.EqualValues(tt, schemaRequest.Schema, resp.Schema.Schema) + + // verify schema + verifySchemaRequest := router.VerifySchemaRequest{SchemaJWT: *resp.SchemaJWT} + verifySchemaRequestValue := newRequestValue(tt, verifySchemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas/verification", verifySchemaRequestValue) + err = schemaService.VerifySchema(newRequestContext(), w, req) + assert.NoError(tt, err) + + var verifyResp router.VerifySchemaResponse + err = json.NewDecoder(w.Body).Decode(&verifyResp) + assert.NoError(tt, err) + assert.NotEmpty(tt, verifyResp) + assert.True(tt, verifyResp.Verified) + + // verify a bad schema + verifySchemaRequest = router.VerifySchemaRequest{SchemaJWT: "bad"} + verifySchemaRequestValue = newRequestValue(tt, verifySchemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas/verification", verifySchemaRequestValue) + err = schemaService.VerifySchema(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 verify schema") + }) + + t.Run("Test Get Schema and Get Schemas", 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) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) // get schema that doesn't exist w := httptest.NewRecorder() @@ -160,4 +249,74 @@ func TestSchemaAPI(t *testing.T) { assert.NoError(tt, err) assert.Len(tt, getSchemasResp.Schemas, 1) }) + + t.Run("Test Delete Schema", 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) + schemaService := testSchemaRouter(tt, bolt, keyStoreService, didService) + + w := httptest.NewRecorder() + + // delete a schema that doesn't exist + req := httptest.NewRequest(http.MethodDelete, "https://ssi-service.com/v1/schemas/bad", nil) + err = schemaService.DeleteSchema(newRequestContextWithParams(map[string]string{"id": "bad"}), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not delete schema with id: bad") + + // create a schema + simpleSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + }, + }, + "required": []interface{}{"foo"}, + "additionalProperties": false, + } + + schemaRequest := router.CreateSchemaRequest{Author: "did:test", Name: "test schema", Schema: simpleSchema} + schemaRequestValue := newRequestValue(tt, schemaRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) + err = schemaService.CreateSchema(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateSchemaResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.ID) + assert.EqualValues(tt, schemaRequest.Schema, resp.Schema.Schema) + + w.Flush() + + // get schema by id + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) + err = schemaService.GetSchema(newRequestContextWithParams(map[string]string{"id": resp.ID}), w, req) + assert.NoError(tt, err) + + w.Flush() + + // delete it + req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) + err = schemaService.DeleteSchema(newRequestContextWithParams(map[string]string{"id": resp.ID}), w, req) + assert.NoError(tt, err) + + w.Flush() + + // get it back + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/schemas/%s", resp.ID), nil) + err = schemaService.GetSchema(newRequestContextWithParams(map[string]string{"id": resp.ID}), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "schema not found") + }) } diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 95a8a21c0..5d49ce37d 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -237,15 +237,15 @@ func testDIDRouter(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Servic return didRouter } -func testSchemaService(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Service) *schema.Service { - schemaService, err := schema.NewSchemaService(config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "test-schema"}}, bolt, keyStore) +func testSchemaService(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Service, did *did.Service) *schema.Service { + schemaService, err := schema.NewSchemaService(config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "test-schema"}}, bolt, keyStore, did.GetResolver()) require.NoError(t, err) require.NotEmpty(t, schemaService) return schemaService } -func testSchemaRouter(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Service) *router.SchemaRouter { - schemaService := testSchemaService(t, bolt, keyStore) +func testSchemaRouter(t *testing.T, bolt *storage.BoltDB, keyStore *keystore.Service, did *did.Service) *router.SchemaRouter { + schemaService := testSchemaService(t, bolt, keyStore, did) // create router for service schemaRouter, err := router.NewSchemaRouter(schemaService) diff --git a/pkg/service/credential/credential.go b/pkg/service/credential/credential.go index 11aab8cfe..297c5c8f6 100644 --- a/pkg/service/credential/credential.go +++ b/pkg/service/credential/credential.go @@ -98,7 +98,7 @@ func (s Service) CreateCredential(request CreateCredentialRequest) (*CreateCrede } } - // if a schema value exists, set it + // if a schema value exists, verify we can access it, validate the data against it, then set it if request.JSONSchema != "" { schema := credential.CredentialSchema{ ID: request.JSONSchema, diff --git a/pkg/service/schema/model.go b/pkg/service/schema/model.go index 9037e7f7e..7acd6ed72 100644 --- a/pkg/service/schema/model.go +++ b/pkg/service/schema/model.go @@ -1,6 +1,9 @@ package schema -import "github.com/TBD54566975/ssi-sdk/credential/schema" +import ( + "github.com/TBD54566975/ssi-sdk/credential/schema" + "github.com/TBD54566975/ssi-sdk/util" +) const ( Version1 string = "1.0.0" @@ -10,15 +13,21 @@ type CreateSchemaRequest struct { Author string `json:"author" validate:"required"` Name string `json:"name" validate:"required"` Schema schema.JSONSchema `json:"schema" validate:"required"` + Sign bool `json:"signed"` +} + +func (csr CreateSchemaRequest) IsValid() bool { + return util.IsValidStruct(csr) == nil } type CreateSchemaResponse struct { - ID string `json:"id"` - Schema schema.VCJSONSchema `json:"schema"` + ID string `json:"id"` + Schema schema.VCJSONSchema `json:"schema"` + SchemaJWT *string `json:"schemaJwt,omitempty"` } type GetSchemasResponse struct { - Schemas []schema.VCJSONSchema `json:"schemas,omitempty"` + Schemas []GetSchemaResponse `json:"schemas,omitempty"` } type GetSchemaRequest struct { @@ -26,5 +35,11 @@ type GetSchemaRequest struct { } type GetSchemaResponse struct { - Schema schema.VCJSONSchema `json:"schema"` + ID string `json:"id"` + Schema schema.VCJSONSchema `json:"schema"` + SchemaJWT *string `json:"schemaJwt,omitempty"` +} + +type DeleteSchemaRequest struct { + ID string `json:"id" validate:"required"` } diff --git a/pkg/service/schema/schema.go b/pkg/service/schema/schema.go index a67507751..d6946f9de 100644 --- a/pkg/service/schema/schema.go +++ b/pkg/service/schema/schema.go @@ -5,12 +5,17 @@ import ( "time" "github.com/TBD54566975/ssi-sdk/credential/schema" + didsdk "github.com/TBD54566975/ssi-sdk/did" schemalib "github.com/TBD54566975/ssi-sdk/schema" + sdkutil "github.com/TBD54566975/ssi-sdk/util" "github.com/goccy/go-json" "github.com/google/uuid" + "github.com/lestrrat-go/jwx/jwt" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "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/service/framework" "github.com/tbd54566975/ssi-service/pkg/service/keystore" @@ -24,6 +29,7 @@ type Service struct { // external dependencies keyStore *keystore.Service + resolver *didsdk.Resolver } func (s Service) Type() framework.Type { @@ -44,7 +50,7 @@ func (s Service) Config() config.SchemaServiceConfig { return s.config } -func NewSchemaService(config config.SchemaServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) { +func NewSchemaService(config config.SchemaServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, resolver *didsdk.Resolver) (*Service, error) { schemaStorage, err := schemastorage.NewSchemaStorage(s) if err != nil { errMsg := "could not instantiate storage for the schema service" @@ -54,13 +60,22 @@ func NewSchemaService(config config.SchemaServiceConfig, s storage.ServiceStorag storage: schemaStorage, config: config, keyStore: keyStore, + resolver: resolver, }, nil } // CreateSchema houses the main service logic for schema creation. It validates the input, and // produces a schema value that conforms with the VC JSON JSONSchema specification. -// TODO(gabe) support proof generation on schemas, versioning, and more +// TODO(gabe) support data integrity proof generation on schemas, versioning, and more func (s Service) CreateSchema(request CreateSchemaRequest) (*CreateSchemaResponse, error) { + + logrus.Debugf("creating schema: %+v", request) + + if !request.IsValid() { + errMsg := fmt.Sprintf("invalid create schema request: %+v", request) + return nil, util.LoggingNewError(errMsg) + } + schemaBytes, err := json.Marshal(request.Schema) if err != nil { return nil, errors.Wrap(err, "could not marshal schema in request") @@ -69,6 +84,7 @@ func (s Service) CreateSchema(request CreateSchemaRequest) (*CreateSchemaRespons return nil, util.LoggingErrorMsg(err, "provided value is not a valid JSON schema") } + // create schema schemaID := uuid.NewString() schemaValue := schema.VCJSONSchema{ Type: schema.VCJSONSchemaType, @@ -80,22 +96,121 @@ func (s Service) CreateSchema(request CreateSchemaRequest) (*CreateSchemaRespons Schema: request.Schema, } - storedSchema := schemastorage.StoredSchema{Schema: schemaValue} + storedSchema := schemastorage.StoredSchema{ID: schemaID, Schema: schemaValue} + + // sign the schema + if request.Sign { + signedSchema, err := s.signSchemaJWT(request.Author, schemaValue) + if err != nil { + return nil, util.LoggingError(err) + } + storedSchema.SchemaJWT = signedSchema + } + if err := s.storage.StoreSchema(storedSchema); err != nil { return nil, util.LoggingErrorMsg(err, "could not store schema") } - return &CreateSchemaResponse{ID: schemaID, Schema: schemaValue}, nil + return &CreateSchemaResponse{ID: schemaID, Schema: schemaValue, SchemaJWT: storedSchema.SchemaJWT}, nil +} + +// signSchemaJWT signs a schema after the key associated with the provided author for the schema as a JWT +func (s Service) signSchemaJWT(author string, schema schema.VCJSONSchema) (*string, error) { + gotKey, err := s.keyStore.GetKey(keystore.GetKeyRequest{ID: author}) + if err != nil { + errMsg := fmt.Sprintf("could not get key for signing schema for author<%s>", author) + return nil, util.LoggingErrorMsg(err, errMsg) + } + keyAccess, err := keyaccess.NewJWKKeyAccess(gotKey.ID, gotKey.Key) + if err != nil { + errMsg := fmt.Sprintf("could not create key access for signing schema for author<%s>", author) + return nil, errors.Wrap(err, errMsg) + } + schemaJSONBytes, err := sdkutil.ToJSONMap(schema) + if err != nil { + errMsg := fmt.Sprintf("could not marshal schema for signing for author<%s>", author) + return nil, errors.Wrap(err, errMsg) + } + schemaToken, err := keyAccess.SignJWT(schemaJSONBytes) + if err != nil { + errMsg := fmt.Sprintf("could not sign schema for author<%s>", author) + return nil, errors.Wrap(err, errMsg) + } + if err = s.verifySchemaJWT(string(schemaToken)); err != nil { + return nil, errors.Wrap(err, "could not verify signed schema") + } + return sdkutil.StringPtr(string(schemaToken)), nil +} + +type VerifySchemaRequest struct { + SchemaJWT string `json:"credentialJwt"` +} + +type VerifySchemaResponse struct { + Verified bool `json:"verified" json:"verified"` + Reason string `json:"reason,omitempty" json:"reason,omitempty"` +} + +func (s Service) VerifySchema(request VerifySchemaRequest) (*VerifySchemaResponse, error) { + if err := s.verifySchemaJWT(request.SchemaJWT); err != nil { + return &VerifySchemaResponse{Verified: false, Reason: "could not verify schema: " + err.Error()}, nil + } + return &VerifySchemaResponse{Verified: true}, nil +} + +func (s Service) verifySchemaJWT(token string) error { + parsed, err := jwt.Parse([]byte(token)) + if err != nil { + errMsg := "could not parse JWT" + logrus.WithError(err).Error(errMsg) + return util.LoggingErrorMsg(err, errMsg) + } + claims := parsed.PrivateClaims() + claimsJSONBytes, err := json.Marshal(claims) + if err != nil { + errMsg := "could not marshal claims" + logrus.WithError(err).Error(errMsg) + return util.LoggingErrorMsg(err, errMsg) + } + var parsedSchema schema.VCJSONSchema + if err := json.Unmarshal(claimsJSONBytes, &parsedSchema); err != nil { + errMsg := "could not unmarshal claims into schema" + logrus.WithError(err).Error(errMsg) + return util.LoggingErrorMsg(err, errMsg) + } + resolved, err := s.resolver.Resolve(parsedSchema.Author) + if err != nil { + return errors.Wrapf(err, "failed to resolve schema author's did: %s", parsedSchema.Author) + } + kid, pubKey, err := keyaccess.GetVerificationInformation(resolved.DIDDocument, "") + if err != nil { + return util.LoggingErrorMsg(err, "could not get verification information from schema") + } + 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 the schema's signature") + } + return nil } func (s Service) GetSchemas() (*GetSchemasResponse, error) { + + logrus.Debug("getting all schema") + storedSchemas, err := s.storage.GetSchemas() if err != nil { return nil, util.LoggingErrorMsg(err, "error getting schemas") } - var schemas []schema.VCJSONSchema + var schemas []GetSchemaResponse for _, stored := range storedSchemas { - schemas = append(schemas, stored.Schema) + schemas = append(schemas, GetSchemaResponse{ + ID: stored.Schema.ID, + Schema: stored.Schema, + SchemaJWT: stored.SchemaJWT, + }) } return &GetSchemasResponse{ Schemas: schemas, @@ -103,6 +218,9 @@ func (s Service) GetSchemas() (*GetSchemasResponse, error) { } func (s Service) GetSchema(request GetSchemaRequest) (*GetSchemaResponse, error) { + + logrus.Debugf("getting schema: %s", request.ID) + gotSchema, err := s.storage.GetSchema(request.ID) if err != nil { err := errors.Wrapf(err, "error getting schema: %s", request.ID) @@ -112,5 +230,17 @@ func (s Service) GetSchema(request GetSchemaRequest) (*GetSchemaResponse, error) err := fmt.Errorf("schema with id<%s> could not be found", request.ID) return nil, util.LoggingError(err) } - return &GetSchemaResponse{Schema: gotSchema.Schema}, nil + return &GetSchemaResponse{Schema: gotSchema.Schema, SchemaJWT: gotSchema.SchemaJWT}, nil +} + +func (s Service) DeleteSchema(request DeleteSchemaRequest) error { + + logrus.Debugf("deleting schema: %s", request.ID) + + if err := s.storage.DeleteSchema(request.ID); err != nil { + errMsg := fmt.Sprintf("could not delete schema with id: %s", request.ID) + return util.LoggingErrorMsg(err, errMsg) + } + + return nil } diff --git a/pkg/service/schema/storage/bolt.go b/pkg/service/schema/storage/bolt.go index 638d9ce11..7bc6f5fea 100644 --- a/pkg/service/schema/storage/bolt.go +++ b/pkg/service/schema/storage/bolt.go @@ -2,9 +2,11 @@ package storage import ( "fmt" + "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -24,7 +26,7 @@ func NewBoltSchemaStorage(db *storage.BoltDB) (*BoltSchemaStorage, error) { } func (b BoltSchemaStorage) StoreSchema(schema StoredSchema) error { - id := schema.Schema.ID + id := schema.ID if id == "" { err := errors.New("could not store schema without an ID") logrus.WithError(err).Error() diff --git a/pkg/service/schema/storage/storage.go b/pkg/service/schema/storage/storage.go index 4379c7933..8797cb462 100644 --- a/pkg/service/schema/storage/storage.go +++ b/pkg/service/schema/storage/storage.go @@ -10,7 +10,9 @@ import ( ) type StoredSchema struct { - Schema schema.VCJSONSchema `json:"schema"` + ID string `json:"id"` + Schema schema.VCJSONSchema `json:"schema"` + SchemaJWT *string `json:"token,omitempty"` } type Storage interface { diff --git a/pkg/service/service.go b/pkg/service/service.go index 991d174cb..3f2cdcae0 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -83,13 +83,14 @@ func instantiateServices(config config.ServicesConfig) ([]framework.Service, err if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate the DID service") } + didResolver := didService.GetResolver() - schemaService, err := schema.NewSchemaService(config.SchemaConfig, storageProvider, keyStoreService) + schemaService, err := schema.NewSchemaService(config.SchemaConfig, storageProvider, keyStoreService, didResolver) if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate the schema service") } - credentialService, err := credential.NewCredentialService(config.CredentialConfig, storageProvider, keyStoreService, didService.GetResolver()) + credentialService, err := credential.NewCredentialService(config.CredentialConfig, storageProvider, keyStoreService, didResolver) if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate the credential service") }