Exploiting EC-Recover For Efficient Borromean Ring Signatures

Attempting to implement the Borromean Ring Signature algorithm with Schnorr for the EVM results in rather costly transactions. However, the EVM has one pre-compile contract available that executes several point addition and multiplication operations on the classical secp256k1n curve for a very cheap price: ecrecover. This article shows how it can not only be used to validate an ECDSA signature but also, how one may use it as a gadget for other use cases requiring multiple EC addition and multiplication operations.
尝试使用 Schnorr 为 EVM 实现 Borromean 环签名算法会导致相当昂贵的交易。但是,EVM 有一个可用的预编译合约,可以以非常便宜的价格在经典的 secp256k1n 曲线上执行多个点加法和乘法运算: ecrecover .本文展示了它不仅可以用于验证 ECDSA 签名,还可以将其用作需要多个 EC 加法和乘法运算的其他用例的小工具。

The Math 数学

ECDSA Public Key Recovery
ECDSA 公钥恢复

When verifying an ECDSA signature where the public key  (as �=��) of the signer is known, the following equation must hold for the signature to be valid:
在验证签名者的公钥  (as �=�� ) 已知的 ECDSA 签名时,必须满足以下等式才能使签名有效:

�=(ℎ��ℎ(�)⋅�-1)�+(�⋅�-1)�

But, as you might know, the ecrecover pre-compile in the EVM is only passed
但是,您可能知道,EVM 中的 ecrecover 预编译仅通过

  • The keccak256 hash of the transaction/message (ℎ��ℎ(�))
    事务/消息的 keccak256 哈希值 ( ℎ��ℎ(�) )
  • The signature components  and  which are used to derive  (as �=��)
    签名组件  和  用于派生  (作为 �=�� )
  • The signature component 
    签名组件 

The result is the address of the signer – which is a partial hash of public key . The validity is then based on whether the expected signer address is returned. If not, or if the address returned is the zero-address, the signature is considered invalid.
结果是签名者的地址 – 它是公钥  的部分哈希值。然后,有效性基于是否返回预期的签名者地址。如果不是,或者返回的地址为零地址,则认为签名无效。

So obviously, it must be possible to rearrange the equation and solve for :
所以很明显,必须能够重新排列方程并求解  :

(�⋅�-1)�=�-(ℎ��ℎ(�)⋅�-1)�

�=�-(ℎ��ℎ(�)⋅�-1)��⋅�-1

�=��-ℎ��ℎ(�)⋅��

�=(��-ℎ��ℎ(�)⋅�)⋅�-1

Therefore ecrecover basically takes in two scalars ℎ��ℎ(�), and a point  and returns �������(�) which is the (partial) hash of a resulting point from handling the input variables.
因此 ℎ��ℎ(�) , ecrecover 基本上接受两个标量 , , 和一个点,  并返回 �������(�) 处理输入变量的结果点  的(部分)哈希值。

�������(�)=ecrecover(ℎ��ℎ(�),�,�,�)=�������((��-ℎ��ℎ(�)⋅�)⋅�-1)

Exploiting EC-Recover 利用 EC-Recover

This doesn’t seem too dissimilar from what is happening in a single forward-step in the Borromean Ring Signature algorithm:
这似乎与Borromean Ring Signature算法中单个向前步骤中发生的情况没有太大区别:

�+1=ℎ��ℎ(�,(��+��))

So can’t we then exploit this precompile to implement a more efficient EVM-based Borromean Ring Signature validation by
因此,我们难道不能利用这个预编译来实现更高效的基于 EVM 的 Borromean 环签名验证吗?

  • Using the input point  as  (public key of a ring member, with the signer’s private key being  for �=��) by  being the x-coordinate of  and  to derive the correct y-coordinate of 
    使用输入点  作为  (环成员的公钥,签名者的私钥为  ) �=�� 作为  的 x 坐标   ,并得出 
  • Using what is expected to be the hashed transaction/message as  component for the ring
    使用预期的散列事务/消息作为  环的组件
  • Using the ECDSA’s  component as input hash  of the ring
    使用 ECDSA 的  组件作为环  的输入哈希
  • Mixing in the message  by hashing it with the resulting address
    通过将消息与生成的地址进行哈希处理来混合消息 

�+1=ℎ��ℎ(�,ecrecover(�,�,�,�))=ℎ��ℎ(�,�������((��-��)⋅�-1))

Chameleon Hashes within EC-Recover
EC-Recover中的Chameleon Hashes

As before, the signer of the Ring Signature must be able to make use of a Chameleon Hash in order to determine �+1 before knowing what  will be.
和以前一样,环签名的签名者必须能够使用变色龙哈希,以便在知道将是什么  之前确定 �+1 。

We can follow the original pattern of choosing a random scalar . Thankfully, we already know the signer’s public key point , therefore we also know �-1.
我们可以遵循选择随机标量  的原始模式。值得庆幸的是,我们已经知道了签名者的公钥点  ,因此我们也知道 �-1 了。

�+1=ℎ��ℎ(�,�������(��⋅�-1))

After using this to step through the entire ring, we will know what the input  will be. Therefore we can replace  by solving for :
使用它来逐步执行整个环之后,我们将知道输入  的内容。因此,我们可以通过求解来  替换  :

��⋅�-1=(��-��)⋅�-1

��⋅�-1=(��-��)⋅�-1

��=���-��

�⋅�=��⋅�-�⋅�

�=��-�

�=��-�

This resulting  of the signer should then be indistinguishable from the randomly chosen  values of the other ring members.
然后,签名者的这种结果  应该与其他环成员的随机选择  值无法区分。

ℎ��ℎ(�,�������(��⋅�-1))=ℎ��ℎ(�,�������((��-��)⋅�-1))

The Code 守则

Off-Chain Signing 链下签名

from Crypto.Hash import keccak
from ecdsa import ellipticcurve, numbertheory, SECP256k1
import secrets
from eth_abi import encode
from collections import defaultdict

curve = SECP256k1.curve
n = SECP256k1.order
G = SECP256k1.generator

def H(data, encoding):
    return int.from_bytes(keccak.new(digest_bits=256).update(encode(encoding, data)).digest(), 'big') % n

# Signs a message with a structure of rings of members similar to this:
#
# [ [ (P,0), (P,0), (P,x), (P,0) ], [ (P,0), (P,x), (P,0) ] ]
# ^ ^               ^^^^^        ^  ^ ^^^^^               ^ ^
# ^ ^               Signer       ^  ^ Member (unknown x)  ^ ^
# ^ ^                            ^  ^                     ^ ^
# ^ ^---------- Ring 0 ----------^  ^------- Ring 1 ------^ ^
# ^                                                         ^
# ^------------------- Borromean Rings ---------------------^
#
def sign(message, members):
    # Determine v and r values for each public key point P.
    v = [[28 if P.y() & 1 else 27 for P, _ in ring] for ring in members]
    r = [[P.x() for P, _ in ring] for ring in members]
    M = H([message, v, r], ["bytes", "uint8[][]", "uint256[][]"])

    # Create Chameleon Hashes for each signer.
    k = defaultdict(int)
    e = defaultdict(lambda: defaultdict(int))
    signer_idx = defaultdict(int)
    for i, ring in enumerate(members):
        # Random scalar for Chameleon Hash (later replaced by e & s).
        k[i] = secrets.randbelow(n) + 1

        for j, (P, x) in enumerate(ring):
            # Set Chameleon Hash as e of signer.
            if x != 0:
                # e = hash(m, address(kG * r^(-1)))
                R = (k[i] * G) * numbertheory.inverse_mod(r[i][j], n)
                address = '0x' + keccak.new(digest_bits=256).update(R.to_bytes()).digest()[-20:].hex()
                e[i][j + 1] = H([M, address, i, j], ["uint256", "address", "uint8", "uint8"])
                # Remember signer's index in current ring.
                signer_idx[i] = j

    # Determine e for each member after signer.
    s = defaultdict(lambda: defaultdict(int))
    for i, ring in enumerate(members):
        for j in range(signer_idx[i] + 1, len(ring)):
            (P, _) = ring[j]
            # Random scalar s for non-signers.
            s[i][j] = secrets.randbelow(n) + 1
            # e' = hash(m, address((eP - sG) * r^(-1)))
            R = (e[i][j]*P + (-(s[i][j] * G))) * numbertheory.inverse_mod(r[i][j], n)
            address = '0x' + keccak.new(digest_bits=256).update(R.to_bytes()).digest()[-20:].hex()
            e[i][j + 1] = H([M, address, i, j], ["uint256", "address", "uint8", "uint8"])

    # Gather the last e value for each ring (e[i][-1]).
    ring_ends = [e[i][max(e[i].keys())] for i in e]
    # And determine e0 based on each ring's last member.
    e0 = H([ring_ends], ["uint256[]"])

    # Starting from e0, determine e for each member before the signer.
    for i, ring in enumerate(members):
        e[i][0] = e0
        for j in range(signer_idx[i]):
            (P, _) = ring[j]
            # Random scalar s for non-signers.
            s[i][j] = secrets.randbelow(n) + 1
            # e' = hash(m, address((eP - sG) * r^(-1)))
            R = (e[i][j]*P + (-(s[i][j] * G))) * numbertheory.inverse_mod(r[i][j], n)
            address = '0x' + keccak.new(digest_bits=256).update(R.to_bytes()).digest()[-20:].hex()
            e[i][j + 1] = H([M, address, i, j], ["uint256", "address", "uint8", "uint8"])

    # Finally, calculate s for each Chameleon Hash to replace k with.
    for i, ring in enumerate(members):
        j = signer_idx[i]
        (P, x) = ring[j]
        s[i][j] = (e[i][j]*x - k[i]) % n
        # Sanity-check: Hash with s & e should be the same as hash with k.
        R_ = (e[i][j]*P + (-(s[i][j] * G))) * numbertheory.inverse_mod(r[i][j], n)
        address_ = '0x' + keccak.new(digest_bits=256).update(R_.to_bytes()).digest()[-20:].hex()
        e_ = H([M, address_, i, j], ["uint256", "address", "uint8", "uint8"])
        assert e[i][j + 1] == e_

    return (e0, v, r, [ [s[i][j] for j in sorted(s[i])] for i in sorted(s) ])


# ---------------------- Playground ----------------------------------

def member(signer = False):
    x = secrets.randbelow(n) + 1
    P = G*x
    return (P, x if signer else 0)

m = b"hello"
signature = sign(m,  [
    [ member(), member(), member(True),  member() ],
    [ member(), member(True), member() ]
])

print(f'''BorromeanRing.validate(
  m = 0x{m.hex()}
  e0 = {signature[0]}
  v = {signature[1]}
  r = {signature[2]}
  s = {signature[3]}
)''')
$ python borromean-ecdsa.py 
BorromeanRing.validate(
  m = 0x68656c6c6f
  e0 = 109125325252662397704443391788259493773533497479890032494653283252810772602958
  v = [[27, 27, 27, 28], [27, 28, 28]]
  r = [[55150867365147610330436483336757752946760082639320608573853394922858405031248, 44324768931115417252417103419087001014616806521272598659415820542004032154792, 85740849830320470813451766704578545536484323110426743630330214133618289818508, 40093116712223621022063979627270424589535872400915748056121284698792389206752], [67638234765727728523624259252248319317865344202198443209806192542902514194911, 79101705934303499580660335186719424455362081223457855793876495474664908758285, 108775676743731072371387601891326407936759016425842354275036641669646997537653]]
  s = [[57239406502032993091643979135786211342444107443510211006808219707319777747289, 16848866492206211083856507954968088143833099320997232323930353416909551719419, 69888003942361774324397468790806152708741706628862979889227810665815345552676, 85553560707166780066278918897902846075897912973709723370418788677963503376623], [98599600510980798246984761639501715191746992931585718902740505965772955348078, 68672496231270726515305844924296592732589210054688804822920292004641009329066, 65829000216344475900388419547580193157261411351990824781389896139499037837904]]
)

On-Chain Verification 链上验证

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract BorromeanRing {

    uint256 internal constant secp256k1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337;

    function validate(
        bytes calldata m,
        uint256 e0,
        uint8[][] calldata v,
        uint256[][] calldata r,
        uint256[][] calldata s
    ) external pure returns (bool) {
        // M = H(m, P)
        uint256 M = uint256(keccak256(abi.encode(m, v, r))) % secp256k1n;

        uint256[] memory e = new uint256[](v.length);

        // For each Ring i
        for (uint8 i = 0; i < v.length; i++) {
            // Each Ring starts from the same e0
            e[i] = e0;
            // For each Member j
            for (uint8 j = 0; j < v[i].length; j++) {
                require(v[i][j] == 27 || v[i][j] == 28);
                // (Mis-)Use ecrecover as a widget to calculate (eP - sG) * r^(-1)
                // e = hash(m, address((eP - sG) * r^(-1)))
                address R = ecrecover(bytes32(s[i][j]), v[i][j], bytes32(r[i][j]), bytes32(e[i]));
                require(R != address(0x0));
                e[i] = uint256(keccak256(abi.encode(M, R, i, j))) % secp256k1n;
            }
        }

        // Each Ring's last e value build e0
        // e'0 = H(e, e, ...)
        uint256 e0_ = uint256(keccak256(abi.encode(e))) % secp256k1n;

        // The signature is valid if when
        // e0 == e'0
        return e0 == e0_;
    }

}

原文始发于patrickd:Exploiting EC-Recover For Efficient Borromean Ring Signatures

版权声明:admin 发表于 2023年11月2日 下午10:28。
转载请注明:Exploiting EC-Recover For Efficient Borromean Ring Signatures | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...