k98kurz icon

ed25519.py

k98kurz | PRO | 06/08/22 07:26:32 PM UTC (Edited) | 0 ⭐ | 1429 👁️ | Never ⏰ | []
Python |

18.56 KB

|

None

|

0 👍

/

0 👎

from __future__ import annotations
from hashlib import new
from nacl.signing import SigningKey, SignedMessage, VerifyKey
from secrets import token_bytes
import nacl.bindings
 
 
"""
Demonstration of Twisted Edwards Curve 2^255-19 digital signature system, its
properties, and its adaptations. See below sources for more details.
 
Ed25519 (RFC 8032): https://www.rfc-editor.org/rfc/rfc8032.html
Adapters: https://medium.com/crypto-garage/adaptor-signature-schnorr-signature-and-ecdsa-da0663c2adc4
AMHLs: https://eprint.iacr.org/2018/472
"""
 
# helper functions
def clamp_scalar(scalar: bytes, from_private_key: bool = False) -> bytes:
    """Make a clamped scalar."""
    if type(scalar) is bytes and len(scalar) >= 32:
        x_i = bytearray(scalar[:32])
    elif type(scalar) is SigningKey:
        x_i = bytearray(new('sha512', bytes(scalar)).digest()[:32])
        from_private_key = True
    else:
        raise ValueError('not a SigningKey and not 32+ bytes scalar')
 
    if from_private_key:
        # set bits 0, 1, and 2 to 0
        # nb: lsb is right-indexed
        x_i[0] &= 0b11111000
        # set bit 254 to 1
        x_i[31] |= 0b01000000
 
    # set bit 255 to 0
    x_i[31] &= 0b01111111
 
    return bytes(x_i)
 
def H_big(*parts) -> bytes:
    """The big, 64-byte hash function."""
    return new('sha512', b''.join(parts)).digest()
 
def H_small(*parts) -> bytes:
    """The small, 32-byte hash function."""
    return nacl.bindings.crypto_core_ed25519_scalar_reduce(H_big(*parts))
 
def derive_key_from_seed(seed: bytes) -> bytes:
    """Derive the scalar used for signing from a seed."""
    return clamp_scalar(H_big(seed)[:32], True)
 
def aggregate_points(points: list) -> bytes:
    """Aggregate points on the Ed25519 curve."""
    # type checking inputs
    for pt in points:
        if type(pt) is not bytes and type(pt) is not VerifyKey:
            raise TypeError('each point must be bytes or VerifyKey')
 
    # normalize points to bytes
    points = [pt if type(pt) is bytes else bytes(pt) for pt in points]
 
    # raise an error for invalid points
    for pt in points:
        if not nacl.bindings.crypto_core_ed25519_is_valid_point(pt):
            raise ValueError('each point must be a valid ed25519 point')
 
    # compute the sum
    sum = points[0]
    for i in range(1, len(points)):
        sum = nacl.bindings.crypto_core_ed25519_add(sum, points[i])
 
    return sum
 
def xor(b1: bytes, b2: bytes) -> bytes:
    """XOR two equal-length byte strings together."""
    b3 = bytearray()
    for i in range(len(b1)):
        b3.append(b1[i] ^ b2[i])
 
    return bytes(b3)
 
def bytes_are_same(b1: bytes, b2: bytes) -> bool:
    """Timing-attack safe bytes comparison."""
    return len(b1) == len(b2) and int.from_bytes(xor(b1, b2), 'little') == 0
 
def hexify(thing):
    """Convert a data structure to something printable."""
    if type(thing) is dict:
        new_thing = {}
        for key in thing:
            value = thing[key]
            key = key.hex() if type(key) is bytes else key
            new_thing[key] = hexify(value)
        return new_thing
 
    if type(thing) is list:
        return [hexify(t) for t in thing]
 
    if type(thing) is tuple:
        return tuple([hexify(t) for t in thing])
 
    if type(thing) is bytes:
        return thing.hex()
 
    return thing
 
 
# adapter functions
def public_tweak_adapter(seed: bytes, m: bytes, T: bytes) -> tuple[bytes]:
    """Create a public tweak adapter signature. Return vbalue is (R, T, sa)."""
    x = derive_key_from_seed(seed)
    X = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(x) # G^x
    nonce = H_big(seed)[32:]
    r = clamp_scalar(H_small(H_big(nonce, m))) # H(nonce || m)
    R = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(r) # G^r
    RT = aggregate_points((R, T)) # R + t
    ca = clamp_scalar(H_small(RT, X, m)) # H(R + T || X || m)
    sa = nacl.bindings.crypto_core_ed25519_scalar_add(r, nacl.bindings.crypto_core_ed25519_scalar_mul(ca, x)) # r + H(R + T || X || m) * x
    return (R, T, sa)
 
def verify_public_tweak_adapter(adapter: tuple[bytes], m: bytes, X: bytes) -> bool:
    """Verify a public tweak adapter is valid for a given message."""
    (R, T, sa) = adapter
    sa_G = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(sa) # sa_G = G^sa
    RT = aggregate_points((R, T)) # R + T
    ca = clamp_scalar(H_small(RT, X, m)) # H(R + T || X || m)
    caX = nacl.bindings.crypto_scalarmult_ed25519_noclamp(ca, X) # X^H(R + T || X || m)
    RcaX = aggregate_points((R, caX)) # R + X^H(R + T || X || m)
    return bytes_are_same(sa_G, RcaX) # G^sa == R + X^H(R + T || X || m)
 
def decrypt_public_tweak_adapter(adapter: tuple[bytes], m: bytes, t: bytes) -> SignedMessage:
    """Decrypt a public tweak adapter signature."""
    (R, T, sa) = adapter
    RT = aggregate_points((R, T)) # R + T
    s = nacl.bindings.crypto_core_ed25519_scalar_add(sa, t) # s = sa + t
    sig = SignedMessage(RT + s + m)
    return sig
 
 
# AMHL functions
# Messy conventions taken directly from the Anonymous Multi-Hop Lock paper
def sample() -> bytes:
    """Take 1 sample from the domain of the homomorphic one-way function."""
    return clamp_scalar(token_bytes(32))
 
def samples(n: int) -> tuple[bytes]:
    """Take n samples from the domain of the homomorphic one-way function."""
    return tuple(sample() for i in range(n))
 
def oneway(scalar: bytes) -> bytes:
    """Run the homomorphic one-way function on the input scalar."""
    return nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(scalar)
 
def setup(n_users: int) -> tuple[tuple[bytes], tuple[bytes]]:
    """Setup the lock inputs."""
    y = samples(n_users)
    Y = [oneway(y[0])]
    for i, y_i in enumerate(y):
        if i > 0:
            Y.append(aggregate_points((Y[i-1], oneway(y_i))))
    return (y, tuple(Y))
 
def scalar_sum(*scalars: tuple[bytes]) -> bytes:
    """Compute the sum of the scalars."""
    sum = scalars[0]
    for i in range(1, len(scalars)):
        sum = nacl.bindings.crypto_core_ed25519_scalar_add(sum, scalars[i])
    return sum
 
def setup_for(s: tuple[tuple[bytes], tuple[bytes]], i: int) -> tuple[bytes]:
    """Generate the setup for a particular user given the setup s and
        the user index i.
    """
    if i == 0:
        return (s[0][0],)
 
    if i == len(s[0]):
        return ((s[1][i-1], 0, 0), scalar_sum(*s[0]))
 
    return (s[1][i-1], s[1][i], s[0][i])
 
def check_setup(s: tuple[bytes], i: int, n: int) -> bool:
    """Verifies the setup for the ith of n users is valid."""
    if i == 0:
        return len(s) == 1 and isinstance(s[0], bytes)
 
    if i == n:
        return len(s) == 2 and type(s[0]) is tuple and len(s[0]) == 3 and type(s[0][0]) is bytes and type(s[1]) is bytes
 
    Y_i = aggregate_points((s[0], oneway(s[2])))
    return bytes_are_same(Y_i, s[1])
 
def lock(s: tuple) -> tuple[bytes, bool]:
    """Create the lock from the setup."""
    return (s[1], False)
 
def release(k: bytes, sI: tuple) -> bytes:
    """Release a left lock given a key from a released intermediate lock."""
    y = sI[2] if type(sI) is tuple else sI
    return nacl.bindings.crypto_core_ed25519_scalar_sub(k, y)
 
def verify_lock_key(l: bytes, k: bytes) -> bool:
    """Verify that a key opens a lock."""
    return bytes_are_same(l, oneway(k))
 
 
# tests
def test_sign_and_verify():
    """Replication of the maths involved in signing and verifying."""
    # set seed and generate key pair from it
    seed = token_bytes(32)
    skey = SigningKey(seed)
 
    # derive key pair manually
    x = derive_key_from_seed(seed) # clamp(H(seed)[:32])
    X = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(x) # G^x
    m = b'hello world'
 
    # create signature with nacl
    sig = skey.sign(m)
 
    # create signature manually
    nonce = H_big(seed)[32:]
    r = clamp_scalar(H_small(nonce, m))
    R = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(r) # G^r
    c = clamp_scalar(H_small(R, X, m)) # clamp(H(R || X || m))
    s = nacl.bindings.crypto_core_ed25519_scalar_add(r, nacl.bindings.crypto_core_ed25519_scalar_mul(c, x)) # r + c*x
    nsig = SignedMessage(R + s + m)
 
    # G, X, R, s, and m are public values
    # c can be derived: H(R || X || m)
    # Therefore, G^s = R * X^c can be verified by anyone
    # But only the holder of x and r can create the signature
 
    # verify both
    assert bytes_are_same(bytes(sig), bytes(nsig))
    assert skey.verify_key.verify(sig) # s = c*x + r
    assert skey.verify_key.verify(nsig) # G^s = G^(c*x + r) = R * G^(x*c) = R * X^c
 
 
def test_homomorphic_one_way():
    """Test if Ed25519 meets the homomorphic one way condition."""
    x1, x2 = clamp_scalar(token_bytes(32)), clamp_scalar(token_bytes(32))
    y1 = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(x1) # G^x1
    y2 = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(x2) # G^x2
 
    # test
    y3_1 = nacl.bindings.crypto_core_ed25519_add(y1, y2) # G^x1 + G^x2
    x3 = nacl.bindings.crypto_core_ed25519_scalar_add(x1, x2) # x1 + x2
    y3_2 = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(x3) # G^(x1+x2)
    assert y3_1 == y3_2 # G^x1 * G^x2 = G^(x1+x2) where * denotes group operator
 
 
def test_adapter_signature():
    """Test the construction, unlocking, and verification of adapters.
        For an explanation and possible uses, see this medium article:
        https://medium.com/crypto-garage/adaptor-signature-schnorr-signature-and-ecdsa-da0663c2adc4
    """
    # key pairs and message
    seed1, seed2 = token_bytes(32), token_bytes(32)
    skey = SigningKey(seed1)
    x, t = derive_key_from_seed(seed1), derive_key_from_seed(seed2)
    X = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(x) # G^x
    T = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(t) # G^t
    m = b'txn sending money to counterparty as part of payment route'
 
    # private key tweaking construction
    # sa = t + r + H(R || X || m) * x
    nonce = H_big(seed1)[32:]
    r = clamp_scalar(H_small(nonce, m))
    R = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(r) # G^r
    c = clamp_scalar(H_small(R, X, m)) # clamp(H(R || X || m))
    tr = nacl.bindings.crypto_core_ed25519_scalar_add(t, r)
    sa = nacl.bindings.crypto_core_ed25519_scalar_add(tr, nacl.bindings.crypto_core_ed25519_scalar_mul(c, x)) # t + r + c*x
 
    # public values: R, sa, m, X, T
    # sig = (T, R, sa, m)
 
    # private key tweaking adapter verification
    # sa_G = G^sa
    sa_G = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(sa)
    c = clamp_scalar(H_small(R, X, m)) # clamp(H(R || X || m))
    cX = nacl.bindings.crypto_scalarmult_ed25519_noclamp(c, X)
    TRcX = aggregate_points((T, R, cX))
    assert bytes_are_same(sa_G, TRcX)
    # G^sa == T + R + X^H(R || X || m)
 
    # private key tweaking signature decryption and verification
    # s = sa - t
    s = nacl.bindings.crypto_core_ed25519_scalar_sub(sa, t)
    sig = SignedMessage(R + s + m)
    assert skey.verify_key.verify(sig)
    # G^(sa - t) == T + R + X^H(R || X || m) - T
    # G^s == R + X^H(R || X || m)
 
 
    # public key tweaking construction
    # sa = r + H(R + T || X || m) * x
    RT = aggregate_points((R, T))
    ca = clamp_scalar(H_small(RT, X, m))
    sa = nacl.bindings.crypto_core_ed25519_scalar_add(r, nacl.bindings.crypto_core_ed25519_scalar_mul(ca, x))
 
    # public values: R, sa, m, X, T
    # ca and RT can be derived from public values
    # sig = (T, R, sa, m)
 
    # public key tweaking adapter verification
    # sa_G = G^sa
    sa_G = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(sa)
    caX = nacl.bindings.crypto_scalarmult_ed25519_noclamp(ca, X)
    RcaX = aggregate_points((R, caX))
    assert bytes_are_same(sa_G, RcaX)
    # G^sa == R + X^H(R + T || X || m)
 
    # public key tweaking signature decryption and verification
    # s = sa + t
    s = nacl.bindings.crypto_core_ed25519_scalar_add(sa, t)
    sig = SignedMessage(RT + s + m)
    assert skey.verify_key.verify(sig)
    # G^(sa + t) == R + X^H(R + T || X || m) + T
    # G^s == (R + T) + X^H((R + T) || X || m)
    ...
 
 
def test_AMHL():
    """Test for setup, locking, and release of an Anonymous Multi-Hop
        Lock using the homomorphic qualities of ed25519.
    """
    # first run the initial setup for 5 payers (4 intermediate)
    n = 5
    s = setup(n)
 
    # validate setups for each user
    for i in range(len(s[0])):
        assert check_setup(setup_for(s, i), i, n)
 
    # validate releasing of locks from right to left
    s_n = setup_for(s, n)
    k = s_n[1]
 
    for i in range(n-1, 1, -1):
        s_i = setup_for(s, i)
        r = release(k, s_i)
        assert verify_lock_key(setup_for(s, i-1)[1], r)
        k = r
 
 
def test_AMHL_adapters():
    """Test for combining the AMHL primitive with the adapter signature
        primitive for atomic transaction locking/unlocking. In this case,
        Alice pays Dave through Bob and Carla, with Bob and Carla each
        taking a fee for faciliating the transaction.
    """
    # first run the initial setup for 3 payers (2 intermediate)
    n = 3
    s = setup(n)
 
    # validate setups for each user
    for i in range(len(s[0])):
        assert check_setup(setup_for(s, i), i, n)
 
    # Alice setup
    Alice = {
        'seed' : token_bytes(32),
        'outbound_txn' : {
            'm': b'Alice pays Bob 12'
        },
        'lock': setup_for(s, 0)
    }
    Alice['skey'] = SigningKey(Alice['seed'])
    Alice['vkey'] = Alice['skey'].verify_key
    Alice['x'] = derive_key_from_seed(Alice['seed'])
    Alice['X'] = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(Alice['x'])
    # create adapter
    Alice['outbound_txn']['adapter'] = public_tweak_adapter(Alice['seed'], Alice['outbound_txn']['m'], oneway(Alice['lock'][0]))
 
    # Bob setup
    Bob = {
        'seed' : token_bytes(32),
        'inbound_txn': Alice['outbound_txn'],
        'outbound_txn' : {
            'm': b'Bob pays Carla 11'
        },
        'lock': setup_for(s, 1)
    }
    Bob['skey'] = SigningKey(Bob['seed'])
    Bob['vkey'] = Bob['skey'].verify_key
    Bob['x'] = derive_key_from_seed(Bob['seed'])
    Bob['X'] = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(Bob['x'])
    # verify Bob's left lock is Alice's right lock
    assert bytes_are_same(Bob['lock'][0], oneway(Alice['lock'][0]))
    # verify Alice's adapter
    assert verify_public_tweak_adapter(Bob['inbound_txn']['adapter'], Bob['inbound_txn']['m'], Alice['X'])
    # create adapter
    Bob['outbound_txn']['adapter'] = public_tweak_adapter(Bob['seed'], Bob['outbound_txn']['m'], Bob['lock'][1])
 
    # Carla setup
    Carla = {
        'seed' : token_bytes(32),
        'inbound_txn' : Bob['outbound_txn'],
        'outbound_txn': {
            'm': b'Carla pays Dave 10'
        },
        'lock': setup_for(s, 2)
    }
    Carla['skey'] = SigningKey(Carla['seed'])
    Carla['vkey'] = Carla['skey'].verify_key
    Carla['x'] = derive_key_from_seed(Carla['seed'])
    Carla['X'] = nacl.bindings.crypto_scalarmult_ed25519_base_noclamp(Carla['x'])
    # verify Carla's left lock is Bob's right lock
    assert bytes_are_same(Carla['lock'][0], Bob['lock'][1])
    # verify Bob's adapter
    assert verify_public_tweak_adapter(Carla['inbound_txn']['adapter'], Carla['inbound_txn']['m'], Bob['X'])
    # create adapter
    Carla['outbound_txn']['adapter'] = public_tweak_adapter(Carla['seed'], Carla['outbound_txn']['m'], Carla['lock'][1])
 
    # Dave setup
    Dave = {
        'seed' : token_bytes(32),
        'inbound_txn' : Carla['outbound_txn'],
        'lock': setup_for(s, 3)
    }
    # verify Dave's left lock is Carla's right lock
    assert bytes_are_same(Dave['lock'][0][0], Carla['lock'][1])
    # verify Carla's adapter
    assert verify_public_tweak_adapter(Dave['inbound_txn']['adapter'], Dave['inbound_txn']['m'], Carla['X'])
 
    # decrypt adapter paying Dave
    k = Dave['lock'][1]
    Dave['inbound_txn']['signature'] = decrypt_public_tweak_adapter(Dave['inbound_txn']['adapter'], Dave['inbound_txn']['m'], k)
    assert Carla['vkey'].verify(Dave['inbound_txn']['signature'])
 
    # release lock for Carla
    r = release(k, Carla['lock'])
    assert verify_lock_key(Carla['lock'][0], r) # Carla's left lock == Bob's right lock
 
    # decrypt adapter paying Carla
    Carla['inbound_txn']['signature'] = decrypt_public_tweak_adapter(Carla['inbound_txn']['adapter'], Carla['inbound_txn']['m'], r)
    assert Bob['vkey'].verify(Carla['inbound_txn']['signature'])
 
    # release lock for Bob
    r = release(r, Bob['lock'])
    assert verify_lock_key(Bob['lock'][0], r) # Bob's left lock == Alice's right lock
 
    # decrypt adapter paying Bob
    Bob['inbound_txn']['signature'] = decrypt_public_tweak_adapter(Bob['inbound_txn']['adapter'], Bob['inbound_txn']['m'], r)
    assert Alice['vkey'].verify(Bob['inbound_txn']['signature'])
 
    # Dave now has a signed transaction paying him from Carla
    # Carla now has a signed transaction paying her from Bob
    # Bob now has a signed transaction paying him from Alice
    # Alice has paid Dave through Bob and Carla
    ...
 
 
def license():
    """Copyleft (c) 2022 k98kurz
 
        Permission to use, copy, modify, and/or distribute this software
        for any purpose with or without fee is hereby granted, provided
        that the above copyleft notice and this permission notice appear in
        all copies.
 
        THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
        WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
        WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
        AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
        CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
        OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
        NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
        CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    """
    return license.__doc__
 
 
if __name__ == '__main__':
    for i in range(128):
        test_homomorphic_one_way()
    print('homomorphic one-way function test passed')
 
    for i in range(128):
        test_sign_and_verify()
    print('signature creation and verification test passed')
 
    for i in range(128):
        test_adapter_signature()
    print('adapter signature test passed')
 
    for i in range(128):
        test_AMHL()
    print('anonymous multihop lock test passed')
 
    for i in range(128):
        test_AMHL_adapters()
    print('AMHL adapter (payment channel routing) test passed')
 

Comments