# 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.
"""ECDSA adaptor signatures.
This module wraps every entry point `secp256k1_ecdsa_adaptor.h`
declares: `encrypt`, `verify`, `decrypt` and `recover`. An adaptor
signature is an ECDSA signature that is not yet valid: it is encrypted
under a public key Y, and whoever knows the secret key y of Y can
`decrypt` it into an ordinary signature. Whoever then sees both the
adaptor signature and the decrypted signature can `recover` y, which is
what makes the construction useful for atomic swaps and DLCs.
**WARNING.** The adaptor signature leaks the Diffie-Hellman point of the
signing key and the encryption key: given X = x*G, Y = y*G and the
adaptor signature, x*Y = y*X is easy to compute. Do not use the signing key in
protocols that rely on the hardness of the computational Diffie-Hellman
problem, such as a key exchange or ElGamal encryption. The header says
the same.
An adaptor signature is 162 bytes, `R || R' || s' || e || s`: the
33-byte compressed points R = k*Y and R' = k*G, the scalar
s' = k^-1 (m + R.x * x), and a proof (e, s) that R and R' have the same
discrete logarithm k with respect to Y and G. `verify` checks the proof
and that s' is a signature on the message under R'. Public keys cross
this boundary as SEC bytes and signatures as the 64 bytes of `r || s`,
as in `zkp.ecdsa_s2c`.
The nonce function is fixed to the library's default: the C entry point
accepts another, and this module does not offer one. The default's
auxiliary randomness is `encrypt`'s `aux_rand32`.
This module reads `context._bindings()` inside each function rather than
at its own top level, for the reason `zkp.context`'s docstring gives.
"""
from __future__ import annotations
from typing import overload
from btclib_secp256k1 import BytesLike, MutableBytesLike
from btclib_secp256k1._scalar import octets, scalar
from btclib_secp256k1._secret import take, wipe
from . import context
from .ecdsa_s2c import _signature_parse, _signature_serialize
__all__ = ["decrypt", "encrypt", "recover", "verify"]
_ADAPTOR_SIGNATURE_SIZE = 162
_DECKEY_SIZE = 32
_SIGNATURE_SIZE = 64
[docs]
def encrypt(
msg32: BytesLike,
prvkey: BytesLike | int,
enckey: BytesLike,
*,
aux_rand32: BytesLike | None = None,
verify: bool = True,
) -> bytes:
"""Create an adaptor signature, encrypted under a public key.
With `verify`, the result is checked by `verify` under the public
key of `prvkey` before it is returned, as `ecdsa_s2c.sign` checks
its own: what that catches is the computation going wrong.
Args:
msg32: the 32-byte hash of the message.
prvkey: the private key, 32 bytes or an int below 2**256.
enckey: the encryption public key, SEC compressed or
uncompressed.
aux_rand32: 32 bytes of auxiliary randomness for the nonce, as
BIP340 recommends, or None for none.
verify: whether to check the adaptor signature before returning
it.
Returns:
The 162-byte adaptor signature.
Raises:
TypeError: if an argument is not of a type its name allows.
ValueError: if the message hash or `aux_rand32` is not 32 bytes,
if the private key is not 32 bytes, does not fit in them, or
is not in [1, n-1], or if the encryption key is not a valid
point.
RuntimeError: if `verify` asks and the adaptor signature does
not verify, which no input can make happen.
Example:
>>> from btclib_secp256k1 import keys
>>> from btclib_secp256k1.zkp import ecdsa_adaptor
>>> enckey = keys.pubkey_from_prvkey(2)
>>> adaptor_sig = ecdsa_adaptor.encrypt(bytes(32), 1, enckey)
>>> ecdsa_adaptor.verify(
... adaptor_sig, keys.pubkey_from_prvkey(1), bytes(32), enckey
... )
True
"""
msg_bytes = octets(msg32, "message hash", 32)
prvkey_bytes = scalar(prvkey, "private key")
aux_bytes = None if aux_rand32 is None else octets(aux_rand32, "aux_rand32", 32)
ffi, lib, ctx = context._bindings()
enckey_obj = context._pubkey_parse(ffi, lib, ctx, enckey, "encryption key")
adaptor_sig = ffi.new(f"char[{_ADAPTOR_SIGNATURE_SIZE}]")
if not lib.secp256k1_ecdsa_adaptor_encrypt(
ctx,
adaptor_sig,
prvkey_bytes,
enckey_obj,
msg_bytes,
ffi.NULL,
ffi.NULL if aux_bytes is None else aux_bytes,
):
raise ValueError("invalid private key: not in [1, n-1]")
if verify:
pubkey = ffi.new("secp256k1_pubkey *")
if not lib.secp256k1_ec_pubkey_create(
ctx, pubkey, prvkey_bytes
) or not lib.secp256k1_ecdsa_adaptor_verify(
ctx, adaptor_sig, pubkey, msg_bytes, enckey_obj
):
raise RuntimeError(
"encryption produced an adaptor signature that does not verify"
)
return bytes(ffi.unpack(adaptor_sig, _ADAPTOR_SIGNATURE_SIZE))
[docs]
def verify(
adaptor_sig162: BytesLike,
pubkey: BytesLike,
msg32: BytesLike,
enckey: BytesLike,
) -> bool:
"""Verify an adaptor signature.
Answers whether the decryption key can be extracted from the
adaptor signature and the signature completed from it, which is
what the adaptor signature's proof and its signature on the message
under the signer's key establish together.
Args:
adaptor_sig162: the 162-byte adaptor signature.
pubkey: the signer's public key, SEC compressed or uncompressed.
msg32: the 32-byte hash of the message.
enckey: the encryption public key, SEC compressed or
uncompressed.
Returns:
True if the adaptor signature verifies, False if it does not,
a malformed one included.
Raises:
TypeError: if an argument is not of a type its name allows.
ValueError: if the adaptor signature is not 162 bytes or the
message hash is not 32, or if a public key is not a valid
point.
"""
adaptor_bytes = octets(adaptor_sig162, "adaptor signature", _ADAPTOR_SIGNATURE_SIZE)
msg_bytes = octets(msg32, "message hash", 32)
ffi, lib, ctx = context._bindings()
pubkey_obj = context._pubkey_parse(ffi, lib, ctx, pubkey, "public key")
enckey_obj = context._pubkey_parse(ffi, lib, ctx, enckey, "encryption key")
return bool(
lib.secp256k1_ecdsa_adaptor_verify(
ctx, adaptor_bytes, pubkey_obj, msg_bytes, enckey_obj
)
)
[docs]
def decrypt(deckey: BytesLike | int, adaptor_sig162: BytesLike) -> bytes:
"""Complete an adaptor signature into an ECDSA signature.
The adaptor signature is not verified here, and a signature
decrypted from an unverified one may be invalid: call `verify` first.
The result is the low-s form.
Args:
deckey: the decryption key, the private key of the encryption
public key: 32 bytes or an int below 2**256.
adaptor_sig162: the 162-byte adaptor signature.
Returns:
The signature, as the 64 bytes of `r || s`.
Raises:
TypeError: if an argument is not of a type its name allows.
ValueError: if the decryption key is not 32 bytes, does not fit
in them, or is not in [1, n-1], or if the adaptor signature
is not 162 bytes, its s' is not in [1, n-1], or its R.x is 0
modulo n.
RuntimeError: if libsecp256k1-zkp fails to serialize the
signature, which no signature it produced can make it do.
"""
deckey_bytes = scalar(deckey, "decryption key")
adaptor_bytes = octets(adaptor_sig162, "adaptor signature", _ADAPTOR_SIGNATURE_SIZE)
ffi, lib, ctx = context._bindings()
signature = ffi.new("secp256k1_ecdsa_signature *")
if not lib.secp256k1_ecdsa_adaptor_decrypt(
ctx, signature, deckey_bytes, adaptor_bytes
):
raise ValueError("invalid decryption key or adaptor signature")
return _signature_serialize(ffi, lib, ctx, signature)
@overload
def recover(
sig64: BytesLike, adaptor_sig162: BytesLike, enckey: BytesLike
) -> bytes: ...
@overload
def recover(
sig64: BytesLike,
adaptor_sig162: BytesLike,
enckey: BytesLike,
*,
into: MutableBytesLike,
) -> None: ...
@overload
def recover(
sig64: BytesLike,
adaptor_sig162: BytesLike,
enckey: BytesLike,
*,
into: MutableBytesLike | None,
) -> bytes | None: ...
[docs]
def recover(
sig64: BytesLike,
adaptor_sig162: BytesLike,
enckey: BytesLike,
*,
into: MutableBytesLike | None = None,
) -> bytes | None:
"""Recover the decryption key from a signature and its adaptor signature.
Whoever completes an adaptor signature into `sig64` has revealed the
decryption key to anyone holding both. Neither is verified here
beyond what recovery itself checks: that `sig64` and the adaptor
signature share `r`, and that the key they imply matches `enckey`.
Args:
sig64: the 64-byte completed signature, `r || s`.
adaptor_sig162: the 162-byte adaptor signature it was completed
from.
enckey: the encryption public key, SEC compressed or
uncompressed.
into: a writable 32-byte buffer to receive the key, 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 decryption key -- or None where `into` was given and
holds it.
Raises:
TypeError: if an argument is not of a type its name allows, or
if `into` is not a writable buffer of contiguous
one-dimensional octets.
ValueError: if `sig64` is not 64 bytes or is not a valid compact
signature, if the adaptor signature is not 162 bytes or its
s' is not in [1, n-1], if the encryption key is not a valid
point, if the signature and the adaptor signature do not
match, or if `into` is not 32 bytes.
"""
sig_bytes = octets(sig64, "signature", _SIGNATURE_SIZE)
adaptor_bytes = octets(adaptor_sig162, "adaptor signature", _ADAPTOR_SIGNATURE_SIZE)
ffi, lib, ctx = context._bindings()
signature = _signature_parse(ffi, lib, ctx, sig_bytes)
enckey_obj = context._pubkey_parse(ffi, lib, ctx, enckey, "encryption key")
deckey = ffi.new(f"char[{_DECKEY_SIZE}]")
if not lib.secp256k1_ecdsa_adaptor_recover(
ctx, deckey, signature, adaptor_bytes, enckey_obj
):
# recovery may have written a key before refusing it
wipe(deckey)
raise ValueError("the signature does not match the adaptor signature")
return take(deckey, into=into)