Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Toncoin support #18

Merged
merged 6 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -139,8 +139,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 @@ -154,7 +156,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.