You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
494 lines
16 KiB
494 lines
16 KiB
"""
|
|
Multilayer Linkable Spontaneous Anonymous Group (MLSAG)
|
|
Optimized versions with incremental hashing.
|
|
|
|
See https://eprint.iacr.org/2015/1098.pdf for details.
|
|
Also explained in From Zero to Monero section 3.3 and 5.
|
|
|
|
----------
|
|
|
|
Please note, that the MLSAG code is written in a generic manner,
|
|
where it is designed for multiple public keys (aka inputs). In another
|
|
words, MLSAG should be used to sign multiple inputs, but that is currently
|
|
not the case of Monero, where the inputs are signed one by one.
|
|
So the public keys matrix has always two rows (one for public keys,
|
|
one for commitments), although the algorithm is designed for `n` rows.
|
|
|
|
This has one unfortunate effect where `rows` is always equal to 2 and
|
|
dsRows always to 1, but the algorithm is still written as the numbers
|
|
are arbitrary. That's why there are loops such as `for i in range(dsRows)`
|
|
where it is run only once currently.
|
|
|
|
----------
|
|
|
|
Also note, that the matrix of public keys is indexed by columns first.
|
|
This is because the code was ported from the official Monero client,
|
|
which is written in C++ and where it does have some memory advantages.
|
|
|
|
For ring size = 3 and one input the matrix M will look like this:
|
|
|------------------------|------------------------|------------------------|
|
|
| public key 0 | public key 1 | public key 2 |
|
|
| cmt 0 - pseudo_out cmt | cmt 1 - pseudo_out cmt | cmt 2 - pseudo_out cmt |
|
|
|
|
and `sk` is equal to:
|
|
|--------------|-----------------------------------------------------|
|
|
| private key* | input secret key's mask - pseudo_out's mask (alpha) |
|
|
|
|
* corresponding to one of the public keys (`index` denotes which one)
|
|
|
|
----------
|
|
|
|
Mostly ported from official Monero client, but also inspired by Mininero.
|
|
Author: Dusan Klinec, ph4r05, 2018
|
|
"""
|
|
|
|
import gc
|
|
from typing import TYPE_CHECKING
|
|
|
|
from apps.monero.xmr import crypto
|
|
from apps.monero.xmr.serialize import int_serialize
|
|
|
|
if TYPE_CHECKING:
|
|
from apps.monero.xmr.types import Ge25519, Sc25519
|
|
from apps.monero.xmr.serialize_messages.tx_ct_key import CtKey
|
|
from trezor.messages import MoneroRctKeyPublic
|
|
|
|
KeyM = list[list[bytes]]
|
|
|
|
|
|
_HASH_KEY_CLSAG_ROUND = b"CLSAG_round\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
_HASH_KEY_CLSAG_AGG_0 = b"CLSAG_agg_0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
_HASH_KEY_CLSAG_AGG_1 = b"CLSAG_agg_1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
|
|
|
|
def generate_mlsag_simple(
|
|
message: bytes,
|
|
pubs: list[MoneroRctKeyPublic],
|
|
in_sk: CtKey,
|
|
a: Sc25519,
|
|
cout: Ge25519,
|
|
index: int,
|
|
mg_buff: list[bytes],
|
|
) -> list[bytes]:
|
|
"""
|
|
MLSAG for RctType.Simple
|
|
:param message: the full message to be signed (actually its hash)
|
|
:param pubs: vector of MoneroRctKey; this forms the ring; point values in encoded form; (dest, mask) = (P, C)
|
|
:param in_sk: CtKey; spending private key with input commitment mask (original); better_name: input_secret_key
|
|
:param a: mask from the pseudo output commitment; better name: pseudo_out_alpha
|
|
:param cout: pseudo output commitment; point, decoded; better name: pseudo_out_c
|
|
:param index: specifies corresponding public key to the `in_sk` in the pubs array
|
|
:param mg_buff: buffer to store the signature to
|
|
"""
|
|
# Monero signs inputs separately, so `rows` always equals 2 (pubkey, commitment)
|
|
# and `dsRows` is always 1 (denotes where the pubkeys "end")
|
|
rows = 2
|
|
dsRows = 1
|
|
cols = len(pubs)
|
|
if cols == 0:
|
|
raise ValueError("Empty pubs")
|
|
|
|
sk = _key_vector(rows)
|
|
M = _key_matrix(rows, cols)
|
|
|
|
sk[0] = in_sk.dest
|
|
sk[1] = crypto.sc_sub(in_sk.mask, a)
|
|
tmp_pt = crypto.new_point()
|
|
|
|
for i in range(cols):
|
|
crypto.point_sub_into(
|
|
tmp_pt, crypto.decodepoint_into(tmp_pt, pubs[i].commitment), cout
|
|
)
|
|
|
|
M[i][0] = pubs[i].dest
|
|
M[i][1] = crypto.encodepoint(tmp_pt)
|
|
pubs[i] = None
|
|
|
|
del pubs
|
|
gc.collect()
|
|
|
|
return generate_mlsag(message, M, sk, index, dsRows, mg_buff)
|
|
|
|
|
|
def gen_mlsag_assert(pk: KeyM, xx: list[Sc25519], index: int, dsRows: int):
|
|
"""
|
|
Conditions check
|
|
"""
|
|
cols = len(pk)
|
|
if cols <= 1:
|
|
raise ValueError("Cols == 1")
|
|
if index >= cols:
|
|
raise ValueError("Index out of range")
|
|
|
|
rows = len(pk[0])
|
|
if rows == 0:
|
|
raise ValueError("Empty pk")
|
|
|
|
for i in range(cols):
|
|
if len(pk[i]) != rows:
|
|
raise ValueError("pk is not rectangular")
|
|
if len(xx) != rows:
|
|
raise ValueError("Bad xx size")
|
|
if dsRows > rows:
|
|
raise ValueError("Bad dsRows size")
|
|
return rows, cols
|
|
|
|
|
|
def generate_first_c_and_key_images(
|
|
message: bytes,
|
|
pk: KeyM,
|
|
xx: list[Sc25519],
|
|
index: int,
|
|
dsRows: int,
|
|
rows: int,
|
|
cols: int,
|
|
) -> tuple[Sc25519, list[Ge25519], list[Ge25519]]:
|
|
"""
|
|
MLSAG computation - the part with secret keys
|
|
:param message: the full message to be signed (actually its hash)
|
|
:param pk: matrix of public keys and commitments
|
|
:param xx: input secret array composed of a private key and commitment mask
|
|
:param index: specifies corresponding public key to the `xx`'s private key in the `pk` array
|
|
:param dsRows: row number where the pubkeys "end" (and commitments follow)
|
|
:param rows: total number of rows
|
|
:param cols: size of ring
|
|
"""
|
|
II = _key_vector(dsRows)
|
|
alpha = _key_vector(rows)
|
|
|
|
tmp_buff = bytearray(32)
|
|
Hi = crypto.new_point()
|
|
aGi = crypto.new_point()
|
|
aHPi = crypto.new_point()
|
|
hasher = _hasher_message(message)
|
|
|
|
for i in range(dsRows):
|
|
# this is somewhat extra as compared to the Ring Confidential Tx paper
|
|
# see footnote in From Zero to Monero section 3.3
|
|
hasher.update(pk[index][i])
|
|
|
|
crypto.hash_to_point_into(Hi, pk[index][i])
|
|
alpha[i] = crypto.random_scalar()
|
|
# L = alpha_i * G
|
|
crypto.scalarmult_base_into(aGi, alpha[i])
|
|
# Ri = alpha_i * H(P_i)
|
|
crypto.scalarmult_into(aHPi, Hi, alpha[i])
|
|
# key image
|
|
II[i] = crypto.scalarmult(Hi, xx[i])
|
|
_hash_point(hasher, aGi, tmp_buff)
|
|
_hash_point(hasher, aHPi, tmp_buff)
|
|
|
|
for i in range(dsRows, rows):
|
|
alpha[i] = crypto.random_scalar()
|
|
# L = alpha_i * G
|
|
crypto.scalarmult_base_into(aGi, alpha[i])
|
|
# for some reasons we omit calculating R here, which seems
|
|
# contrary to the paper, but it is in the Monero official client
|
|
# see https://github.com/monero-project/monero/blob/636153b2050aa0642ba86842c69ac55a5d81618d/src/ringct/rctSigs.cpp#L191
|
|
hasher.update(pk[index][i])
|
|
_hash_point(hasher, aGi, tmp_buff)
|
|
|
|
# the first c
|
|
c_old = hasher.digest()
|
|
c_old = crypto.decodeint(c_old)
|
|
return c_old, II, alpha
|
|
|
|
|
|
def generate_mlsag(
|
|
message: bytes,
|
|
pk: KeyM,
|
|
xx: list[Sc25519],
|
|
index: int,
|
|
dsRows: int,
|
|
mg_buff: list[bytes],
|
|
) -> list[bytes]:
|
|
"""
|
|
Multilayered Spontaneous Anonymous Group Signatures (MLSAG signatures)
|
|
|
|
:param message: the full message to be signed (actually its hash)
|
|
:param pk: matrix of public keys and commitments
|
|
:param xx: input secret array composed of a private key and commitment mask
|
|
:param index: specifies corresponding public key to the `xx`'s private key in the `pk` array
|
|
:param dsRows: separates pubkeys from commitment
|
|
:param mg_buff: mg signature buffer
|
|
"""
|
|
rows, cols = gen_mlsag_assert(pk, xx, index, dsRows)
|
|
rows_b_size = int_serialize.uvarint_size(rows)
|
|
|
|
# Preallocation of the chunked buffer, len + cols + cc
|
|
for _ in range(1 + cols + 1):
|
|
mg_buff.append(None)
|
|
|
|
mg_buff[0] = int_serialize.dump_uvarint_b(cols)
|
|
cc = crypto.new_scalar() # rv.cc
|
|
c = crypto.new_scalar()
|
|
L = crypto.new_point()
|
|
R = crypto.new_point()
|
|
Hi = crypto.new_point()
|
|
|
|
# calculates the "first" c, key images and random scalars alpha
|
|
c_old, II, alpha = generate_first_c_and_key_images(
|
|
message, pk, xx, index, dsRows, rows, cols
|
|
)
|
|
|
|
i = (index + 1) % cols
|
|
if i == 0:
|
|
crypto.sc_copy(cc, c_old)
|
|
|
|
ss = [crypto.new_scalar() for _ in range(rows)]
|
|
tmp_buff = bytearray(32)
|
|
|
|
while i != index:
|
|
hasher = _hasher_message(message)
|
|
|
|
# Serialize size of the row
|
|
mg_buff[i + 1] = bytearray(rows_b_size + 32 * rows)
|
|
int_serialize.dump_uvarint_b_into(rows, mg_buff[i + 1])
|
|
|
|
for x in ss:
|
|
crypto.random_scalar(x)
|
|
|
|
for j in range(dsRows):
|
|
# L = rv.ss[i][j] * G + c_old * pk[i][j]
|
|
crypto.add_keys2_into(
|
|
L, ss[j], c_old, crypto.decodepoint_into(Hi, pk[i][j])
|
|
)
|
|
crypto.hash_to_point_into(Hi, pk[i][j])
|
|
|
|
# R = rv.ss[i][j] * H(pk[i][j]) + c_old * Ip[j]
|
|
crypto.add_keys3_into(R, ss[j], Hi, c_old, II[j])
|
|
|
|
hasher.update(pk[i][j])
|
|
_hash_point(hasher, L, tmp_buff)
|
|
_hash_point(hasher, R, tmp_buff)
|
|
|
|
for j in range(dsRows, rows):
|
|
# again, omitting R here as discussed above
|
|
crypto.add_keys2_into(
|
|
L, ss[j], c_old, crypto.decodepoint_into(Hi, pk[i][j])
|
|
)
|
|
hasher.update(pk[i][j])
|
|
_hash_point(hasher, L, tmp_buff)
|
|
|
|
for si in range(rows):
|
|
crypto.encodeint_into(mg_buff[i + 1], ss[si], rows_b_size + 32 * si)
|
|
|
|
crypto.decodeint_into(c, hasher.digest())
|
|
crypto.sc_copy(c_old, c)
|
|
pk[i] = None
|
|
i = (i + 1) % cols
|
|
|
|
if i == 0:
|
|
crypto.sc_copy(cc, c_old)
|
|
gc.collect()
|
|
|
|
del II
|
|
|
|
# Finalizing rv.ss by processing rv.ss[index]
|
|
mg_buff[index + 1] = bytearray(rows_b_size + 32 * rows)
|
|
int_serialize.dump_uvarint_b_into(rows, mg_buff[index + 1])
|
|
for j in range(rows):
|
|
crypto.sc_mulsub_into(ss[j], c, xx[j], alpha[j])
|
|
crypto.encodeint_into(mg_buff[index + 1], ss[j], rows_b_size + 32 * j)
|
|
|
|
# rv.cc
|
|
mg_buff[-1] = crypto.encodeint(cc)
|
|
return mg_buff
|
|
|
|
|
|
def generate_clsag_simple(
|
|
message: bytes,
|
|
pubs: list[MoneroRctKeyPublic],
|
|
in_sk: CtKey,
|
|
a: Sc25519,
|
|
cout: Ge25519,
|
|
index: int,
|
|
mg_buff: list[bytes],
|
|
) -> list[bytes]:
|
|
"""
|
|
CLSAG for RctType.Simple
|
|
https://eprint.iacr.org/2019/654.pdf
|
|
|
|
Corresponds to proveRctCLSAGSimple in rctSigs.cpp
|
|
|
|
:param message: the full message to be signed (actually its hash)
|
|
:param pubs: vector of MoneroRctKey; this forms the ring; point values in encoded form; (dest, mask) = (P, C)
|
|
:param in_sk: CtKey; spending private key with input commitment mask (original); better_name: input_secret_key
|
|
:param a: mask from the pseudo output commitment; better name: pseudo_out_alpha
|
|
:param cout: pseudo output commitment; point, decoded; better name: pseudo_out_c
|
|
:param index: specifies corresponding public key to the `in_sk` in the pubs array
|
|
:param mg_buff: buffer to store the signature to
|
|
"""
|
|
cols = len(pubs)
|
|
if cols == 0:
|
|
raise ValueError("Empty pubs")
|
|
|
|
P = _key_vector(cols)
|
|
C_nonzero = _key_vector(cols)
|
|
p = in_sk.dest
|
|
z = crypto.sc_sub(in_sk.mask, a)
|
|
|
|
for i in range(cols):
|
|
P[i] = pubs[i].dest
|
|
C_nonzero[i] = pubs[i].commitment
|
|
pubs[i] = None
|
|
|
|
del pubs
|
|
gc.collect()
|
|
|
|
return _generate_clsag(message, P, p, C_nonzero, z, cout, index, mg_buff)
|
|
|
|
|
|
def _generate_clsag(
|
|
message: bytes,
|
|
P: list[bytes],
|
|
p: Sc25519,
|
|
C_nonzero: list[bytes],
|
|
z: Sc25519,
|
|
Cout: Ge25519,
|
|
index: int,
|
|
mg_buff: list[bytes],
|
|
) -> list[bytes]:
|
|
sI = crypto.new_point() # sig.I
|
|
sD = crypto.new_point() # sig.D
|
|
sc1 = crypto.new_scalar() # sig.c1
|
|
a = crypto.random_scalar()
|
|
H = crypto.new_point()
|
|
D = crypto.new_point()
|
|
Cout_bf = crypto.encodepoint(Cout)
|
|
|
|
tmp_sc = crypto.new_scalar()
|
|
tmp = crypto.new_point()
|
|
tmp_bf = bytearray(32)
|
|
|
|
crypto.hash_to_point_into(H, P[index])
|
|
crypto.scalarmult_into(sI, H, p) # I = p*H
|
|
crypto.scalarmult_into(D, H, z) # D = z*H
|
|
crypto.sc_mul_into(tmp_sc, z, crypto.sc_inv_eight()) # 1/8*z
|
|
crypto.scalarmult_into(sD, H, tmp_sc) # sig.D = 1/8*z*H
|
|
sD = crypto.encodepoint(sD)
|
|
|
|
hsh_P = crypto.get_keccak() # domain, I, D, P, C, C_offset
|
|
hsh_C = crypto.get_keccak() # domain, I, D, P, C, C_offset
|
|
hsh_P.update(_HASH_KEY_CLSAG_AGG_0)
|
|
hsh_C.update(_HASH_KEY_CLSAG_AGG_1)
|
|
|
|
def hsh_PC(x):
|
|
nonlocal hsh_P, hsh_C
|
|
hsh_P.update(x)
|
|
hsh_C.update(x)
|
|
|
|
for x in P:
|
|
hsh_PC(x)
|
|
|
|
for x in C_nonzero:
|
|
hsh_PC(x)
|
|
|
|
hsh_PC(crypto.encodepoint_into(tmp_bf, sI))
|
|
hsh_PC(sD)
|
|
hsh_PC(Cout_bf)
|
|
mu_P = crypto.decodeint(hsh_P.digest())
|
|
mu_C = crypto.decodeint(hsh_C.digest())
|
|
|
|
del (hsh_PC, hsh_P, hsh_C)
|
|
c_to_hash = crypto.get_keccak() # domain, P, C, C_offset, message, aG, aH
|
|
c_to_hash.update(_HASH_KEY_CLSAG_ROUND)
|
|
for i in range(len(P)):
|
|
c_to_hash.update(P[i])
|
|
for i in range(len(P)):
|
|
c_to_hash.update(C_nonzero[i])
|
|
c_to_hash.update(Cout_bf)
|
|
c_to_hash.update(message)
|
|
|
|
chasher = c_to_hash.copy()
|
|
crypto.scalarmult_base_into(tmp, a)
|
|
chasher.update(crypto.encodepoint_into(tmp_bf, tmp)) # aG
|
|
crypto.scalarmult_into(tmp, H, a)
|
|
chasher.update(crypto.encodepoint_into(tmp_bf, tmp)) # aH
|
|
c = crypto.decodeint(chasher.digest())
|
|
del (chasher, H)
|
|
|
|
L = crypto.new_point()
|
|
R = crypto.new_point()
|
|
c_p = crypto.new_scalar()
|
|
c_c = crypto.new_scalar()
|
|
i = (index + 1) % len(P)
|
|
if i == 0:
|
|
crypto.sc_copy(sc1, c)
|
|
|
|
mg_buff.append(int_serialize.dump_uvarint_b(len(P)))
|
|
for _ in range(len(P)):
|
|
mg_buff.append(bytearray(32))
|
|
|
|
while i != index:
|
|
crypto.random_scalar(tmp_sc)
|
|
crypto.encodeint_into(mg_buff[i + 1], tmp_sc)
|
|
|
|
crypto.sc_mul_into(c_p, mu_P, c)
|
|
crypto.sc_mul_into(c_c, mu_C, c)
|
|
|
|
# L = tmp_sc * G + c_P * P[i] + c_c * C[i]
|
|
crypto.add_keys2_into(L, tmp_sc, c_p, crypto.decodepoint_into(tmp, P[i]))
|
|
crypto.decodepoint_into(tmp, C_nonzero[i]) # C = C_nonzero - Cout
|
|
crypto.point_sub_into(tmp, tmp, Cout)
|
|
crypto.scalarmult_into(tmp, tmp, c_c)
|
|
crypto.point_add_into(L, L, tmp)
|
|
|
|
# R = tmp_sc * HP + c_p * I + c_c * D
|
|
crypto.hash_to_point_into(tmp, P[i])
|
|
crypto.add_keys3_into(R, tmp_sc, tmp, c_p, sI)
|
|
crypto.point_add_into(R, R, crypto.scalarmult_into(tmp, D, c_c))
|
|
|
|
chasher = c_to_hash.copy()
|
|
chasher.update(crypto.encodepoint_into(tmp_bf, L))
|
|
chasher.update(crypto.encodepoint_into(tmp_bf, R))
|
|
crypto.decodeint_into(c, chasher.digest())
|
|
|
|
P[i] = None
|
|
C_nonzero[i] = None
|
|
|
|
i = (i + 1) % len(P)
|
|
if i == 0:
|
|
crypto.sc_copy(sc1, c)
|
|
|
|
if i & 3 == 0:
|
|
gc.collect()
|
|
|
|
# Final scalar = a - c * (mu_P * p + mu_c * Z)
|
|
crypto.sc_mul_into(tmp_sc, mu_P, p)
|
|
crypto.sc_muladd_into(tmp_sc, mu_C, z, tmp_sc)
|
|
crypto.sc_mulsub_into(tmp_sc, c, tmp_sc, a)
|
|
crypto.encodeint_into(mg_buff[index + 1], tmp_sc)
|
|
|
|
mg_buff.append(crypto.encodeint(sc1))
|
|
mg_buff.append(sD)
|
|
return mg_buff
|
|
|
|
|
|
def _key_vector(rows):
|
|
return [None] * rows
|
|
|
|
|
|
def _key_matrix(rows, cols):
|
|
"""
|
|
first index is columns (so slightly backward from math)
|
|
"""
|
|
rv = [None] * cols
|
|
for i in range(0, cols):
|
|
rv[i] = _key_vector(rows)
|
|
return rv
|
|
|
|
|
|
def _hasher_message(message):
|
|
"""
|
|
Returns incremental hasher for MLSAG
|
|
"""
|
|
ctx = crypto.get_keccak()
|
|
ctx.update(message)
|
|
return ctx
|
|
|
|
|
|
def _hash_point(hasher, point, tmp_buff):
|
|
crypto.encodepoint_into(tmp_buff, point)
|
|
hasher.update(tmp_buff)
|