diff --git a/cmd/hid-noded/cmd/debug_extensions.go b/cmd/hid-noded/cmd/debug_extensions.go index 883579f..1d6c68c 100644 --- a/cmd/hid-noded/cmd/debug_extensions.go +++ b/cmd/hid-noded/cmd/debug_extensions.go @@ -35,6 +35,7 @@ func secp256k1Cmd() *cobra.Command { cmd.AddCommand( secp256k1RandomCmd(), + secp256k1Bech32AddressCmd(), secp256k1EthRandomCmd(), ) @@ -79,6 +80,30 @@ func secp256k1RandomCmd() *cobra.Command { return cmd } +func secp256k1Bech32AddressCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "bech32-addr [base64-encoded-public-key] [prefix]", + Short: "Converts a compressed base64 encoded secp256k1 public key to bech32 address", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + base64PubKey := args[0] + addrPrefix := args[1] + + publicKeyBytes, err := base64.StdEncoding.DecodeString(base64PubKey) + if err != nil { + panic(err) + } + + bech32address := publicKeyToBech32Address(addrPrefix, publicKeyBytes) + + _, err = fmt.Fprintln(cmd.OutOrStdout(), bech32address) + return err + }, + } + + return cmd +} + func secp256k1EthRandomCmd() *cobra.Command { cmd := &cobra.Command{ Use: "eth-hex-random", diff --git a/cmd/hid-noded/cmd/debug_extensions_utils.go b/cmd/hid-noded/cmd/debug_extensions_utils.go new file mode 100644 index 0000000..47e6e1b --- /dev/null +++ b/cmd/hid-noded/cmd/debug_extensions_utils.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "crypto/sha256" + "fmt" + + "golang.org/x/crypto/ripemd160" //nolint: staticcheck + + bech32 "github.com/cosmos/cosmos-sdk/types/bech32" +) + +// publicKeyToBech32Address converts publicKey byteArray to Bech32 encoded blockchain address +func publicKeyToBech32Address(addressPrefix string, pubKeyBytes []byte) string { + // Throw error if the length of secp256k1 publicKey is not 33 + if len(pubKeyBytes) != 33 { + panic(fmt.Sprintf("invalid secp256k1 public key length %v", len(pubKeyBytes))) + } + + // Hash pubKeyBytes as: RIPEMD160(SHA256(public_key_bytes)) + pubKeySha256Hash := sha256.Sum256(pubKeyBytes) + ripemd160hash := ripemd160.New() + ripemd160hash.Write(pubKeySha256Hash[:]) + addressBytes := ripemd160hash.Sum(nil) + + // Convert addressBytes to bech32 encoded address + address, err := bech32.ConvertAndEncode(addressPrefix, addressBytes) + if err != nil { + panic(err) + } + return address +} diff --git a/go.mod b/go.mod index d5eabd9..f770eaf 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/tendermint/spm v0.1.9 github.com/tendermint/tendermint v0.34.23 github.com/tendermint/tm-db v0.6.7 + golang.org/x/crypto v0.1.0 google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a google.golang.org/grpc v1.50.1 ) @@ -115,7 +116,6 @@ require ( github.com/tendermint/go-amino v0.16.0 // indirect github.com/zondax/hid v0.9.0 // indirect go.etcd.io/bbolt v1.3.6 // indirect - golang.org/x/crypto v0.1.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/sys v0.5.0 // indirect diff --git a/tests/e2e/ssi_tests/README.md b/tests/e2e/ssi_tests/README.md index 80160b0..3a40c51 100644 --- a/tests/e2e/ssi_tests/README.md +++ b/tests/e2e/ssi_tests/README.md @@ -1,19 +1,3 @@ -# SSI Module E2E Tests - -Following scenarios are covered for E2E testing: - -`TC-1`: A simple SSI flow where three elements of SSI (DID Document, Schema Document and Credential Status Document).
-`TC-2`: Controller DID attempts to create Credential Schema and Credential Status Documents on behalf of Parent DID.
-`TC-3`: Multiple Controller DID attempt to create Credential Schema and Credential Status Documents on behalf of Parent DID.
-`TC-4`: Non-Controller DID attempts to create Credential Schema and Credential Status Documents on behalf of Parent DID (Invalid Case).
-`TC-5`: Non-Controller DID attempts to update a DID Document (Invalid Case).
-`TC-6`: Controller DID attempts to update the Parent DID Document.
-`TC-7`: Parent DID adds multiple DIDs in its controller group, and removes itself. One of the controllers and the Parent DID attemps to change the DID Document.
-`TC-8`: Deactivated DID attempts to create Schema and Credential Status Documents.
-`TC-9`: `x/ssi` module related transactions, using `secp256k1` keypair.
-`TC-10`: Test scenarios for `blockchainAccountId`.
-`TC-11`: `x/ssi` module related transactions, using ethereum based `secp256k1` keypair.
- ## Run Tests Run the following to run tests diff --git a/tests/e2e/ssi_tests/e2e_tests.py b/tests/e2e/ssi_tests/e2e_tests.py index 4bf4710..5b32a37 100644 --- a/tests/e2e/ssi_tests/e2e_tests.py +++ b/tests/e2e/ssi_tests/e2e_tests.py @@ -3,7 +3,7 @@ sys.path.insert(1, os.getcwd()) import time -from utils import run_blockchain_command, generate_key_pair +from utils import run_blockchain_command, generate_key_pair, secp256k1_pubkey_to_address from generate_doc import generate_did_document, generate_schema_document, generate_cred_status_document from transactions import form_did_create_tx_multisig, form_did_update_tx_multisig, \ query_did, form_create_schema_tx, form_did_deactivate_tx_multisig, form_create_cred_status_tx @@ -141,7 +141,7 @@ def create_did_test(): create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) run_blockchain_command(create_tx_cmd, f"Registering DID by passing both Alice's and Eve's Signature") - print("\n--- Test Completed ---\n") + print("--- Test Completed ---\n") # TC - II : Update DID scenarios def update_did_test(): @@ -353,7 +353,7 @@ def update_did_test(): update_tx_cmd = form_did_update_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) run_blockchain_command(update_tx_cmd, f"Jenny (controller) attempts to update Tx") - print("\n--- Test Completed ---\n") + print("--- Test Completed ---\n") def deactivate_did(): print("\n--- Deactivate DID Test ---\n") @@ -431,7 +431,9 @@ def deactivate_did(): signers = [] signers.append(signPair_mike) deactivate_tx_cmd = form_did_deactivate_tx_multisig(did_doc_mike, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) - run_blockchain_command(deactivate_tx_cmd, f"Deactivation of Mike's DID with Id: {did_doc_mike}") + run_blockchain_command(deactivate_tx_cmd, f"Deactivation of Mike's DID with Id: {did_doc_mike}") + + print("--- Test Completed ---\n") def schema_test(): print("\n--- Schema Test ---\n") @@ -505,7 +507,7 @@ def schema_test(): schema_doc_id = schema_doc["id"] run_blockchain_command(create_schema_cmd, f"Registering Schema with Id: {schema_doc_id}") - print("\n--- Test Completed ---\n") + print("--- Test Completed ---\n") def credential_status_test(): print("\n--- Credential Status Test ---\n") @@ -579,4 +581,291 @@ def credential_status_test(): cred_id = cred_doc["claim"]["id"] run_blockchain_command(register_cred_status_cmd, f"Registering Credential status with Id: {cred_id}") - print("\n--- Test Completed ---\n") \ No newline at end of file + print("--- Test Completed ---\n") + +def caip10_ethereum_support_test(): + print("\n--- CAIP-10 Test: Ethereum Chains ---\n") + kp_algo = "recover-eth" + + # Invalid blockchain Account Ids + invalid_blockchain_account_ids = [ + "abc345566", + "eip:1:0x1234", + "eip155", + "eip155:1:", + "eip155::", + "eip155:1", + "eip155:::::23", + "eip155::0x1234567" + ] + + for invalid_blockchain_id in invalid_blockchain_account_ids: + print("Registering a DID Document with an invalid blockchainAccountId:", invalid_blockchain_id) + kp = generate_key_pair(algo=kp_algo) + + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_string["verificationMethod"][0]["blockchainAccountId"] = invalid_blockchain_id + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering a DID Document with an invalid blockchainAccountId: {invalid_blockchain_id}", True) + + print("Registering a DID with a VM of type EcdsaSecp256k1SignatureRecovery2020 having publicKeyMultibase attribute populated") + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + did_doc_string["verificationMethod"][0]["publicKeyMultibase"] = "zrxxgf1f9xPYTraixqi9tipLta61hp4VJWQUUW5pmwcVz" + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, "Registering a DID with a VM of type EcdsaSecp256k1SignatureRecovery2020 having publicKeyMultibase attribute populated", True, True) + + # Test for valid blockchainAccountId + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + print( + "Registering a DID Document with a valid blockchainAccountId:", + did_doc_string["verificationMethod"][0]["blockchainAccountId"] + ) + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}") + + print("--- Test Completed ---\n") + +def caip10_cosmos_support_test(): + print("\n--- CAIP-10 Test: Cosmos Chains ---\n") + + kp_algo = "secp256k1" + + # Invalid blockchain Account Ids + invalid_blockchain_account_ids = [ + "abc345566", + "cos:1:0x1234", + "cosmos", + "cosmos:1", + "cosmos:jagrat", + "cosmos:1:", + "cosmos::", + "cosmos:1", + "cosmos:::::23", + "cosmos::0x1234567" + ] + + for invalid_blockchain_id in invalid_blockchain_account_ids: + print("Registering a DID Document with an invalid blockchainAccountId:", invalid_blockchain_id) + kp = generate_key_pair(algo=kp_algo) + + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_string["verificationMethod"][0]["blockchainAccountId"] = invalid_blockchain_id + + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering a DID Document with an invalid blockchainAccountId: {invalid_blockchain_id}", True) + + print("Registering a DID with a VM of type EcdsaSecp256k1VerificationKey2019 having both publicKeyMultibase and blockchainAccountId attributes populated") + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering the DID with DID Id {did_doc_id}") + + print("Registering a DID with invalid chain-id in blockchainAccountId") + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + blockchainAccountId = secp256k1_pubkey_to_address(kp["pub_key_base_64"], "hid") + did_doc_string["verificationMethod"][0]["blockchainAccountId"] = "cosmos:hidnode02:" + blockchainAccountId + + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}", True) + + print("--- Test Completed ---\n") + +def vm_type_test(): + print("\n--- Verification Method Types Test ---\n") + + # Ed25519VerificationKey2020 + print("1. FAIL: Registering DID Document with a verification method of type Ed25519VerificationKey2020. Both publicKeyMultibase and blockchainAccountId are passed.") + kp_alice = generate_key_pair() + signers = [] + did_doc_string = generate_did_document(kp_alice) + did_doc_alice = did_doc_string["id"] + did_doc_string["verificationMethod"][0]["blockchainAccountId"] = "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ:7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv" + signPair_alice = { + "kp": kp_alice, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": "ed25519" + } + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_alice}", True, True) + + print("2. FAIL: Registering DID Document with a verification method of type Ed25519VerificationKey2020. Only blockchainAccountId is passed.") + kp_alice = generate_key_pair() + signers = [] + did_doc_string = generate_did_document(kp_alice) + did_doc_alice = did_doc_string["id"] + did_doc_string["verificationMethod"][0]["publicKeyMultibase"] = "" + did_doc_string["verificationMethod"][0]["blockchainAccountId"] = "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ:7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv" + signPair_alice = { + "kp": kp_alice, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": "ed25519" + } + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_alice}", True, True) + + print("3. PASS: Registering DID Document with a verification method of type Ed25519VerificationKey2020. Only publicKeyMultibase is passed.") + kp_alice = generate_key_pair() + signers = [] + did_doc_string = generate_did_document(kp_alice) + did_doc_alice = did_doc_string["id"] + signPair_alice = { + "kp": kp_alice, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": "ed25519" + } + signers.append(signPair_alice) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_alice}") + + # EcdsaSecp256k1VerificationKey2019 + print("4. PASS: Registering DID Document with a verification method of type EcdsaSecp256k1VerificationKey2019. Only publicKeyMultibase is passed.") + kp_algo = "secp256k1" + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + did_doc_string["verificationMethod"][0]["blockchainAccountId"] = "" + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}") + + print("5. PASS: Registering DID Document with a verification method of type EcdsaSecp256k1VerificationKey2019. Both publicKeyMultibase and blockchainAccountId are passed.") + kp_algo = "secp256k1" + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}") + + print("6. FAIL: Registering DID Document with a verification method of type EcdsaSecp256k1VerificationKey2019. Only blockchainAccountId is passed.") + kp_algo = "secp256k1" + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + did_doc_string["verificationMethod"][0]["publicKeyMultibase"] = "" + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}", True, True) + + # EcdsaSecp256k1RecoveryMethod2020 + print("7. FAIL: Registering DID Document with a verification method of type EcdsaSecp256k1RecoveryMethod2020. Only publicKeyMultibase is passed.") + kp_algo = "recover-eth" + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + did_doc_string["verificationMethod"][0]["publicKeyMultibase"] = "z22XxPVrzTr24zUBGfZiZCm9bwj3RkHKLmx9ENYBxmY57o" + did_doc_string["verificationMethod"][0]["blockchainAccountId"] = "" + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}", True, True) + + print("8. FAIL: Registering DID Document with a verification method of type EcdsaSecp256k1RecoveryMethod2020. Both publicKeyMultibase and blockchainAccountId is passed.") + kp_algo = "recover-eth" + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + did_doc_string["verificationMethod"][0]["publicKeyMultibase"] = "z22XxPVrzTr24zUBGfZiZCm9bwj3RkHKLmx9ENYBxmY57o" + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}", True, True) + + print("9. PASS: Registering DID Document with a verification method of type EcdsaSecp256k1RecoveryMethod2020. Only blockchainAccountId is passed.") + kp_algo = "recover-eth" + kp = generate_key_pair(algo=kp_algo) + did_doc_string = generate_did_document(kp, kp_algo) + did_doc_id = did_doc_string["id"] + signers = [] + signPair = { + "kp": kp, + "verificationMethodId": did_doc_string["verificationMethod"][0]["id"], + "signing_algo": kp_algo + } + signers.append(signPair) + create_tx_cmd = form_did_create_tx_multisig(did_doc_string, signers, DEFAULT_BLOCKCHAIN_ACCOUNT_NAME) + run_blockchain_command(create_tx_cmd, f"Registering DID with Id: {did_doc_id}") + + print("--- Test Completed ---\n") + \ No newline at end of file diff --git a/tests/e2e/ssi_tests/generate_doc.py b/tests/e2e/ssi_tests/generate_doc.py index 34c1905..a1aea56 100644 --- a/tests/e2e/ssi_tests/generate_doc.py +++ b/tests/e2e/ssi_tests/generate_doc.py @@ -3,7 +3,8 @@ sys.path.insert(1, os.getcwd()) import json -from utils import run_command, generate_document_id, get_document_signature +from utils import run_command, generate_document_id, get_document_signature, \ + secp256k1_pubkey_to_address def generate_did_document(key_pair, algo="ed25519"): base_document = { @@ -38,6 +39,11 @@ def generate_did_document(key_pair, algo="ed25519"): } if algo == "recover-eth": verification_method["blockchainAccountId"] = "eip155:1:" + key_pair["ethereum_address"] + elif algo == "secp256k1": + + verification_method["blockchainAccountId"] = "cosmos:jagrat:" + \ + secp256k1_pubkey_to_address(key_pair["pub_key_base_64"], "hid") + verification_method["publicKeyMultibase"] = key_pair["pub_key_multibase"] else: verification_method["publicKeyMultibase"] = key_pair["pub_key_multibase"] diff --git a/tests/e2e/ssi_tests/run.py b/tests/e2e/ssi_tests/run.py index ac12599..75d5607 100644 --- a/tests/e2e/ssi_tests/run.py +++ b/tests/e2e/ssi_tests/run.py @@ -29,6 +29,9 @@ def run_all_tests(): schema_test() deactivate_did() credential_status_test() + caip10_ethereum_support_test() + caip10_cosmos_support_test() + vm_type_test() print("============= 😃️ All test cases completed successfully ============== \n") diff --git a/tests/e2e/ssi_tests/utils.py b/tests/e2e/ssi_tests/utils.py index be1f6ba..3997f97 100644 --- a/tests/e2e/ssi_tests/utils.py +++ b/tests/e2e/ssi_tests/utils.py @@ -102,4 +102,9 @@ def get_document_signature(doc: dict, doc_type: str, key_pair: dict, algo: str = cmd_string = f"hid-noded debug sign-ssi-doc {doc_cmd} '{json.dumps(doc)}' {private_key} {algo}" signature, _ = run_command(cmd_string) - return signature \ No newline at end of file + return signature + +def secp256k1_pubkey_to_address(pub_key, prefix): + cmd_string = f"hid-noded debug secp256k1 bech32-addr {pub_key} {prefix}" + addr, _ = run_command(cmd_string) + return addr diff --git a/x/ssi/types/common.go b/x/ssi/types/common.go index f5f53e0..ad55034 100644 --- a/x/ssi/types/common.go +++ b/x/ssi/types/common.go @@ -12,6 +12,16 @@ var VerificationKeySignatureMap = map[string]string{ EcdsaSecp256k1RecoveryMethod2020: "EcdsaSecp256k1RecoverySignature2020", } +var supportedVerificationMethodTypes []string = func() []string { + result := []string{} + + for vmType := range VerificationKeySignatureMap { + result = append(result, vmType) + } + + return result +}() + // Supported Service Types var SupportedServiceTypes = []string{ "LinkedDomains", @@ -21,14 +31,42 @@ var SupportedServiceTypes = []string{ const DocumentIdentifierDid = "did" const DidMethod = "hid" +// CAIP-10 prefixes +const EthereumCAIP10Prefix string = "eip155" // Ethereum Based Chains +const CosmosCAIP10Prefix string = "cosmos" // Cosmos Based Chains + // Supported CAIP-10 prefixes -const EIP155 string = "eip155" +var CAIP10PrefixForEcdsaSecp256k1RecoveryMethod2020 []string = []string{ + EthereumCAIP10Prefix, +} + +var CAIP10PrefixForEcdsaSecp256k1VerificationKey2019 []string = []string{ + CosmosCAIP10Prefix, +} -// Support Client Specs const ADR036Spec string = "cosmos-ADR036" const PersonalSignSpec string = "eth-personalSign" +// Supported Client Specs var SupportedClientSpecs []string = []string{ ADR036Spec, PersonalSignSpec, } + +// Map between supported cosmos chain-id and their respective blockhchain address prefix +var CosmosCAIP10ChainIdBech32PrefixMap = map[string]string{ + // Mainnet Chains + "cosmoshub-4": "cosmos", + "osmosis-1": "osmo", + "akashnet-2": "akash", + "stargaze-1": "stars", + "core-1": "persistence", + "crypto-org-chain-mainnet-1": "cro", + + // Testnet Chains + "theta-testnet-001": "cosmos", + "osmo-test-4": "osmo", + "elgafar-1": "stars", + "test-core-1": "persistence", + "jagrat": "hid", +} diff --git a/x/ssi/types/diddoc_validation.go b/x/ssi/types/diddoc_validation.go index a6dba62..3f1aee7 100644 --- a/x/ssi/types/diddoc_validation.go +++ b/x/ssi/types/diddoc_validation.go @@ -76,31 +76,27 @@ func isDidUrl(id string) error { } func isSupportedVmType(typ string) error { - supportedList := func() string { - var resultStr string = "" - for vKeyType := range VerificationKeySignatureMap { - resultStr += fmt.Sprintf("%s, ", vKeyType) - } - return resultStr - } - if _, supported := VerificationKeySignatureMap[typ]; !supported { return fmt.Errorf( - "%s verification method type not supported, supported verification method types: %s ", + "%v verification method type not supported, supported verification method types: %v ", typ, - supportedList(), + supportedVerificationMethodTypes, ) } return nil } // verificationKeyCheck validates of publicKeyMultibase and blockchainAccountId. -// If the verfication method type is EcdsaSecp256k1RecoveryMethod2020, only blockchainAccountId -// field must be populated, else only publicKeyMultibase must be populated. func verificationKeyCheck(vm *VerificationMethod) error { - // Verification Method of Type EcdsaSecp256k1RecoveryMethod2020 should only have - // `blockchainAccountId` field populated switch vm.Type { + case EcdsaSecp256k1VerificationKey2019: + if vm.GetPublicKeyMultibase() == "" { + return fmt.Errorf( + "publicKeyMultibase cannot be empty for verification method %s as it is type %s", + vm.Id, + vm.Type, + ) + } case EcdsaSecp256k1RecoveryMethod2020: if vm.GetBlockchainAccountId() == "" { return fmt.Errorf( @@ -109,29 +105,31 @@ func verificationKeyCheck(vm *VerificationMethod) error { vm.Type, ) } + if vm.GetPublicKeyMultibase() != "" { return fmt.Errorf( - "publicKeyMultibase should be empty for verification method %s as it is type %s", + "publicKeyMultibase should not be provided for verification method %s as it is of type %s", vm.Id, vm.Type, ) } - - default: + case Ed25519VerificationKey2020: if vm.GetBlockchainAccountId() != "" { return fmt.Errorf( - "blockchainAccountId should be empty for verification method %s as it is of type %s", + "blockchainAccountId is currently not supported for verification method %s as it is of type %s", vm.Id, vm.Type, ) } if vm.GetPublicKeyMultibase() == "" { return fmt.Errorf( - "publicKeyMultibase cannot be empty for verification method %s as it is type %s", + "publicKeyMultibase cannot be empty for verification method %s as it is of type %s", vm.Id, vm.Type, ) } + default: + return fmt.Errorf("unsupported verification method type: %v", supportedVerificationMethodTypes) } return nil diff --git a/x/ssi/verification/address.go b/x/ssi/verification/address.go new file mode 100644 index 0000000..cdcf49b --- /dev/null +++ b/x/ssi/verification/address.go @@ -0,0 +1,31 @@ +package verification + +import ( + "crypto/sha256" + "fmt" + + "golang.org/x/crypto/ripemd160" // nolint: staticcheck + + bech32 "github.com/cosmos/cosmos-sdk/types/bech32" +) + +// publicKeyToCosmosBech32Address converts publicKey byteArray to Bech32 encoded blockchain address +func publicKeyToCosmosBech32Address(addressPrefix string, pubKeyBytes []byte) string { + // Throw error if the length of secp256k1 publicKey is not 33 + if len(pubKeyBytes) != 33 { + panic(fmt.Sprintf("invalid secp256k1 public key length %v", len(pubKeyBytes))) + } + + // Hash pubKeyBytes as: RIPEMD160(SHA256(public_key_bytes)) + pubKeySha256Hash := sha256.Sum256(pubKeyBytes) + ripemd160hash := ripemd160.New() + ripemd160hash.Write(pubKeySha256Hash[:]) + addressBytes := ripemd160hash.Sum(nil) + + // Convert addressBytes to bech32 encoded address + address, err := bech32.ConvertAndEncode(addressPrefix, addressBytes) + if err != nil { + panic(err) + } + return address +} diff --git a/x/ssi/verification/caip10.go b/x/ssi/verification/caip10.go index 7cfc193..54bb476 100644 --- a/x/ssi/verification/caip10.go +++ b/x/ssi/verification/caip10.go @@ -1,6 +1,7 @@ package verification import ( + "fmt" "strings" "github.com/hypersign-protocol/hid-node/x/ssi/types" @@ -13,15 +14,37 @@ func getBlockchainAddress(blockchainAccountId string) string { return blockchainAddress } +// getChainIdFromBlockchainAccountId extracts chain from blockchainAccountId +func getChainIdFromBlockchainAccountId(blockchainAccountId string) string { + chainIdIdx := 1 + + blockchainAccountIdElements := strings.Split(blockchainAccountId, ":") + blockchainChainId := blockchainAccountIdElements[chainIdIdx] + return blockchainChainId +} + // Extracts the CAIP-10 prefix from blockchainAccountId and returns the chain spec -func getCAIP10Chain(blockchainAccountId string) string { +func getCAIP10Prefix(blockchainAccountId string) (string, error) { segments := strings.Split(blockchainAccountId, ":") - userPrefix := strings.Join(segments[0:len(segments)-1], ":") - // Ethereum based chain (EIP-155) check - if strings.HasPrefix(userPrefix, types.EIP155) { - return types.EIP155 + // Validate blockchainAccountId + if len(segments) != 3 { + return "", fmt.Errorf( + "invalid CAIP-10 format for blockchainAccountId '%v'. Please refer: https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md", + blockchainAccountId, + ) + } + + // Prefix check + if segments[0] == types.EthereumCAIP10Prefix { + return types.EthereumCAIP10Prefix, nil + } else if segments[0] == types.CosmosCAIP10Prefix { + return types.CosmosCAIP10Prefix, nil } else { - return "" + return "", fmt.Errorf( + "unsupported CAIP-10 prefix in blockchainAccountId '%v'. Supported CAIP-10 prefixes: %v", + blockchainAccountId, + types.CAIP10PrefixForEcdsaSecp256k1RecoveryMethod2020, + ) } } diff --git a/x/ssi/verification/crypto.go b/x/ssi/verification/crypto.go index 385d770..0231063 100644 --- a/x/ssi/verification/crypto.go +++ b/x/ssi/verification/crypto.go @@ -41,92 +41,173 @@ func verifyAny(extendedVmList []*types.ExtendedVerificationMethod, data []byte) } func verify(extendedVm *types.ExtendedVerificationMethod, data []byte) error { - var verificationKey string - var signature string = extendedVm.Signature - - if extendedVm.PublicKeyMultibase != "" { - verificationKey = extendedVm.PublicKeyMultibase - } else if extendedVm.BlockchainAccountId != "" { - verificationKey = extendedVm.BlockchainAccountId - } else { - return fmt.Errorf("either publicKeyMultibase or BlockchainAccountId must be present") - } - switch extendedVm.Type { case types.Ed25519VerificationKey2020: - return verifyEd25519(verificationKey, signature, data) + return verifyEd25519VerificationKey2020Key(extendedVm, data) case types.EcdsaSecp256k1VerificationKey2019: - return verifySecp256k1(verificationKey, signature, data) + return verifyEcdsaSecp256k1VerificationKey2019Key(extendedVm, data) case types.EcdsaSecp256k1RecoveryMethod2020: - chain := getCAIP10Chain(verificationKey) - // Check for supported chains - switch chain { - // Ethereum based chains - case types.EIP155: - return recoverEthPublicKey(verificationKey, signature, data) - default: - return fmt.Errorf("unsupported blockchain address: %s", verificationKey) - } + return verifyEcdsaSecp256k1RecoveryMethod2020Key(extendedVm, data) default: return fmt.Errorf("unsupported verification method: %s", extendedVm.Type) } } -func verifyEd25519(publicKey string, signature string, documentBytes []byte) error { +// verifyEcdsaSecp256k1RecoveryMethod2020Key verifies the verification key for verification method type EcdsaSecp256k1RecoveryMethod2020 +func verifyEcdsaSecp256k1RecoveryMethod2020Key(extendedVm *types.ExtendedVerificationMethod, documentBytes []byte) error { + extractedCAIP10Prefix, err := getCAIP10Prefix(extendedVm.BlockchainAccountId) + if err != nil { + return err + } + + switch extractedCAIP10Prefix { + case types.EthereumCAIP10Prefix: + return verifyEthereumBlockchainAccountId( + extendedVm, + documentBytes, + ) + default: + return fmt.Errorf( + "unsupported CAIP-10 prefix: '%v', supported CAIP-10 prefixes for verification method type %v: %v", + extractedCAIP10Prefix, + extendedVm.Type, + types.CAIP10PrefixForEcdsaSecp256k1RecoveryMethod2020, + ) + } +} + +// verifyEd25519VerificationKey2020Key verifies the verification key for verification method type Ed25519VerificationKey2020 +func verifyEd25519VerificationKey2020Key(extendedVm *types.ExtendedVerificationMethod, documentBytes []byte) error { // Decode Public Key - _, publicKeyBytes, err := multibase.Decode(publicKey) + _, publicKeyBytes, err := multibase.Decode(extendedVm.PublicKeyMultibase) if err != nil { return fmt.Errorf( "cannot decode Ed25519 public key %s", - publicKey, + extendedVm.PublicKeyMultibase, ) } // Decode Signatures - signatureBytes, err := base64.StdEncoding.DecodeString(signature) + signatureBytes, err := base64.StdEncoding.DecodeString(extendedVm.Signature) if err != nil { return err } if !ed25519.Verify(publicKeyBytes, documentBytes, signatureBytes) { - return fmt.Errorf("ed25519: signature could not be verified") + return fmt.Errorf("signature could not be verified for verificationMethodId: %v", extendedVm.Id) } else { return nil } } -func verifySecp256k1(publicKey string, signature string, documentBytes []byte) error { +// verifyEcdsaSecp256k1VerificationKey2019Key verifies the verification key for verification method type EcdsaSecp256k1VerificationKey2019 +func verifyEcdsaSecp256k1VerificationKey2019Key(extendedVm *types.ExtendedVerificationMethod, documentBytes []byte) error { + // Decode and Parse Signature + signatureBytes, err := base64.StdEncoding.DecodeString(extendedVm.Signature) + if err != nil { + return err + } + // Decode Public Key - _, publicKeyBytes, err := multibase.Decode(publicKey) + _, publicKeyBytes, err := multibase.Decode(extendedVm.PublicKeyMultibase) if err != nil { return err } var pubKeyObj secp256k1.PubKey = publicKeyBytes - // Decode and Parse Signature - signatureBytes, err := base64.StdEncoding.DecodeString(signature) + // Check if the signature is valid for given publicKeyMultibase + if !pubKeyObj.VerifySignature(documentBytes, signatureBytes) { + return fmt.Errorf("signature could not be verified for verificationMethodId: %v", extendedVm.Id) + } + + // Check if blockchainAccountId is passed + if extendedVm.BlockchainAccountId != "" { + extractedCAIP10Prefix, err := getCAIP10Prefix(extendedVm.BlockchainAccountId) + if err != nil { + return err + } + + switch extractedCAIP10Prefix { + case types.CosmosCAIP10Prefix: + return verifyCosmosBlockchainAccountId( + extendedVm.BlockchainAccountId, + extendedVm.PublicKeyMultibase, + ) + default: + return fmt.Errorf( + "unsupported CAIP-10 prefix: '%v', supported CAIP-10 prefixes for verification method type %v: %v", + extractedCAIP10Prefix, + extendedVm.Type, + types.CAIP10PrefixForEcdsaSecp256k1VerificationKey2019, + ) + } + } + + return nil +} + +// verifyCosmosBlockchainAccountId verifies Cosmos Ecosystem based blockchain address. The verified +// publicKeyMultibase is converted to a bech32 encoded blockchain address which is then compared with the +// user provided blockchain address. If they do not match, error is returned. +func verifyCosmosBlockchainAccountId(blockchainAccountId, publicKeyMultibase string) error { + // Check if the blockchainAccountId prefix is valid + extractedCAIP10Prefix, err := getCAIP10Prefix(blockchainAccountId) if err != nil { return err } + if extractedCAIP10Prefix != types.CosmosCAIP10Prefix { + return fmt.Errorf( + "expected CAIP-10 prefix to be '%v', got '%v'", + types.CosmosCAIP10Prefix, + extractedCAIP10Prefix, + ) + } - if !pubKeyObj.VerifySignature(documentBytes, signatureBytes) { - return fmt.Errorf("secp256k1: signature could not be verified") + // Decode public key + _, publicKeyBytes, err := multibase.Decode(publicKeyMultibase) + if err != nil { + return err + } + + // Convert publicKeyMultibase to bech32 encoded blockchain address + chainId := getChainIdFromBlockchainAccountId(blockchainAccountId) + addressPrefix, isChainSupported := types.CosmosCAIP10ChainIdBech32PrefixMap[chainId] + if !isChainSupported { + return fmt.Errorf( + "cosmos chain with chain-id '%v' in blockchainAccountId '%v' is not supported", + chainId, + blockchainAccountId, + ) + } + convertedAddress := publicKeyToCosmosBech32Address( + addressPrefix, + publicKeyBytes, + ) + + // Compare converted blockchain address + if convertedAddress != getBlockchainAddress(blockchainAccountId) { + return fmt.Errorf( + "blockchain address provided in blockchainAccountId '%v' is unexpected", + blockchainAccountId, + ) } else { return nil } } -// Support for EIP155 based blockchain address -func recoverEthPublicKey(blockchainAccountId string, signature string, documentBytes []byte) error { +// verifyEthereumBlockchainAccountId verifies Ethereum Ecosystem based blockchain address. A secp256k1 based +// publicKey is extracted from the recoverable Secp256k1 signature. It is converted into a hex encoded based +// blockchain address, and matched with user provided blockchain address. If they do not match, error is returned. +func verifyEthereumBlockchainAccountId(extendedVm *types.ExtendedVerificationMethod, documentBytes []byte) error { // Extract blockchain address from blockchain account id - blockchainAddress := getBlockchainAddress(blockchainAccountId) + blockchainAddress := getBlockchainAddress(extendedVm.BlockchainAccountId) // Convert message bytes to hash // More info on the `personal_sign` here: https://docs.metamask.io/guide/signing-data.html#personal-sign msgHash := etheraccounts.TextHash(documentBytes) // Decode hex-encoded signature string to bytes - signatureBytes, err := etherhexutil.Decode(signature) + signatureBytes, err := etherhexutil.Decode(extendedVm.Signature) if err != nil { return err } @@ -142,7 +223,7 @@ func recoverEthPublicKey(blockchainAccountId string, signature string, documentB return err } - // Convert public key to hex-encoded address + // Convert public key to b-encoded address recoveredBlockchainAddress := ethercrypto.PubkeyToAddress(*recoveredPublicKey).Hex() // Match the recovered address against user provided address diff --git a/x/ssi/verification/signature_verification.go b/x/ssi/verification/signature_verification.go index 5d43a49..0ce83c6 100644 --- a/x/ssi/verification/signature_verification.go +++ b/x/ssi/verification/signature_verification.go @@ -16,7 +16,7 @@ func VerifySignatureOfEveryController( } err := verifyAll(vmList, didDoc) if err != nil { - return fmt.Errorf("need every signature for controller %s to be valid", controller) + return fmt.Errorf("%s: need every signature for controller %s to be valid", err.Error(), controller) } } return nil