Class: Noise::Functions::DH::Secp256k1
- Inherits:
-
Object
- Object
- Noise::Functions::DH::Secp256k1
- 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
-
.decode_private_key(private_key) ⇒ Object
Decodes a private key into the scalar it denotes, rejecting the values that cannot serve as one.
- .from_private(private_key) ⇒ Object
Instance Method Summary collapse
-
#dh(private_key, public_key) ⇒ Object
Computes the ECDH shared secret for the given remote public key.
- #dhlen ⇒ Object
- #generate_keypair ⇒ Object
-
#initialize ⇒ Secp256k1
constructor
Builds the curve group once, because dh needs it on every call.
Constructor Details
#initialize ⇒ Secp256k1
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.}" 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.
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 |
#dhlen ⇒ Object
65 66 67 |
# File 'lib/noise/functions/dh/secp256k1.rb', line 65 def dhlen DHLEN end |
#generate_keypair ⇒ Object
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 |