React Native ECDSA Key Generation Guide

This guide provides proper implementation for generating valid ECDSA P-256 keys in React Native for biometric device registration.

Problem Summary

The current mobile implementation is generating invalid ECDSA public keys where the elliptic curve points are not on the P-256 curve, causing signature verification failures with error: x509: failed to unmarshal elliptic curve point.

Correct Implementation

1. Install Required Libraries

npm install react-native-keychain react-native-crypto
# or
yarn add react-native-keychain react-native-crypto

# For iOS
cd ios && pod install

For more robust crypto operations, consider:

npm install elliptic react-native-randombytes buffer
# Link native dependencies
react-native link react-native-randombytes

2. Generate Valid ECDSA P-256 Keys

import { ec as EC } from 'elliptic';
import { randomBytes } from 'react-native-randombytes';
import { Buffer } from 'buffer';

class BiometricKeyManager {
  constructor() {
    // Use P-256 curve (also known as secp256r1 or prime256v1)
    this.ec = new EC('p256');
  }

  /**
   * Generate a new ECDSA P-256 key pair
   * @returns {Object} Object containing publicKey and privateKey
   */
  async generateKeyPair() {
    return new Promise((resolve, reject) => {
      try {
        // Generate random bytes for entropy
        randomBytes(32, (error, bytes) => {
          if (error) {
            reject(error);
            return;
          }

          // Generate key pair
          const keyPair = this.ec.genKeyPair({
            entropy: bytes
          });

          // Validate the generated key
          if (!this.validateKeyPair(keyPair)) {
            reject(new Error('Generated invalid key pair'));
            return;
          }

          // Get public key in correct format
          const publicKey = this.exportPublicKeyPEM(keyPair);

          // Store private key securely (see section 3)
          const privateKeyHex = keyPair.getPrivate('hex');

          resolve({
            publicKey,
            privateKeyHex,
            keyPair // Keep for signing
          });
        });
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * Validate that the key pair is correct
   * @param {Object} keyPair - The EC key pair to validate
   * @returns {boolean} True if valid
   */
  validateKeyPair(keyPair) {
    try {
      const pub = keyPair.getPublic();

      // Check if point is on curve
      if (!this.ec.curve.validate(pub)) {
        console.error('Public key point is not on P-256 curve!');
        return false;
      }

      // Verify we can sign and verify with the key
      const testMessage = 'test';
      const signature = keyPair.sign(testMessage);
      const isValid = keyPair.verify(testMessage, signature);

      if (!isValid) {
        console.error('Key pair failed sign/verify test');
        return false;
      }

      return true;
    } catch (error) {
      console.error('Key validation failed:', error);
      return false;
    }
  }

  /**
   * Export public key in PEM format (X.509 SubjectPublicKeyInfo)
   * @param {Object} keyPair - The EC key pair
   * @returns {string} PEM formatted public key
   */
  exportPublicKeyPEM(keyPair) {
    const pub = keyPair.getPublic();

    // Get uncompressed public key bytes (0x04 + X + Y)
    const pubBytes = Buffer.from(pub.encode('array', false));

    // Build ASN.1 DER structure for SubjectPublicKeyInfo
    const algorithmIdentifier = Buffer.from([
      0x30, 0x13, // SEQUENCE (19 bytes)
      0x06, 0x07, // OID (7 bytes)
      0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID: 1.2.840.10045.2.1 (ecPublicKey)
      0x06, 0x08, // OID (8 bytes)
      0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07 // OID: 1.2.840.10045.3.1.7 (P-256)
    ]);

    // BIT STRING containing the public key
    const bitString = Buffer.concat([
      Buffer.from([0x03, pubBytes.length + 1, 0x00]), // BIT STRING header
      pubBytes
    ]);

    // Complete SubjectPublicKeyInfo SEQUENCE
    const spki = Buffer.concat([
      Buffer.from([0x30, algorithmIdentifier.length + bitString.length]), // SEQUENCE header
      algorithmIdentifier,
      bitString
    ]);

    // Convert to PEM
    const base64 = spki.toString('base64');
    const pem = `-----BEGIN PUBLIC KEY-----\n${base64.match(/.{1,64}/g).join('\n')}\n-----END PUBLIC KEY-----`;

    return pem;
  }

  /**
   * Sign a challenge for device registration
   * @param {Object} keyPair - The EC key pair
   * @param {string} challenge - Base64 encoded challenge from server
   * @returns {string} Base64 encoded signature
   */
  signChallenge(keyPair, challenge) {
    // Decode base64 challenge
    const challengeBytes = Buffer.from(challenge, 'base64');

    // Sign the challenge
    const signature = keyPair.sign(challengeBytes);

    // Get signature in DER format (required for server verification)
    const derSig = signature.toDER();

    // Return base64 encoded signature
    return Buffer.from(derSig).toString('base64');
  }
}

// Usage Example
async function registerDevice() {
  const keyManager = new BiometricKeyManager();

  try {
    // Step 1: Generate key pair
    console.log('Generating ECDSA P-256 key pair...');
    const { publicKey, privateKeyHex, keyPair } = await keyManager.generateKeyPair();

    console.log('Public Key (PEM):');
    console.log(publicKey);

    // Step 2: Send public key to server to get challenge
    const challengeResponse = await fetch('https://api.example.com/api/v1/auth/devices/register/challenge', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${userToken}`
      },
      body: JSON.stringify({
        deviceName: 'iPhone 13 Pro',
        deviceType: 'ios',
        deviceFingerprint: await getDeviceFingerprint(),
        keyAlgorithm: 'ES256',
        publicKey: publicKey
      })
    });

    const { sessionId, challenge } = await challengeResponse.json();

    // Step 3: Sign the challenge
    console.log('Signing challenge...');
    const signedChallenge = keyManager.signChallenge(keyPair, challenge);

    // Step 4: Complete registration
    const verifyResponse = await fetch('https://api.example.com/api/v1/auth/devices/register/verify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${userToken}`
      },
      body: JSON.stringify({
        sessionId: sessionId,
        signedChallenge: signedChallenge
      })
    });

    const result = await verifyResponse.json();

    if (result.success) {
      console.log('Device registered successfully!');
      // Store private key securely (see section 3)
      await storePrivateKeySecurely(privateKeyHex);
    }

  } catch (error) {
    console.error('Registration failed:', error);
  }
}

3. Secure Key Storage

iOS - Using Keychain

import * as Keychain from 'react-native-keychain';

async function storePrivateKeySecurely(privateKeyHex) {
  try {
    await Keychain.setInternetCredentials(
      'com.yourapp.biometric',
      'private_key',
      privateKeyHex,
      {
        accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
        authenticatePrompt: 'Authenticate to access your biometric key',
        authenticationPrompt: {
          title: 'Authentication Required',
          subtitle: 'Access your biometric key',
          description: 'Use biometric or device passcode',
          cancel: 'Cancel'
        }
      }
    );
    console.log('Private key stored securely in Keychain');
  } catch (error) {
    console.error('Failed to store key:', error);
  }
}

async function retrievePrivateKey() {
  try {
    const credentials = await Keychain.getInternetCredentials('com.yourapp.biometric');
    if (credentials) {
      return credentials.password; // This is the privateKeyHex
    }
    return null;
  } catch (error) {
    console.error('Failed to retrieve key:', error);
    return null;
  }
}

Android - Using Android Keystore

import { NativeModules } from 'react-native';

// You'll need to create a native module for Android Keystore
const { AndroidKeystore } = NativeModules;

async function storePrivateKeySecurely(privateKeyHex) {
  try {
    await AndroidKeystore.storeKey('biometric_private_key', privateKeyHex, {
      requireAuthentication: true,
      invalidateOnEnrollment: true
    });
    console.log('Private key stored in Android Keystore');
  } catch (error) {
    console.error('Failed to store key:', error);
  }
}

4. Device Fingerprint Generation

import DeviceInfo from 'react-native-device-info';
import { createHash } from 'crypto';

async function getDeviceFingerprint() {
  const deviceId = DeviceInfo.getUniqueId();
  const deviceModel = DeviceInfo.getModel();
  const systemVersion = DeviceInfo.getSystemVersion();
  const appVersion = DeviceInfo.getVersion();

  // Create a stable fingerprint
  const fingerprintData = `${deviceId}-${deviceModel}-${systemVersion}-${appVersion}`;

  // Hash it for consistency
  const hash = createHash('sha256');
  hash.update(fingerprintData);
  return hash.digest('hex');
}

Common Issues and Solutions

Issue 1: Invalid Curve Point Error

Error: x509: failed to unmarshal elliptic curve point

Cause: The public key's EC point is not on the P-256 curve.

Solution:

  • Always validate keys after generation using validateKeyPair()
  • Ensure you're using the correct curve ('p256', not 'secp256k1' or others)
  • Verify the public key export format is correct

Issue 2: Signature Verification Failure

Error: signature verification failed

Cause: Incorrect signature format or wrong data being signed.

Solution:

  • Ensure signature is in DER format (use signature.toDER())
  • Decode base64 challenge before signing
  • Don't hash the challenge - sign it directly

Issue 3: Key Storage Security

Problem: Keys stored insecurely can be compromised.

Solution:

  • iOS: Use Keychain with biometric protection
  • Android: Use Android Keystore with hardware backing when available
  • Never store keys in plain text or SharedPreferences/UserDefaults

Testing Your Implementation

Use this test to verify your key generation:

async function testKeyGeneration() {
  const keyManager = new BiometricKeyManager();

  console.log('Testing key generation...');
  const { publicKey, keyPair } = await keyManager.generateKeyPair();

  // Test 1: Validate PEM format
  if (!publicKey.includes('-----BEGIN PUBLIC KEY-----')) {
    console.error('āŒ Invalid PEM format');
    return false;
  }
  console.log('āœ… Valid PEM format');

  // Test 2: Validate key is on curve
  if (!keyManager.validateKeyPair(keyPair)) {
    console.error('āŒ Key validation failed');
    return false;
  }
  console.log('āœ… Key is on P-256 curve');

  // Test 3: Test signing
  const testChallenge = Buffer.from('test-challenge').toString('base64');
  const signature = keyManager.signChallenge(keyPair, testChallenge);

  if (!signature || signature.length < 50) {
    console.error('āŒ Signature generation failed');
    return false;
  }
  console.log('āœ… Signature generation successful');

  console.log('\nāœ… All tests passed!');
  console.log('Public Key:', publicKey);
  return true;
}

Server-Side Validation

You can validate generated keys using the provided Go tool:

// Use check_ec_point.go to validate the public key
go run check_ec_point.go

This will confirm if the EC point is on the P-256 curve.

References

Support

If you encounter issues with key generation:

  1. Enable debug logging with DEBUG_BIOMETRIC=true on the server
  2. Use check_ec_point.go to validate your public keys
  3. Ensure you're using P-256 curve (not secp256k1 from Bitcoin)
  4. Verify your Base64 encoding/decoding is correct
  5. Test with the provided test script test_valid_ecdsa.sh