Source code for btclib_secp256k1.ellswift

# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.

"""ElligatorSwift encoding and x-only ECDH, according to BIP324.

https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki
"""

from __future__ import annotations

from typing import overload

from . import BytesLike, CData, MutableBytesLike, ffi, lib
from ._scalar import entropy, in_range, octets, scalar
from ._secret import take
from .context import ctx
from .keys import parse, serialize

__all__ = ["create", "decode", "encode", "xdh"]

# the width of an ElligatorSwift encoding: 64 bytes, the two field
# elements it is. `create` and `_encode_` write it, `_decode_` and `xdh`
# accept it, and the one statement of it answers for all four -- the
# buffer's type is built from it and so is every length check. It saves
# some hundredths of a microsecond on calls of 19.90 and 14.85, where
# timing each of those against itself moves 0.004 and 0.10: nothing that
# measurement resolves, and not the reason either -- `xonly.py` says the
# reason
_ENCODING_SIZE = 64
_ENCODING_BUFFER_TYPE = ffi.typeof(f"char[{_ENCODING_SIZE}]")


[docs] def create(prvkey: BytesLike | int, aux_rand32: BytesLike | None = None) -> bytes: """Create the 64-byte ElligatorSwift encoding of a private key's public key. This is safer than encoding the public key of that private key, as the key itself is used as entropy for the encoding. Args: prvkey: the private key, 32 bytes or an int below 2**256. aux_rand32: 32 bytes of auxiliary randomness, or None for fresh randomness. Returns: The 64-byte ElligatorSwift encoding of the public key. Raises: ValueError: if aux_rand32 is given and is not 32 bytes, or if the private key is not 32 bytes, does not fit in them, or is not in [1, n-1]. """ prvkey_bytes = scalar(prvkey, "private key") ell_bytes = ffi.new(_ENCODING_BUFFER_TYPE) if not lib.secp256k1_ellswift_create( ctx, ell_bytes, prvkey_bytes, entropy(aux_rand32) ): raise ValueError("invalid private key: not in [1, n-1]") return ffi.unpack(ell_bytes, _ENCODING_SIZE)
def _encode_(pubkey: CData, aux_rand32: BytesLike | None = None) -> bytes: """Encode an already-parsed public key as 64 ElligatorSwift bytes. The private half of `encode`, for a caller who already holds the parsed point -- one encoding a key it has just decoded, or one encoding the same key more than once, which BIP324 does with fresh randomness at every connection: see the package docstring for what the two underscores mean throughout. Args: pubkey: the already-parsed public key, as `keys.parse` returns. aux_rand32: the 32 bytes deciding which of the encodings of that key is produced, or None for fresh randomness. Returns: The 64-byte ElligatorSwift encoding. Raises: ValueError: if aux_rand32 is given and is not 32 bytes. RuntimeError: if libsecp256k1 fails to encode, which no valid input can make it do. """ ell_bytes = ffi.new(_ENCODING_BUFFER_TYPE) aux_rand32_bytes = entropy(aux_rand32) encoded = lib.secp256k1_ellswift_encode(ctx, ell_bytes, pubkey, aux_rand32_bytes) if not encoded: msg = "ElligatorSwift encoding failed" raise RuntimeError(msg) return ffi.unpack(ell_bytes, _ENCODING_SIZE)
[docs] def encode(pubkey_bytes: BytesLike, aux_rand32: BytesLike | None = None) -> bytes: """Encode a public key as 64 ElligatorSwift bytes. The randomness must not be a deterministic function of the public key; when it is not provided, it is freshly generated. Args: pubkey_bytes: the public key to encode, 33 or 65 bytes. aux_rand32: the 32 bytes deciding which of the encodings of that key is produced, or None for fresh randomness. Returns: The 64-byte ElligatorSwift encoding, indistinguishable from uniform bytes. Raises: ValueError: if the public key is not a valid point, or if aux_rand32 is given and is not 32 bytes. RuntimeError: if libsecp256k1 fails to encode, which no valid input can make it do. """ return _encode_(parse(pubkey_bytes), aux_rand32)
def _decode_(ell_bytes: BytesLike) -> CData: """Decode a 64-byte ElligatorSwift public key into a parsed key. The private half of `decode`, and the one that answers with the point rather than with its serialization: see the package docstring for what the two underscores mean throughout. A decoded key that is about to be tweaked, verified against or encoded again is what this is for -- libsecp256k1 hands the point over already lifted, and serializing it only to lift it back is a field square root nothing asked for. Args: ell_bytes: the 64-byte encoding. Returns: The libsecp256k1 public key object. Every 64 bytes decode to a point, which is what makes the encoding indistinguishable from random: there is nothing to reject. Raises: ValueError: if the input is not 64 bytes. RuntimeError: if libsecp256k1 fails to decode, which no 64 bytes can make it do. """ ell_bytes = octets(ell_bytes, "ElligatorSwift public key", _ENCODING_SIZE) pubkey = ffi.new("secp256k1_pubkey *") if not lib.secp256k1_ellswift_decode(ctx, pubkey, ell_bytes): raise RuntimeError("ElligatorSwift decoding failed") return pubkey
[docs] def decode(ell_bytes: BytesLike, compressed: bool = True) -> bytes: """Decode a 64-byte ElligatorSwift public key into a public key. Args: ell_bytes: the 64-byte encoding. compressed: whether to return 33 bytes rather than 65. Returns: The serialized public key. Every 64 bytes decode to a point, which is what makes the encoding indistinguishable from random: there is nothing to reject. Raises: ValueError: if the input is not 64 bytes. RuntimeError: if libsecp256k1 fails to decode or serialize, which no 64 bytes can make it do. """ return serialize(_decode_(ell_bytes), compressed)
@overload def xdh( ell_a_bytes: BytesLike, ell_b_bytes: BytesLike, prvkey: BytesLike | int, party: int ) -> bytes: ... @overload def xdh( ell_a_bytes: BytesLike, ell_b_bytes: BytesLike, prvkey: BytesLike | int, party: int, *, into: MutableBytesLike, ) -> None: ... @overload def xdh( ell_a_bytes: BytesLike, ell_b_bytes: BytesLike, prvkey: BytesLike | int, party: int, *, into: MutableBytesLike | None, ) -> bytes | None: ...
[docs] def xdh( ell_a_bytes: BytesLike, ell_b_bytes: BytesLike, prvkey: BytesLike | int, party: int, *, into: MutableBytesLike | None = None, ) -> bytes | None: """Compute the x-only ECDH shared secret of two ElligatorSwift keys. The private key must be the one of the given party: 0 for party A, 1 for party B; the correspondence is not checked. The 32-byte secret is derived with the BIP324 hash function. Args: ell_a_bytes: the 64-byte encoding of party A's key. ell_b_bytes: the 64-byte encoding of party B's key. prvkey: the private key of the party below, 32 bytes or an int below 2**256. party: 0 if the private key is A's, 1 if it is B's. into: a writable 32-byte buffer to receive the result, instead of the `bytes` this otherwise returns. See `_secret.take` and SECURITY.md for what that does and does not buy. Returns: The 32-byte shared secret, which both parties compute alike -- or None where `into` was given and holds it. The two encodings enter the hash in the order they are given here, so BIP324's initiator goes first. Raises: TypeError: if the party is not an int, or if `into` is not a writable bytearray or memoryview of octets. ValueError: if either encoding is not 64 bytes, if party is not 0 or 1, or if the private key is not 32 bytes, does not fit in them, or is not a valid scalar. """ ell_a_bytes = octets(ell_a_bytes, "ElligatorSwift public key of A", _ENCODING_SIZE) ell_b_bytes = octets(ell_b_bytes, "ElligatorSwift public key of B", _ENCODING_SIZE) # 0 is A and 1 is B, which is what the argument's own name says party = in_range(party, "party", 1) prvkey_bytes = scalar(prvkey, "private key") output = ffi.new("char[32]") if not lib.secp256k1_ellswift_xdh( ctx, output, ell_a_bytes, ell_b_bytes, prvkey_bytes, party, lib.secp256k1_ellswift_xdh_hash_function_bip324, ffi.NULL, ): raise ValueError("invalid private key") return take(output, into=into)