Skip to content

Commit 1d439ae

Browse files
committed
fix: mitigate CVE-2024-48930 by validating secp256k1 curve points, enforce secp256k1 v5.0.1
TICKET: DX-1569
1 parent 4a8b2be commit 1d439ae

File tree

5 files changed

+68
-1
lines changed

5 files changed

+68
-1
lines changed

modules/bitgo/test/ecdh.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'should';
55
import { bip32 } from '@bitgo/utxo-lib';
66
import * as crypto from 'crypto';
77
import * as utxolib from '@bitgo/utxo-lib';
8+
import * as secp256k1 from 'secp256k1';
89

910
import { getSharedSecret, signMessageWithDerivedEcdhKey, verifyEcdhSignature } from '@bitgo/sdk-core';
1011
import { TestBitGo } from '@bitgo/sdk-test';
@@ -82,4 +83,26 @@ describe('ECDH utils', () => {
8283
isVerify = verifyEcdhSignature(JSON.stringify(message), signedMessage.toString('hex'), derivedPubKey);
8384
isVerify.should.be.false();
8485
});
86+
87+
it('getSharedSecret rejects invalid public keys for CVE-2024-48930 mitigation', function () {
88+
const validPrivateKey = Buffer.alloc(32, 1); // Create a dummy private key
89+
const validPublicKey = Buffer.from(secp256k1.publicKeyCreate(validPrivateKey));
90+
91+
// Valid key should work
92+
const sharedSecret = getSharedSecret(validPrivateKey, validPublicKey);
93+
assert.strictEqual(sharedSecret.length, 32);
94+
95+
// Test with invalid public key
96+
const invalidPublicKey = Buffer.from('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 'hex');
97+
// Modify last byte to make it invalid but keep the format valid
98+
invalidPublicKey[32] = invalidPublicKey[32] ^ 0xff;
99+
100+
try {
101+
getSharedSecret(validPrivateKey, invalidPublicKey);
102+
assert.fail('Should have thrown error for invalid public key');
103+
} catch (e) {
104+
// The test passes as long as an error is thrown
105+
assert.ok(true, 'Error was thrown as expected');
106+
}
107+
});
85108
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"61f039aad587c2000745c687373e0fa9": "{\"iv\":\"O74H8BBv86GBpoTzjVyzWw==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"7n8pAjXCfug=\",\"ct\":\"14MjiKBksaaayrwuc/w8vJ5C3yflQ15//dhLiOgYVqjhJJ7iKrcrjtgfLoI3+MKLaKCycNKi6vTs2xs8xJeSm/XhsOE9EfapkfGHdYuf4C6O1whNOyugZ0ZSOA/buDC3rvBbvCNtLDOxN5XWJN/RADOnZdHuVGk=\"}"
2+
"61f039aad587c2000745c687373e0fa9": "{\"iv\":\"v0AKnjfRjwYGs6fLm4nPIg==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"LiHJlj8SDlQ=\",\"ct\":\"yIfdY5ufDAPgfMpXPPPMlusy1MyAmBX5YsBcm4rO8wGsjBFsvvhJHdwaOTXEhwkg+HkZFzoQw2RxDx6ghfKYs/x6SnbY7lSALt9Q/pCYa+80p4/WfsS4OKpdfJ/lbtj9eDSTe8wMvV5Cc/b65qx7rAitFkdF4aw=\"}"
33
}

modules/sdk-core/src/bitgo/ecdh.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ import * as assert from 'assert';
1616
import * as secp256k1 from 'secp256k1';
1717
import { ECPairInterface, BIP32Interface, bip32, ECPair } from '@bitgo/utxo-lib';
1818

19+
/**
20+
* Validate that a public key is on the secp256k1 curve
21+
* This is a defense-in-depth check to prevent invalid curve attacks
22+
* @param publicKey Buffer containing the public key to validate
23+
* @returns boolean true if the key is valid, false otherwise
24+
*/
25+
function validatePublicKey(publicKey: Buffer): boolean {
26+
try {
27+
// Validates that the public key is a point on the secp256k1 curve
28+
return secp256k1.publicKeyVerify(publicKey);
29+
} catch (e) {
30+
return false;
31+
}
32+
}
33+
1934
/**
2035
* Calculate the Elliptic Curve Diffie Hellman
2136
* @param privateKey HDNode of private key
@@ -55,8 +70,15 @@ export function getSharedSecret(
5570
assert.strictEqual(privateKey.length, 32);
5671
assert.strictEqual(publicKey.length, 33);
5772

73+
// Extra validation to ensure the public key is on the curve
74+
// This provides defense-in-depth against CVE-2024-48930 (GHSA-584q-6j8j-r5pm)
75+
if (!validatePublicKey(publicKey)) {
76+
throw new Error(`invalid public key: not on secp256k1 curve`);
77+
}
78+
5879
// FIXME(BG-34386): we should use `secp256k1.ecdh()` in the future
5980
// see discussion here https://github.com/bitcoin-core/secp256k1/issues/352
81+
// Additional validation has been added above to protect against CVE-2024-48930
6082
const buffer = Buffer.from(secp256k1.publicKeyTweakMul(publicKey, privateKey))
6183
// remove leading parity bit
6284
.slice(1);

package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@
8383
"json5": "^2.2.2",
8484
"ua-parser-js": ">0.7.30 <0.8.0",
8585
"secp256k1": "5.0.1",
86+
"**/ethereum-cryptography/secp256k1": "5.0.1",
87+
"**/hdkey/secp256k1": "5.0.1",
88+
"**/eccrypto/secp256k1": "5.0.1",
89+
"**/bitcoinjs-message/secp256k1": "5.0.1",
90+
"**/bip322-js/*/bitcoinjs-message/secp256k1": "5.0.1",
8691
"socks": "2.7.3",
8792
"web3-utils": "4.2.1",
8893
"@polkadot/api": "14.1.1",
@@ -142,5 +147,21 @@
142147
"terser": "^5.14.2",
143148
"tmp": "^0.2.3"
144149
},
150+
"overrides": {
151+
"ethereum-cryptography": {
152+
"secp256k1": "5.0.1"
153+
},
154+
"hdkey": {
155+
"secp256k1": "5.0.1"
156+
},
157+
"eccrypto": {
158+
"secp256k1": "5.0.1"
159+
},
160+
"bip322-js": {
161+
"bitcoinjs-message": {
162+
"secp256k1": "5.0.1"
163+
}
164+
}
165+
},
145166
"packageManager": "[email protected]"
146167
}

scripts/check-package-dependencies.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const options = {
3434
'sass-loader',
3535
'style-loader',
3636
'ts-loader',
37+
'secp256k1',
3738
],
3839
specials: [
3940
depcheck.special.babel,

0 commit comments

Comments
 (0)