Skip to content

Commit

Permalink
Add Toncoin support (#18)
Browse files Browse the repository at this point in the history
* add support for toncoin

* add toncoin docs

* make description of solana witness more accurate

* add dummy ToncoinAuth to verify hardcoded toncoin signature
  • Loading branch information
contrun authored Dec 8, 2023
1 parent cb8fc84 commit 899f837
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 2 deletions.
88 changes: 88 additions & 0 deletions c/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions c/ckb_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ enum AuthAlgorithmIdType {
AuthAlgorithmIdSolana = 13,
AuthAlgorithmIdRipple = 14,
AuthAlgorithmIdSecp256R1 = 15,
AuthAlgorithmIdToncoin = 16,
AuthAlgorithmIdOwnerLock = 0xFC,
};

Expand Down
13 changes: 11 additions & 2 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
99 changes: 99 additions & 0 deletions docs/toncoin.md
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 899f837

Please sign in to comment.