# 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.
"""Diffie-Hellman elliptic curve key agreement, per SEC 1 v.2.
Two parties, each holding the other's public key, compute the same
shared secret -- their key pair times the other's public point -- and
derive symmetric keying data from it through a key derivation
function. The curve and the KDF are the two things the parties must
agree on beforehand; SEC 1's KDF is `kdf.ansi_x9_63_kdf`, which
btclib_ecc.kdf holds beside RFC 5869's, and diffie_hellman is the agreement
built on it.
**Why `ecdh.shared_secret` of the bindings has no caller here, and this is the
place that says so** (issue btclib-org/btclib#909). That function multiplies and
hashes in one call, and the hash is SHA256 of the compressed shared point with
no way to change it: libsecp256k1 takes it as a C callback, so exposing it would
mean calling back into python from the middle of the computation. Every
ECDH-shaped computation here derives differently, so what is delegated is the
multiplication -- `ecdh.shared_point`, the same `secp256k1_ecdh` call answering
the point instead of its hash, and so the same `secp256k1_ecmult_const`,
constant time in the scalar. The derivation stays in python:
- `diffie_hellman` below runs SEC 1's ANSI-X9.63-KDF over the
x-coordinate, under the hash function the caller passed;
- `ecc.ecies.derive_keys` hashes the *compressed point* with sha512 and
cuts the 64 bytes three ways, which is BIE1's shape and not this one.
So the verdict is not that the function is wrong: it is that a shared
secret is a protocol's own derivation, and only a protocol agreeing with
libsecp256k1's default can hand the whole of it over. Neither of the two
here does.
"""
from __future__ import annotations
from hashlib import sha256
# the module and not the function it calls: `from btclib_ecc.kdf import
# ansi_x9_63_kdf` would bind that name here too, leaving
# `btclib_ecc.ecc.dh.ansi_x9_63_kdf` a live spelling of a function this
# module does not define, where `btclib_ecc.kdf` is to be the only place
# it is defined
from btclib_ecc import kdf
from btclib_ecc._libsecp256k1 import shared_point as libsecp256k1_shared_point
from btclib_ecc.alias import HashF, Integer, Point
from btclib_ecc.curves import (
Curve,
bytes_from_point,
mult,
scalar_from_prv_key,
secp256k1,
)
from btclib_ecc.curves.curve import _libsecp256k1_serves
from btclib_ecc.exceptions import BTClibEccRuntimeError, BTClibEccValueError
__all__ = [
"diffie_hellman",
]
[docs]
def diffie_hellman(
dU: Integer,
QV: Point,
size: int,
shared_info: bytes | None = None,
ec: Curve = secp256k1,
hf: HashF = sha256,
) -> bytes:
"""Diffie-Hellman elliptic curve key agreement scheme.
http://www.secg.org/sec1-v2.pdf, section 6.1
The shared point is a point that is not the generator multiplied by
a secret, and on secp256k1 `ecdh.shared_point` of the bindings
computes it here: `secp256k1_ecdh`, whose `secp256k1_ecmult_const` is
constant time in dU, at a fraction of what the Python endomorphism
path costs.
`ecdh.shared_secret` of the bindings is a different function and not
a substitute: it hashes the compressed shared point with SHA256,
where this derives through ANSI-X9.63-KDF. The module docstring above
has that verdict for both of this package's ECDH-shaped computations.
Cofactor Diffie-Hellman (SEC 1 v.2, section 3.3.2) on a curve whose
cofactor is above 1: QV is multiplied by the cofactor before dU
multiplies the product, so a component of QV outside ⟨G⟩ -- one
`bytes_from_point` still serializes, nothing here confining QV to ⟨G⟩
the way `sec_point.point_from_pub_key` does -- is annihilated by h·QV
rather than surviving into the point dU multiplies and leaking dU's
residue modulo that component's order (issue
btclib-org/btclib-ecc#15). h == 1 on every curve without a
cofactor, where this is QV unchanged.
`dU` is read through `curves.scalar_from_prv_key`, which validates it
into 1..n-1 before either arithmetic arm sees it, so a bool, a float, a
negative int or a value at or above `ec.n` is refused identically
whichever arm ends up serving the call (issue
btclib-org/btclib-ecc#10). `QV` is refused the same way on both
arms when it is the infinity point: nothing here otherwise confines it
to a serializable point, but INF is the one value neither arm can turn
into a shared secret, and checking it once ahead of the dispatch is
what keeps the two arms agreeing on it.
"""
d = scalar_from_prv_key(dU, ec)
ec.require_on_curve(QV)
if QV[1] == 0:
err_msg = "invalid (INF) public key"
raise BTClibEccValueError(err_msg)
if _libsecp256k1_serves(ec, None):
# uncompressed, which is the cheap form to hand over: parsing 65
# octets reads both coordinates where 33 are a field square root,
# and the point is here to be written either way, so the
# multiplication that follows is spared the lift. The answer is
# compressed, whose octets past the tag are the x-coordinate
sec = libsecp256k1_shared_point(bytes_from_point(QV, ec, compressed=False), d)
return kdf.ansi_x9_63_kdf(sec[1:], size, hf, shared_info)
# the cofactor multiplication first and dU's second: h < n always, so
# mult(ec.cofactor, ...)'s own reduction mod n leaves h untouched, and
# only after it lands in ⟨G⟩ is reducing the second scalar mod n valid
QV_in_subgroup = QV if ec.cofactor == 1 else mult(ec.cofactor, QV, ec)
shared_secret_point = mult(d, QV_in_subgroup, ec)
# QV is not INF, checked above, but a QV whose order divides the
# cofactor still lands h*QV on INF, and d, in 1..n-1, cannot recover
# from that
if shared_secret_point[1] == 0:
err_msg = "invalid (INF) key"
raise BTClibEccRuntimeError(err_msg)
shared_secret_field_element = shared_secret_point[0]
z = shared_secret_field_element.to_bytes(ec.p_size, byteorder="big", signed=False)
return kdf.ansi_x9_63_kdf(z, size, hf, shared_info)