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 installFor more robust crypto operations, consider:
npm install elliptic react-native-randombytes buffer
# Link native dependencies
react-native link react-native-randombytes2. 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.goThis will confirm if the EC point is on the P-256 curve.
References
- NIST P-256 Curve Specification
- RFC 5480 - ECC SubjectPublicKeyInfo Format
- React Native Keychain Documentation
- Elliptic Library Documentation
Support
If you encounter issues with key generation:
- Enable debug logging with
DEBUG_BIOMETRIC=trueon the server - Use
check_ec_point.goto validate your public keys - Ensure you're using P-256 curve (not secp256k1 from Bitcoin)
- Verify your Base64 encoding/decoding is correct
- Test with the provided test script
test_valid_ecdsa.sh