diff --git a/tests/e2e/ssi_tests/e2e_tests.py b/tests/e2e/ssi_tests/e2e_tests.py index 4272d42..df87d5a 100644 --- a/tests/e2e/ssi_tests/e2e_tests.py +++ b/tests/e2e/ssi_tests/e2e_tests.py @@ -2,6 +2,7 @@ import sys sys.path.insert(1, os.getcwd()) import time +import copy from utils import run_blockchain_command, generate_key_pair, secp256k1_pubkey_to_address, add_keyAgreeemnt_pubKeyMultibase from generate_doc import generate_did_document, generate_schema_document, generate_cred_status_document @@ -805,8 +806,96 @@ def schema_test(): schema_doc_id = schema_doc["id"] run_blockchain_command(create_schema_cmd, f"Registering Schema with Id: {schema_doc_id}") - print("3. PASS: Bob updates a schema using one of her VMs\n") - updated_schema_doc = schema_doc + print("3. FAIL: Bob creates a Schema where the name field is not in Pascal Case \n") + + invalid_schema_doc_1 = copy.deepcopy(schema_doc) + invalid_schema_doc_1["name"] = "Day Pass Credential" + schema_id_list = list(invalid_schema_doc_1["id"]) + schema_id_list[-3] = '4' + invalid_schema_doc_1["id"] = "".join(schema_id_list) + invalid_schema_doc_1_id = invalid_schema_doc_1["id"] + + _, schema_proof = generate_schema_document( + kp_bob, + did_doc_bob, + did_doc_bob_vm["id"], + updated_schema=invalid_schema_doc_1 + ) + update_schema_cmd = form_update_schema_tx( + invalid_schema_doc_1, + schema_proof, + DEFAULT_BLOCKCHAIN_ACCOUNT_NAME + ) + run_blockchain_command(update_schema_cmd, f"Updating Schema with Id: {invalid_schema_doc_1_id}", True) + + print("4. FAIL: Bob creates a Schema where the property field is some random string\n") + + invalid_schema_doc_2 = copy.deepcopy(schema_doc) + invalid_schema_doc_2["schema"]["properties"] = "somestring" + schema_id_list = list(invalid_schema_doc_2["id"]) + schema_id_list[-3] = '4' + invalid_schema_doc_2["id"] = "".join(schema_id_list) + invalid_schema_doc_2_id = invalid_schema_doc_2["id"] + + _, schema_proof = generate_schema_document( + kp_bob, + did_doc_bob, + did_doc_bob_vm["id"], + updated_schema=invalid_schema_doc_2 + ) + update_schema_cmd = form_update_schema_tx( + invalid_schema_doc_2, + schema_proof, + DEFAULT_BLOCKCHAIN_ACCOUNT_NAME + ) + run_blockchain_command(update_schema_cmd, f"Updating Schema with Id: {invalid_schema_doc_2_id}", True) + + print("5. FAIL: Bob creates a Schema where the property field is a valid JSON, but one of the attributes has an invalid sub-attribute\n") + + invalid_schema_doc_3 = copy.deepcopy(schema_doc) + invalid_schema_doc_3["schema"]["properties"] = "{\"fullName\":{\"type\":\"string\",\"sda\":\"string\"},\"companyName\":{\"type\":\"string\"},\"center\":{\"type\":\"string\"},\"invoiceNumber\":{\"type\":\"string\"}}" + schema_id_list = list(invalid_schema_doc_3["id"]) + schema_id_list[-3] = '4' + invalid_schema_doc_3["id"] = "".join(schema_id_list) + invalid_schema_doc_3_id = invalid_schema_doc_3["id"] + + _, schema_proof = generate_schema_document( + kp_bob, + did_doc_bob, + did_doc_bob_vm["id"], + updated_schema=invalid_schema_doc_3 + ) + update_schema_cmd = form_update_schema_tx( + invalid_schema_doc_3, + schema_proof, + DEFAULT_BLOCKCHAIN_ACCOUNT_NAME + ) + run_blockchain_command(update_schema_cmd, f"Updating Schema with Id: {invalid_schema_doc_3_id}", True) + + print("6. FAIL: Bob creates a Schema where the property field is a valid JSON, but `type` sub-attribute is missing\n") + + invalid_schema_doc_4 = copy.deepcopy(schema_doc) + invalid_schema_doc_4["schema"]["properties"] = "{\"fullName\":{\"format\":\"string\"},\"companyName\":{\"type\":\"string\"},\"center\":{\"type\":\"string\"},\"invoiceNumber\":{\"type\":\"string\"}}" + schema_id_list = list(invalid_schema_doc_4["id"]) + schema_id_list[-3] = '4' + invalid_schema_doc_4["id"] = "".join(schema_id_list) + invalid_schema_doc_4_id = invalid_schema_doc_4["id"] + + _, schema_proof = generate_schema_document( + kp_bob, + did_doc_bob, + did_doc_bob_vm["id"], + updated_schema=invalid_schema_doc_4 + ) + update_schema_cmd = form_update_schema_tx( + invalid_schema_doc_4, + schema_proof, + DEFAULT_BLOCKCHAIN_ACCOUNT_NAME + ) + run_blockchain_command(update_schema_cmd, f"Updating Schema with Id: {invalid_schema_doc_4_id}", True) + + print("7. PASS: Bob updates a schema using one of her VMs\n") + updated_schema_doc = copy.deepcopy(schema_doc) #Increment version in Schema id schema_id_list = list(updated_schema_doc["id"]) schema_id_list[-3] = '4' diff --git a/tests/e2e/ssi_tests/generate_doc.py b/tests/e2e/ssi_tests/generate_doc.py index 063a9f6..ccd9e37 100644 --- a/tests/e2e/ssi_tests/generate_doc.py +++ b/tests/e2e/ssi_tests/generate_doc.py @@ -104,15 +104,19 @@ def generate_schema_document(key_pair, schema_author, vm, signature=None, algo=" "type": "https://schema.org/Person", "modelVersion": "v1.0", "id": "", - "name": "Person", + "name": "SomeCredentialSchema", "author": "", "authored": "2022-08-16T10:22:12Z", "schema": { "schema":"https://json-schema.org/draft-07/schema#", "description":"Person Schema", "type":"object", - "properties":"{givenName:{type:string},gender:{type:string},email:{type:text},address:{type:text}}", - "required":["givenName","address"], + "properties":"{\"fullName\":{\"type\":\"string\"},\"companyName\":{\"type\":\"string\"},\"center\":{\"type\":\"string\"},\"invoiceNumber\":{\"type\":\"string\"}}", + "required": [ + "fullName", + "center", + "invoiceNumber" + ], } } diff --git a/x/ssi/keeper/msg_server_schema.go b/x/ssi/keeper/msg_server_schema.go index 43e76aa..86f7f1e 100644 --- a/x/ssi/keeper/msg_server_schema.go +++ b/x/ssi/keeper/msg_server_schema.go @@ -2,7 +2,9 @@ package keeper import ( "context" + "encoding/json" "fmt" + "regexp" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -47,6 +49,16 @@ func storeCredentialSchema( return sdkerrors.Wrap(types.ErrSchemaExists, fmt.Sprintf("Schema ID: %s", schemaID)) } + // Check if the `name` attribute of schema is in PascalCase + if !isStringInPascalCase(schemaDoc.Name) { + return sdkerrors.Wrapf(types.ErrInvalidCredentialSchema, "name must always be in PascalCase: %v", schemaDoc.Name) + } + + // Check if `properties` field is a valid JSON document + if err := isValidJSONDocument(schemaDoc.Schema.Properties); err != nil { + return sdkerrors.Wrapf(types.ErrInvalidCredentialSchema, "invalid `property` provided: %v", err.Error()) + } + // Signature check if err := k.VerifyDocumentProof(ctx, schemaDoc, schemaProof); err != nil { return sdkerrors.Wrap(types.ErrInvalidClientSpecType, err.Error()) @@ -82,3 +94,44 @@ func (k msgServer) UpdateCredentialSchema(goCtx context.Context, msg *types.MsgU return &types.MsgUpdateCredentialSchemaResponse{}, nil } + +// isStringInPascalCase checks if an input string is in Pascal case or not +func isStringInPascalCase(s string) bool { + pascalCaseRegex := regexp.MustCompile(`^[A-Z][a-zA-Z0-9]*$`) + + return pascalCaseRegex.MatchString(s) +} + +// isValidJSONDocument checks if the schema property attribute is a valid JSON string +// The `type` sub-attribute is a must for every attributes in property. +// The `format` sub-attribute is acceptable, but optional +func isValidJSONDocument(schemaProperty string) error { + var schemaPropertyDocument map[string]map[string]interface{} + if err := json.Unmarshal([]byte(schemaProperty), &schemaPropertyDocument); err != nil { + return err + } + + for attributeName, attributeObj := range schemaPropertyDocument { + isTypeSubAttributePresent := false + + for subAttributeName := range attributeObj { + if subAttributeName == "type" { + isTypeSubAttributePresent = true + } else if subAttributeName == "format" { + continue + } else { + return fmt.Errorf( + "invalid sub-attribute %v of attribute %v. Only `type` and `format` sub-attributes are permitted", + subAttributeName, + attributeName, + ) + } + } + + if !isTypeSubAttributePresent { + return fmt.Errorf("%v attribute is missing the required sub-attribute `type`", attributeName) + } + } + + return nil +} diff --git a/x/ssi/ld-context/types.go b/x/ssi/ld-context/types.go index 2de19c4..7fbbd25 100644 --- a/x/ssi/ld-context/types.go +++ b/x/ssi/ld-context/types.go @@ -191,7 +191,7 @@ func NewJsonLdCredentialSchema(credSchema *types.CredentialSchemaDocument) *Json // It is a similar to `Did` struct, with the exception that the `context` attribute is of type // `contextObject` instead of `[]string`, which is meant for accomodating Context JSON body -// having arbritrary attributes. It should be used for performing Canonization. +// having arbritrary attributes. It should be used for performing Canonization. type JsonLdDidDocumentWithoutVM struct { Context []contextObject `json:"@context,omitempty"` Id string `json:"id,omitempty"` @@ -269,4 +269,3 @@ func newVerificationMethodWithoutController(vm *types.VerificationMethod) verifi } return vmWithoutController } - diff --git a/x/ssi/types/errors.go b/x/ssi/types/errors.go index 12760c7..f9061af 100644 --- a/x/ssi/types/errors.go +++ b/x/ssi/types/errors.go @@ -26,4 +26,5 @@ var ( ErrCredentialStatusExists = sdkerrors.Register(ModuleName, 116, "credential status document already exists") ErrInvalidCredentialStatusID = sdkerrors.Register(ModuleName, 117, "invalid credential status Id") ErrInvalidProof = sdkerrors.Register(ModuleName, 118, "invalid document proof") + ErrInvalidCredentialSchema = sdkerrors.Register(ModuleName, 119, "invalid credential schema") )