Class: Noise::Functions::DH::Secp256k1

Inherits:
Object
  • Object
show all
Defined in:
lib/noise/functions/dh/secp256k1.rb

Overview

The secp256k1 DH function, as the Lightning Network uses it in BOLT #8.

The shared secret is SHA256 of the shared point in compressed form, not the raw X coordinate. That is what BOLT #8 specifies and what libsecp256k1's ecdh returned when this function was backed by that library, so the value on the wire is unchanged.

Constant Summary collapse

DHLEN =

Length of a compressed secp256k1 point. The 65-byte uncompressed form encodes the same point, but Noise exchanges only the compressed one, so it is rejected.

33
PRIVATE_KEY_LEN =

Length of a secp256k1 scalar, which is what a private key is.

32
CURVE =

The name OpenSSL knows the curve by.

'secp256k1'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSecp256k1

Builds the curve group once, because dh needs it on every call.

secp256k1 is a builtin curve of OpenSSL 3, but an OpenSSL restricted to a FIPS provider does not offer it. That leaves the function unusable in the same way a missing system library did, so it is reported as MissingDependencyError rather than as an OpenSSL error raised in the middle of a handshake.



28
29
30
31
32
33
# File 'lib/noise/functions/dh/secp256k1.rb', line 28

def initialize
  @group = OpenSSL::PKey::EC::Group.new(CURVE)
rescue OpenSSL::PKey::EC::Group::Error => e
  raise Noise::Exceptions::MissingDependencyError,
        "OpenSSL does not offer the #{CURVE} curve: #{e.message}"
end

Class Method Details

.decode_private_key(private_key) ⇒ Object

Decodes a private key into the scalar it denotes, rejecting the values that cannot serve as one. Both entry points that take a private key go through this, so a key from_private accepts is one dh can use.

Rejected are a length other than PRIVATE_KEY_LEN, zero, and anything at or above the group order: those scalars multiply every point to infinity, whose compressed encoding is the single byte 0x00. Without the check from_private would hand back a Noise::Key holding that one byte as its public key, and the handshake it is used in would fail only once the key reached the wire. A private key belongs to the caller rather than to the peer, so a bad one raises ArgumentError and not InvalidPublicKeyError.

Raises:

  • (ArgumentError)


85
86
87
88
89
90
91
92
93
94
95
# File 'lib/noise/functions/dh/secp256k1.rb', line 85

def self.decode_private_key(private_key)
  unless private_key.bytesize == PRIVATE_KEY_LEN
    raise ArgumentError, "private key must be #{PRIVATE_KEY_LEN} bytes"
  end

  scalar = ECDSA::Format::IntegerOctetString.decode(private_key)
  order = ECDSA::Group::Secp256k1.order
  raise ArgumentError, 'private key is out of range' unless scalar.between?(1, order - 1)

  scalar
end

.from_private(private_key) ⇒ Object



69
70
71
72
73
# File 'lib/noise/functions/dh/secp256k1.rb', line 69

def self.from_private(private_key)
  scalar = decode_private_key(private_key)
  point = ECDSA::Group::Secp256k1.generator.multiply_by_scalar(scalar)
  Noise::Key.new(private_key, ECDSA::Format::PointOctetString.encode(point, compression: true))
end

Instance Method Details

#dh(private_key, public_key) ⇒ Object

Computes the ECDH shared secret for the given remote public key.

Every way a peer-supplied key can be unusable raises InvalidPublicKeyError, matching the other DH functions: a length other than DHLEN, and an encoding that names no point on the curve. A private key the caller owns is not the peer's fault, so a malformed one raises ArgumentError instead, as it did when the gem parsed it.



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/noise/functions/dh/secp256k1.rb', line 51

def dh(private_key, public_key)
  raise Noise::Exceptions::InvalidPublicKeyError, public_key unless public_key.bytesize == DHLEN

  scalar = OpenSSL::BN.new(self.class.decode_private_key(private_key))
  shared = parse_public_key(public_key).mul(scalar)
  # A backstop rather than a reachable case: secp256k1 has cofactor 1, so a point that
  # parsed has order n, and the scalar is already known to be in [1, n-1]. It stays because
  # the cost is one comparison and the failure it guards is severe - the compressed
  # encoding of infinity is the single byte 0x00, whose SHA256 anyone can precompute.
  raise Noise::Exceptions::InvalidPublicKeyError, public_key if shared.infinity?

  OpenSSL::Digest.digest('SHA256', shared.to_octet_string(:compressed))
end

#dhlenObject



65
66
67
# File 'lib/noise/functions/dh/secp256k1.rb', line 65

def dhlen
  DHLEN
end

#generate_keypairObject



35
36
37
38
39
40
41
42
43
# File 'lib/noise/functions/dh/secp256k1.rb', line 35

def generate_keypair
  group = ECDSA::Group::Secp256k1
  private_key = 1 + SecureRandom.random_number(group.order - 1)
  public_key = group.generator.multiply_by_scalar(private_key)
  Noise::Key.new(
    ECDSA::Format::IntegerOctetString.encode(private_key, PRIVATE_KEY_LEN),
    ECDSA::Format::PointOctetString.encode(public_key, compression: true)
  )
end