Skip to content

Commit

Permalink
Implement Bitcoin::BIP324#v2_ecdh that compute BIP324 shared secret.
Browse files Browse the repository at this point in the history
  • Loading branch information
azuchi committed Jan 11, 2024
1 parent 24c5df7 commit aeb0b1d
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 2 deletions.
37 changes: 37 additions & 0 deletions lib/bitcoin/bip324.rb
Expand Up @@ -92,5 +92,42 @@ def xelligatorswift(x)
end
end
end

# Compute x coordinate of shared ECDH point between +ellswift_theirs+ and +priv_key+.
# @param [Bitcoin::BIP324::EllSwiftPubkey] ellswift_theirs Their EllSwift public key.
# @param [String] priv_key Private key with hex format.
# @return [String] x coordinate of shared ECDH point with hex format.
# @raise ArgumentError
def ellswift_ecdh_xonly(ellswift_theirs, priv_key)
raise ArgumentError, "ellswift_theirs must be a Bitcoin::BIP324::EllSwiftPubkey" unless ellswift_theirs.is_a?(Bitcoin::BIP324::EllSwiftPubkey)
d = priv_key.hex
x = ellswift_theirs.decode.to_point.x
field = BIP324::FIELD
y = BIP324.sqrt(field.mod(field.power(x, 3) + 7))
return nil unless y
point = ECDSA::Point.new(ECDSA::Group::Secp256k1, x, y) * d
ECDSA::Format::FieldElementOctetString.encode(point.x, point.group.field).bth
end

# Compute BIP324 shared secret.
# @param [String] priv_key Private key with hex format.
# @param [Bitcoin::BIP324::EllSwiftPubkey] ellswift_theirs Their EllSwift public key.
# @param [Bitcoin::BIP324::EllSwiftPubkey] ellswift_ours Our EllSwift public key.
# @param [Boolean] initiating Whether your initiator or not.
# @return [String] Shared secret with hex format.
# @raise ArgumentError
def v2_ecdh(priv_key, ellswift_theirs, ellswift_ours, initiating)
raise ArgumentError, "ellswift_theirs must be a Bitcoin::BIP324::EllSwiftPubkey" unless ellswift_theirs.is_a?(Bitcoin::BIP324::EllSwiftPubkey)
raise ArgumentError, "ellswift_ours must be a Bitcoin::BIP324::EllSwiftPubkey" unless ellswift_ours.is_a?(Bitcoin::BIP324::EllSwiftPubkey)

if Bitcoin.secp_impl.is_a?(Bitcoin::Secp256k1::Native)
Bitcoin::Secp256k1::Native.ellswift_ecdh_xonly(ellswift_theirs, ellswift_ours, priv_key, initiating)
else
ecdh_point_x32 = ellswift_ecdh_xonly(ellswift_theirs, priv_key).htb
content = initiating ? ellswift_ours.key + ellswift_theirs.key + ecdh_point_x32 :
ellswift_theirs.key + ellswift_ours.key + ecdh_point_x32
Bitcoin.tagged_hash('bip324_ellswift_xonly_ecdh', content).bth
end
end
end
end
28 changes: 28 additions & 0 deletions lib/bitcoin/secp256k1/native.rb
Expand Up @@ -60,6 +60,10 @@ def load_functions
attach_function(:secp256k1_ecdsa_recoverable_signature_parse_compact, [:pointer, :pointer, :pointer, :int], :int)
attach_function(:secp256k1_ellswift_decode, [:pointer, :pointer, :pointer], :int)
attach_function(:secp256k1_ellswift_create, [:pointer, :pointer, :pointer, :pointer], :int)
# Define function pointer
callback(:secp256k1_ellswift_xdh_hash_function, [:pointer, :pointer, :pointer, :pointer, :pointer], :int)
attach_variable(:secp256k1_ellswift_xdh_hash_function_bip324, :secp256k1_ellswift_xdh_hash_function)
attach_function(:secp256k1_ellswift_xdh, [:pointer, :pointer, :pointer, :pointer, :pointer, :int, :pointer, :pointer], :int)
end

def with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
Expand Down Expand Up @@ -252,6 +256,30 @@ def ellswift_create(priv_key)
end
end

# Compute X coordinate of shared ECDH point between elswift pubkey and privkey.
# @param [Bitcoin::BIP324::EllSwiftPubkey] their_ell_pubkey Their EllSwift public key.
# @param [Bitcoin::BIP324::EllSwiftPubkey] our_ell_pubkey Our EllSwift public key.
# @param [String] priv_key private key with hex format.
# @param [Boolean] initiating Whether your initiator or not.
# @return [String] x coordinate with hex format.
def ellswift_ecdh_xonly(their_ell_pubkey, our_ell_pubkey, priv_key, initiating)
with_context(flags: SECP256K1_CONTEXT_SIGN) do |context|
output = FFI::MemoryPointer.new(:uchar, 32)
our_ell_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, our_ell_pubkey.key)
their_ell_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, their_ell_pubkey.key)
seckey32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, priv_key.htb)
hashfp = secp256k1_ellswift_xdh_hash_function_bip324
result = secp256k1_ellswift_xdh(context, output,
initiating ? our_ell_ptr : their_ell_ptr,
initiating ? their_ell_ptr : our_ell_ptr,
seckey32,
initiating ? 0 : 1,
hashfp, nil)
raise ArgumentError, "secret was invalid or hashfp returned 0" unless result == 1
output.read_string(32).bth
end
end

private

# Calculate full public key(64 bytes) from public key(32 bytes).
Expand Down
29 changes: 27 additions & 2 deletions spec/bitcoin/bip324_spec.rb
Expand Up @@ -3,7 +3,8 @@
RSpec.describe Bitcoin::BIP324 do

let(:decode_vectors) { read_csv('bip324/ellswift_decode_test_vectors.csv') }
let(:inv_vectors) { read_csv('bip324/xswiftec_inv_test_vectors.csv') }
let(:xswiftec_inv_vectors) { read_csv('bip324/xswiftec_inv_test_vectors.csv') }
let(:packet_encoding_vectors) { read_csv('bip324/packet_encoding_test_vectors.csv') }

describe '#decode' do
context 'native', use_secp256k1: true do
Expand All @@ -24,7 +25,7 @@ def test_vectors

describe "xswiftec_inv" do
it do
inv_vectors.each do |v|
xswiftec_inv_vectors.each do |v|
8.times do |c|
r = described_class.xswiftec_inv(v['x'], v['u'], c)
if r.nil?
Expand All @@ -37,4 +38,28 @@ def test_vectors
end
end
end

describe "ellswift_xdh" do
context "native", use_secp256k1: true do
it { test_ellswift_xdh }
end

context "ruby" do
it { test_ellswift_xdh }
end

def test_ellswift_xdh
packet_encoding_vectors.each do |v|
initiating = v['in_initiating'] == "1"
our_priv = Bitcoin::Key.new(priv_key: v['in_priv_ours'])
expect(our_priv.xonly_pubkey).to eq(v['mid_x_ours'])
our_ell = Bitcoin::BIP324::EllSwiftPubkey.new(v['in_ellswift_ours'])
expect(our_ell.decode.xonly_pubkey).to eq(v['mid_x_ours'])
their_ell = Bitcoin::BIP324::EllSwiftPubkey.new(v['in_ellswift_theirs'])
expect(their_ell.decode.xonly_pubkey).to eq(v['mid_x_theirs'])
shared_x = described_class.v2_ecdh(our_priv.priv_key, their_ell, our_ell, initiating)
expect(shared_x).to eq(v['mid_shared_secret'])
end
end
end
end

0 comments on commit aeb0b1d

Please sign in to comment.