Skip to content

摘要

使用钱包密钥对非交易数据进行安全签名。

动机

在TON生态系统中,用户的钱包不仅用于执行资产转移,还充当链上和链下应用程序的通用标识符。

通常,钱包用于发送转移币的TON消息、更改代币所有权和与其他智能合约交互。在所有这些情况下,客户端应用程序签署一个可以通过钱包公钥验证的格式良好的交易。

提议的协议解决了钱包公钥验证在链下应用程序和用户钱包以外的链上合约中使用的任意数据(即,非交易数据)的场景。

示例

  1. 为链下服务证明地址所有权。
  2. 签署一个结构化消息,以在TON智能合约(TVM内部)中进行验证。

安全哈希

为了签署TON的交易(或仅仅是一个单元),需要创建单元表示,对其进行哈希然后签名。我们的目标是创建一个签名方案,保证不会与已签名的单元发生冲突,以便签名的数据消息不能作为钱包的交易使用。

我们观察到钱包智能合约(FunC)中的以下实现,其中签名是对等于任意单元的SHA256哈希的256位消息进行计算的:

check_signature(slice_hash(in_msg), signature, public_key)

在本提案中,我们将对352位消息进行签名,该消息由模式标识符、时间戳和有效负载单元X的哈希组成。

此签名通过消息长度的不同实现与钱包交易的域分离。 数据消息的域分离通过模式标识符和有效负载X的各种布局提供。

规范

X为任意有效负载单元。有效负载单元的布局使用TL-B指定。

timestamp为签名时签名者设备上的UNIX时间戳(自1970年1月1日00:00:00 UTC起的秒数)。

schema_crc为指定有效负载X布局和语义的TL-B方案的4字节CRC32。

为了签署不能作为区块链交易使用的任意单元,我们按以下方式计算签名数据:

ed25519(uint32be(schema_crc) ++ uint64be(timestamp) ++ cell_hash(X), privkey)

在JS中:

js
let X: Cell;                       // 有效负载单元
let prefix = Buffer.alloc(4 + 8);  // 版本 + 时间戳
prefix.writeUint32BE(schema_crc);
prefix.writeUint64BE(timestamp);
let signature = Ed25519Sign(Buffer.concat([prefix, X.hash()]), privkey);

在FunC中:

cell X;  ;; 有效负载单元
var m = begin_cell()
                 .store_uint(schema_crc, 32)
                 .store_uint(timestamp, 64)
                 .store_uint(cell_hash(x), 256)
                 .end_cell();
var is_valid = check_data_signature(begin_parse(m), sig, pk)

将签名数据传输到智能合约中

本规范推荐以下签名数据的包装方式:

Cell {
  bits: signature (512 bits) ++ schema_crc (32 bits) ++ timestamp (64 bits)
  refs: [ X ]
}

有效负载验证

为了用户的安全,每个签名都应该绑定到特定的地点时间。请注意,所有签名都由同一个钱包密钥生成,每个应用程序、服务或智能合约必须为自己强制执行域分离。

模式CRC表示有效负载单元的布局,从而定义域分离。应用程序应验证模式版本值,并拒绝不支持模式的签名。

有效负载单元包含按其TL-B定义的任意数据,供应用程序验证和解释。

时间戳将签名绑定到用户本地时钟的签名时间。应用程序必须根据其内部TTL参数拒绝过期的签名。

标准模式版本

短纯文本消息

此模式用于使用_蛇形格式_(根据TEP-64)签署UTF-8文本消息。

TL-B:

plaintext text:Text = PayloadCell;

// From TEP-64:
tail#_ {bn:#} b:(bits bn) = SnakeData ~0;
cons#_ {bn:#} {n:#} b:(bits bn) next:^(SnakeData ~n) = SnakeData ~(n + 1);
text#_ {n:#} data:(SnakeData ~n) = Text;

模式:

crc32('plaintext text:Text = PayloadCell') = 0x754bf91b

钱包必须向用户显示文本字符串。

应用程序必须验证签名字符串的内容,以强制执行域分离。

应用程序绑定

此模式允许为目标应用程序签署二进制数据,该应用程序由TON.DNS名称、合约地址或两者标识。

TL-B:

app_data address:(Maybe MsgAddress) domain:(Maybe ^Text) data:^Cell ext:(Maybe ^Cell) = PayloadCell;

// From TEP-64:
tail#_ {bn:#} b:(bits bn) = SnakeData ~0;
cons#_ {bn:#} {n:#} b:(bits bn) next:^(SnakeData ~n) = SnakeData ~(n + 1);
text#_ {n:#} data:(SnakeData ~n) = Text;

其中:

  • address 是接收签名消息的可选合约地址;
  • domain 是反向零分隔格式的完全限定TON.DNS域名(例如ton\0example\0myapp\0表示myapp.example.ton);
  • data 包含应用程序特定数据;
  • ext 是用于未来扩展的单元;

模式:

crc32('app_data address:(Maybe MsgAddress) domain:(Maybe ^Text) data:^Cell ext:(Maybe ^Cell) = PayloadCell')
    = 0x54b58535

钱包必须拒绝没有指定域名或地址的请求。

钱包必须向用户显示包含在签名中的合约地址。

钱包必须向用户显示TON.DNS名称(如果包含在签名中)并验证请求来自该DNS记录的当前所有者。 请求来源的验证不在本规范范围内。

钱包必须在签名前向用户显示dataext的内容。

TON合约必须验证消息中包含地址,并且地址与目标合约的地址匹配。

应用程序必须验证签名名称是否与其名称匹配。合约在适用的情况下也可以执行相同的检查。

应用程序可以显示data字段的可读含义,如果他们知道其布局对于给定的智能合约address或由domain指示的服务。

缺点

理由和替代方案

在单元中编码数据

链下应用程序通常不需要处理TON单元,但我们建议使用一种在TVM内部也可用的编码,以保持规范简单。

将签名绑定到当前时间戳

Schnorr签名(如Ed25519)设计上是不可否认且可重放的。为了避免长期漏洞(例如,当密钥泄露时),所有签名必须绑定到合理的最短时间窗口。例如,单方实时使用几分钟,或者多方收集签名时几小时或几天。

为什么时间戳不是有效负载的一部分?

时间戳绑定几乎在每个协议中都需要,因此它可以节省有效负载单元中的应用程序特定数据空间。

为什么域名在另一个单元中?

域名被声明为^Text以允许修剪并存储其单元哈希。

关于域分离的注意事项

用户的安全依赖于钱包和应用程序之间的合作,以强制执行域分离

应用程序强制执行域分离以拒绝来自其他域的签名。

钱包强制执行域分离以保护用户不被无意中为另一个应用程序签名。

如何使用合约地址绑定

钱包可以显示已知合约的名称和图标,甚至可以将参数解释为特定操作。

如何使用TON.DNS绑定

将签名绑定到TON.DNS名称允许钱包进行实时验证,确保请求由命名服务签名。这几乎消除了钓鱼的可能性,因为服务控制整个身份验证流程。即使用户不注意确认窗口中的文本,也不可能在未经用户同意的情况下欺骗用户在该服务上确认操作。

如何在签名前显示数据

plaintext模式中,钱包直接显示UTF-8文本。

app_data模式中,钱包以二进制形式显示dataext单元的内容(例如,以十六进制)。此协议的未来扩展可能会在ext字段中添加布局描述,以人类可读的形式描述data的内容。

先前的艺术

本提案是以太坊EIP-1271的简化版本。

最初受Steve Korshakov安全签名提案启发, 但我们观察到有可能避免不必要的哈希层和构建无效的单元表示, 因为TVM支持对任意大小的消息进行签名检查。

未解决的问题

未来的可能性

协议的未来扩展可能会指定有效负载数据的布局和语义(例如,附加一个TL-B方案)。 这将使钱包显示要签名消息的结构化人类可读形式。