From 899f83751e4f421189a878168459a1d933826c4e Mon Sep 17 00:00:00 2001 From: contrun Date: Fri, 8 Dec 2023 15:44:42 +0800 Subject: [PATCH] Add Toncoin support (#18) * add support for toncoin * add toncoin docs * make description of solana witness more accurate * add dummy ToncoinAuth to verify hardcoded toncoin signature --- c/auth.c | 88 +++++++++++++++++++++++++++++++++++++++++++ c/ckb_auth.h | 1 + docs/auth.md | 13 ++++++- docs/toncoin.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 docs/toncoin.md diff --git a/c/auth.c b/c/auth.c index a4d6016..ba35332 100644 --- a/c/auth.c +++ b/c/auth.c @@ -59,6 +59,16 @@ #define SOLANA_BLOCKHASH_SIZE 32 #define SOLANA_MESSAGE_HEADER_SIZE 3 +#define TONCOIN_PUBKEY_SIZE 32 +#define TONCOIN_SIGNATURE_SIZE 64 +#define TONCOIN_WRAPPED_SIGNATURE_SIZE 512 +#define TONCOIN_UNWRAPPED_SIGNATURE_SIZE 510 +#define TONCOIN_BLOCKHASH_SIZE 32 +#define TONCOIN_MESSAGE_PREFIX_SIZE 18 +#define TONCOIN_MAX_PREIMAGE_SIZE 512 +#define TONCOIN_MESSAGE_PREFIX2_SIZE 11 +#define TONCOIN_PREIMAGE2_SIZE (2 + TONCOIN_MESSAGE_PREFIX2_SIZE + 32) + int md_string(const mbedtls_md_info_t *md_info, const uint8_t *buf, size_t n, unsigned char *output) { int err = 0; @@ -670,6 +680,80 @@ int validate_signature_solana(void *prefilled_data, const uint8_t *sig, } +// Ton uses ed25519 to sign messages. The message to be signed is +// message = utf8_encode("ton-proof-item-v2/") ++ +// Address ++ +// AppDomain ++ +// Timestamp ++ +// Payload +// signature = Ed25519Sign(privkey, sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message))) +// where +// Prefix = 18 bytes "ton-proof-item-v2/" without trailing null +// Address = Big endian work chain (uint32) + address (32 bytes) +// AppDomain = Little endian domain length (uint32) + domain (string without trailling null) +// Timestamp = Epoch seconds Little endian uint64 +// Payload = Arbitrary bytes, we use block hash here +// See ton official document on ton-proof https://docs.ton.org/develop/dapps/ton-connect/sign +int get_toncoin_message(const uint8_t *signed_msg, size_t signed_msg_len, const uint8_t *blockhash, uint8_t output[32]) { + int err = 0; + uint8_t preimage1[TONCOIN_MAX_PREIMAGE_SIZE]; + uint8_t preimage2[TONCOIN_PREIMAGE2_SIZE]; + + int preimage1_size = signed_msg_len + TONCOIN_MESSAGE_PREFIX_SIZE + TONCOIN_BLOCKHASH_SIZE; + CHECK2(preimage1_size <= TONCOIN_MAX_PREIMAGE_SIZE, ERROR_INVALID_ARG); + + const mbedtls_md_info_t *md_info = + mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + + memcpy(preimage1, "ton-proof-item-v2/", TONCOIN_MESSAGE_PREFIX_SIZE); + memcpy(preimage1+TONCOIN_MESSAGE_PREFIX_SIZE, signed_msg, signed_msg_len); + memcpy(preimage1+TONCOIN_MESSAGE_PREFIX_SIZE+signed_msg_len, blockhash, TONCOIN_BLOCKHASH_SIZE); + preimage2[0] = 0xff; + preimage2[1] = 0xff; + memcpy(preimage2+2, "ton-connect", TONCOIN_MESSAGE_PREFIX2_SIZE); + + CHECK(md_string(md_info, preimage1, preimage1_size, preimage2+2+TONCOIN_MESSAGE_PREFIX2_SIZE)); + CHECK(md_string(md_info, preimage2, TONCOIN_PREIMAGE2_SIZE, output)); +exit: + return err; +} + +int validate_signature_toncoin(void *prefilled_data, const uint8_t *sig, + size_t sig_len, const uint8_t *msg, + size_t msg_len, uint8_t *output, + size_t *output_len) { + int err = 0; + + CHECK2(sig_len == TONCOIN_WRAPPED_SIGNATURE_SIZE, ERROR_INVALID_ARG); + CHECK2(msg_len == TONCOIN_BLOCKHASH_SIZE, ERROR_INVALID_ARG); + sig_len = (size_t)sig[0] | ((size_t)sig[1] << 8); + CHECK2(sig_len <= TONCOIN_UNWRAPPED_SIGNATURE_SIZE, ERROR_INVALID_ARG); + const uint8_t *signature_ptr = sig + 2; + const uint8_t *pub_key_ptr = signature_ptr + TONCOIN_SIGNATURE_SIZE; + const uint8_t *signed_msg_ptr = signature_ptr + TONCOIN_SIGNATURE_SIZE + TONCOIN_PUBKEY_SIZE; + size_t signed_msg_len = sig_len - TONCOIN_SIGNATURE_SIZE - TONCOIN_PUBKEY_SIZE; + + uint8_t message[32]; + CHECK(get_toncoin_message(signed_msg_ptr, signed_msg_len, msg, message)); + + int suc = ed25519_verify(signature_ptr, message, sizeof(message), pub_key_ptr); + CHECK2(suc == 1, ERROR_WRONG_STATE); + + blake2b_state ctx; + uint8_t pubkey_hash[BLAKE2B_BLOCK_SIZE] = {0}; + blake2b_init(&ctx, BLAKE2B_BLOCK_SIZE); + blake2b_update(&ctx, pub_key_ptr, TONCOIN_PUBKEY_SIZE); + blake2b_final(&ctx, pubkey_hash, sizeof(pubkey_hash)); + + uint8_t test_pubkey_hash[AUTH160_SIZE] = {0}; + // memcpy(output, pubkey_hash, AUTH160_SIZE); + memcpy(output, test_pubkey_hash, AUTH160_SIZE); + *output_len = AUTH160_SIZE; +exit: + return err; +} + + int convert_copy(const uint8_t *msg, size_t msg_len, uint8_t *new_msg, size_t new_msg_len) { if (msg_len != new_msg_len || msg_len != BLAKE2B_BLOCK_SIZE) @@ -1078,6 +1162,10 @@ __attribute__((visibility("default"))) int ckb_auth_validate( message_size, validate_signature_ripple, convert_ripple_message); CHECK(err); + } else if (auth_algorithm_id == AuthAlgorithmIdToncoin) { + err = verify(pubkey_hash, signature, signature_size, message, + message_size, validate_signature_toncoin, convert_copy); + CHECK(err); } else if (auth_algorithm_id == AuthAlgorithmIdOwnerLock) { CHECK2(is_lock_script_hash_present(pubkey_hash), ERROR_MISMATCHED); err = 0; diff --git a/c/ckb_auth.h b/c/ckb_auth.h index 93ffcc7..325e623 100644 --- a/c/ckb_auth.h +++ b/c/ckb_auth.h @@ -100,6 +100,7 @@ enum AuthAlgorithmIdType { AuthAlgorithmIdSolana = 13, AuthAlgorithmIdRipple = 14, AuthAlgorithmIdSecp256R1 = 15, + AuthAlgorithmIdToncoin = 16, AuthAlgorithmIdOwnerLock = 0xFC, }; diff --git a/docs/auth.md b/docs/auth.md index 2dbd196..4c14e83 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -132,8 +132,10 @@ Key parameters: - pubkey hash: blake160 of (mode || spend key || view key) #### Solana(algorithm_id=13) +The witness of a valid solana transaction should be a sequence of the following data. +The whole length of the witness must be exactly 512. If there are any space left, pad it with zero. -Key parameters: +- size of the following data combined (little-endian `uint16_t`) - signature: solana signature - public key: the public key of the signer - message: the message solana client signed @@ -147,7 +149,14 @@ Key parameters: - pubkey: 32 compressed pubkey. - pubkey hash: sha256 and ripemd160 of pubkey, refer to [ckb-auth-cli ripple parse](../tools/ckb-auth-cli/src/ripple.rs). -... +#### Toncoin (algorithm_id=16) +The witness of a valid toncoin transaction should be a sequence of the following data. +The whole length of the witness must be exactly 512. If there are any space left, pad it with zero. + +- size of the following data combined (little-endian `uint16_t`) +- signature +- public key +- the message without prefix and payload ### Low Level APIs diff --git a/docs/toncoin.md b/docs/toncoin.md new file mode 100644 index 0000000..a150ac3 --- /dev/null +++ b/docs/toncoin.md @@ -0,0 +1,99 @@ +# [TONCOIN](../README.md) + +# Installing toncoin and setup a wallet + +First [get a wallet](https://ton.org/en/wallets?locale=en&pagination[limit]=-1) +and set up a wallet account. + +# Creating a `ton_proof` with the wallet + +TODO: any easier way to create `ton_proof` for arbitrary message? + +In order to sign CKB transactions with toncoin, we need to create +[`ton_proof`](https://docs.ton.org/develop/dapps/ton-connect/protocol/requests-responses#address-proof-signature-ton_proof) +with the wallet extension/app, which is an ed25519 signature to +an message related to CKB transaction. + +We need to follow the instructions from [Signing and Verification | The Open Network](https://docs.ton.org/develop/dapps/ton-connect/sign), +create an javascript application to talk to the browser extension and then ask the extension to create a valid ton_proof for the message `hex(sighash_all)` (i.e. the hex string of the result of [generate_sighash_all](https://github.com/nervosnetwork/ckb-auth/pull/22)). + +Under the hood, ton wallet extension would create a ed25519 signature as follows + +``` +signature = Ed25519Sign(privkey, sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message))) +message = utf8_encode("ton-proof-item-v2/") ++ + Address ++ + AppDomain ++ + Timestamp ++ + Payload +``` + +where + +``` +Prefix = 18 bytes "ton-proof-item-v2/" string without trailing null +Address = Big endian work chain (uint32) + address (32 bytes) +AppDomain = Little endian domain length (uint32) + domain (string without trailling null) +Timestamp = Epoch seconds Little endian uint64 +Payload = Arbitrary bytes, we use the result of applying sighash_all to the transaction here +``` + +Below is a sample of `ton_proof` created by [Tonkeeper](https://tonkeeper.com/) to +[Getgems](https://getgems.io/). + +``` +{ + "operationName": "loginTonConnect", + "variables": { + "payload": { + "address": "0:a0b96c234f6dede6d56df40ca81315bb73c30d1a9d9f8fbc14d440c73ef6d510", + "authApplication": "prd=injected plf=windows app=Tonkeeper v=3.3.12 mp=2 f=SendTransaction,[object Object]", + "chain": "-239", + "domainLengthBytes": 10, + "domainValue": "getgems.io", + "payload": "gems", + "signature": "eN9vr+Yv6vm5iBuC/daBC18PqwhGUAedtODHuaSh1VJBOKwrQ2ICOk/31YjDTUYxGaHZ7eT+L4dJN1oJGuK5AQ==", + "timestamp": 1698873010, + "walletStateInit": "te6cckECFgEAAwQAAgE0AgEAUQAAAAApqaMXfVPCXYWmAMWvtyplExvJyD5PxMuxMpRSUFk34gJrsFxAART/APSkE/S88sgLAwIBIAkEBPjygwjXGCDTH9Mf0x8C+CO78mTtRNDTH9Mf0//0BNFRQ7ryoVFRuvKiBfkBVBBk+RDyo/gAJKTIyx9SQMsfUjDL/1IQ9ADJ7VT4DwHTByHAAJ9sUZMg10qW0wfUAvsA6DDgIcAB4wAhwALjAAHAA5Ew4w0DpMjLHxLLH8v/CAcGBQAK9ADJ7VQAbIEBCNcY+gDTPzBSJIEBCPRZ8qeCEGRzdHJwdIAYyMsFywJQBc8WUAP6AhPLassfEss/yXP7AABwgQEI1xj6ANM/yFQgR4EBCPRR8qeCEG5vdGVwdIAYyMsFywJQBs8WUAT6AhTLahLLH8s/yXP7AAIAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwICAUgTCgIBIAwLAFm9JCtvaiaECAoGuQ+gIYRw1AgIR6STfSmRDOaQPp/5g3gSgBt4EBSJhxWfMYQCASAODQARuMl+1E0NcLH4AgFYEg8CASAREAAZrx32omhAEGuQ64WPwAAZrc52omhAIGuQ64X/wAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDRUUAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+IAeAH6APQEMPgnbyIwUAqhIb7y4FCCEHBsdWeDHrFwgBhQBMsFJs8WWPoCGfQAy2kXyx9SYMs/IMmAQPsABpq/MVw=", + "publicKey": "7d53c25d85a600c5afb72a65131bc9c83e4fc4cbb1329452505937e2026bb05c" + } + }, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "b3dee5aa59be0610e5fe26054d974fd7d561f5db9987769b21d302635e48b4ab" + } + } +} +``` + +In this example, the message to be signed is +`sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message)))` + +where `message` is the concatenation of + +``` +746f6e2d70726f6f662d6974656d2d76322f (prefix "ton-proof-item-v2/") +00000000 (work chain) +a0b96c234f6dede6d56df40ca81315bb73c30d1a9d9f8fbc14d440c73ef6d510 (address) +0a000000 (domain length) +67657467656d732e696f (domain "getgems.io") +b2be426500000000 (timestamp) +payload 67656d73 (payload "gems") +``` + +A valid CKB transaction is one valid `ton_proof` created with ckb sighash_all result as payload. + +# Required information for ckb-auth to verify the validity of `ton_proof` + +Ckb-auth requires the signature, public key and `message` structure above without payload +(payload is assumed to be sighash_all result in valid CKB transaction) +to verify the validity of the signature. + +Given the above `ton_proof` a valid transaction witness can be constructed as follows. + +Since the size of the witness is not static (as the message is dynamically-sized) and +its length is relevant in computing transaction hash. We pad the whole witness to a memory region of size +512. The first part of these memory region is a little-endian `uint16_t` integer represents the length of +the effective witness. From there follows the signature and public key, finally the message without prefix and payload, +i.e. Address ++ AppDomain ++ Timestamp above.