From b23f560a919a791a6a7d6dece52ff9a764955098 Mon Sep 17 00:00:00 2001 From: Lrifton92 Date: Thu, 11 Jun 2026 17:14:02 +0200 Subject: [PATCH 1/2] btcutil: reject out-of-range private keys in DecodeWIF DecodeWIF did not validate that the decoded private key falls within the valid range [1, N-1] for a secp256k1 private key. The raw 32-byte key material was passed straight to btcec.PrivKeyFromBytes, which reduces the scalar modulo the group order N and clamps to zero, returning no error. As a result, a WIF encoding a private key of zero, the group order N, or any value >= N was silently accepted. For keys >= N this is particularly dangerous: DecodeWIF returned a private key that differs from the one actually encoded in the WIF (e.g. a WIF for N+5 decoded to the key 5), so an application importing such a WIF would obtain a valid-looking but wrong key pair, with no indication that anything was off. This is also inconsistent with hdkeychain.NewKeyFromString in the same package, which already rejects private keys outside [1, N-1]. Validate the range using a ModNScalar (constant time): SetByteSlice reports an overflow when the value is >= N, and IsZero covers the zero key. Out-of-range keys now return ErrMalformedPrivateKey. Add regression test cases (zero, N, and N+5) to TestEncodeDecodeWIF. Signed-off-by: Lrifton92 --- btcutil/wif.go | 15 +++++++++++++++ btcutil/wif_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/btcutil/wif.go b/btcutil/wif.go index 4ea06aa5..c8aab1b8 100644 --- a/btcutil/wif.go +++ b/btcutil/wif.go @@ -118,6 +118,21 @@ func DecodeWIF(wif string) (*WIF, error) { netID := decoded[0] privKeyBytes := decoded[1 : 1+btcec.PrivKeyBytesLen] + + // Ensure the private key is within the valid range for a secp256k1 + // private key, that is [1, N-1]. Without this check, a WIF encoding a + // key of zero or one greater than or equal to the group order N is + // silently accepted: btcec.PrivKeyFromBytes reduces the scalar modulo + // N, so DecodeWIF would otherwise return a private key that differs from + // the one actually encoded in the WIF (or the all-zero key) without + // reporting an error. + var keyScalar btcec.ModNScalar + if overflow := keyScalar.SetByteSlice(privKeyBytes); overflow || + keyScalar.IsZero() { + + return nil, ErrMalformedPrivateKey + } + privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) return &WIF{privKey, compress, netID}, nil } diff --git a/btcutil/wif_test.go b/btcutil/wif_test.go index 37d3e46e..93b950aa 100644 --- a/btcutil/wif_test.go +++ b/btcutil/wif_test.go @@ -122,6 +122,30 @@ func TestEncodeDecodeWIF(t *testing.T) { wif: "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTj", err: address.ErrChecksumMismatch, }, + { + // A WIF encoding a private key of zero, which is + // outside the valid range [1, N-1] for a secp256k1 + // private key. + name: "decodeZeroPrivKeyWif", + wif: "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAbuatmU", + err: ErrMalformedPrivateKey, + }, + { + // A WIF encoding a private key equal to the group order + // N, which is outside the valid range [1, N-1]. + name: "decodeOrderNPrivKeyWif", + wif: "5Km2kuu7vtFDPpxywn4u3NLpbr5jKpTB3jsuDU2KYEqetwr388P", + err: ErrMalformedPrivateKey, + }, + { + // A WIF encoding a private key of N+5, which is outside + // the valid range [1, N-1]. Before validation was + // added, this was silently reduced modulo N and decoded + // to a different private key (5) without any error. + name: "decodeAboveOrderNPrivKeyWif", + wif: "5Km2kuu7vtFDPpxywn4u3NLpbr5jKpTB3jsuDU2KYEqeuVhzTbv", + err: ErrMalformedPrivateKey, + }, } for _, invalidCase := range invalidDecodeCases { From f10224dd137db1edf69bb9f02e86fcfc0b9bdded Mon Sep 17 00:00:00 2001 From: Lrifton92 Date: Wed, 24 Jun 2026 11:55:30 +0200 Subject: [PATCH 2/2] btcutil: zero range-check scalar before returning from DecodeWIF Wipe the secp256k1 scalar used for the [1, N-1] range validation as soon as DecodeWIF returns, so the decoded private key value does not linger in this local after use. Addresses review feedback on #2545. --- btcutil/wif.go | 1 + 1 file changed, 1 insertion(+) diff --git a/btcutil/wif.go b/btcutil/wif.go index c8aab1b8..36600bd3 100644 --- a/btcutil/wif.go +++ b/btcutil/wif.go @@ -127,6 +127,7 @@ func DecodeWIF(wif string) (*WIF, error) { // the one actually encoded in the WIF (or the all-zero key) without // reporting an error. var keyScalar btcec.ModNScalar + defer keyScalar.Zero() if overflow := keyScalar.SetByteSlice(privKeyBytes); overflow || keyScalar.IsZero() {