feat(security): Implement full ASN.1 validation for key structure verification

BREAKING CHANGE: Enhanced key validation now performs complete ASN.1 parsing

Security improvements:
- Added complete ASN.1 DER parser for full structure validation
- Implemented OID validation for algorithms and curves (P-256/P-384 only)
- Added EC point format verification (uncompressed format 0x04)
- Validate SPKI structure elements count and types
- Check key size limits to prevent DoS attacks (50-2000 bytes)
- Verify unused bits in BIT STRING (must be 0)
- Added fallback support from P-384 to P-256

This fixes high-risk vulnerability where keys with valid headers but
modified data could be accepted. Now all structural elements are validated
according to PKCS standards.

Affected methods:
- validateKeyStructure() - complete rewrite with ASN.1 parsing
- All key import/export methods now use enhanced validation
This commit is contained in:
lockbitchat
2025-08-27 12:39:18 -04:00
parent 0b01083fce
commit 6aaabbd1df

View File

@@ -1015,39 +1015,289 @@ class EnhancedSecureCryptoUtils {
}
}
// Enhanced DER/SPKI validation with improved error handling
static async validateKeyStructure(keyData, expectedAlgorithm = 'ECDH') {
// Enhanced DER/SPKI validation with full ASN.1 parsing
static async validateKeyStructure(keyData, expectedAlgorithm = 'ECDH') {
try {
if (!Array.isArray(keyData) || keyData.length === 0) {
throw new Error('Invalid key data format');
}
const keyBytes = new Uint8Array(keyData);
// Size limits to prevent DoS
if (keyBytes.length < 50) {
throw new Error('Key data too short - invalid SPKI structure');
}
if (keyBytes.length > 2000) {
throw new Error('Key data too long - possible attack');
}
// Parse ASN.1 DER structure
const asn1 = EnhancedSecureCryptoUtils.parseASN1(keyBytes);
// Validate SPKI structure
if (!asn1 || asn1.tag !== 0x30) {
throw new Error('Invalid SPKI structure - missing SEQUENCE tag');
}
// SPKI should have exactly 2 elements: AlgorithmIdentifier and BIT STRING
if (asn1.children.length !== 2) {
throw new Error(`Invalid SPKI structure - expected 2 elements, got ${asn1.children.length}`);
}
// Validate AlgorithmIdentifier
const algIdentifier = asn1.children[0];
if (algIdentifier.tag !== 0x30) {
throw new Error('Invalid AlgorithmIdentifier - not a SEQUENCE');
}
// Parse algorithm OID
const algOid = algIdentifier.children[0];
if (algOid.tag !== 0x06) {
throw new Error('Invalid algorithm OID - not an OBJECT IDENTIFIER');
}
// Validate algorithm OID based on expected algorithm
const oidBytes = algOid.value;
const oidString = EnhancedSecureCryptoUtils.oidToString(oidBytes);
// Check for expected algorithms
const validAlgorithms = {
'ECDH': ['1.2.840.10045.2.1'], // id-ecPublicKey
'ECDSA': ['1.2.840.10045.2.1'], // id-ecPublicKey (same as ECDH)
'RSA': ['1.2.840.113549.1.1.1'], // rsaEncryption
'AES-GCM': ['2.16.840.1.101.3.4.1.6', '2.16.840.1.101.3.4.1.46'] // AES-128-GCM, AES-256-GCM
};
const expectedOids = validAlgorithms[expectedAlgorithm];
if (!expectedOids) {
throw new Error(`Unknown algorithm: ${expectedAlgorithm}`);
}
if (!expectedOids.includes(oidString)) {
throw new Error(`Invalid algorithm OID: expected ${expectedOids.join(' or ')}, got ${oidString}`);
}
// For EC algorithms, validate curve parameters
if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {
if (algIdentifier.children.length < 2) {
throw new Error('Missing curve parameters for EC key');
}
const curveOid = algIdentifier.children[1];
if (curveOid.tag !== 0x06) {
throw new Error('Invalid curve OID - not an OBJECT IDENTIFIER');
}
const curveOidString = EnhancedSecureCryptoUtils.oidToString(curveOid.value);
// Only allow P-256 and P-384 curves
const validCurves = {
'1.2.840.10045.3.1.7': 'P-256', // secp256r1
'1.3.132.0.34': 'P-384' // secp384r1
};
if (!validCurves[curveOidString]) {
throw new Error(`Invalid or unsupported curve OID: ${curveOidString}`);
}
EnhancedSecureCryptoUtils.secureLog.log('info', 'EC key curve validated', {
curve: validCurves[curveOidString],
oid: curveOidString
});
}
// Validate public key BIT STRING
const publicKeyBitString = asn1.children[1];
if (publicKeyBitString.tag !== 0x03) {
throw new Error('Invalid public key - not a BIT STRING');
}
// Check for unused bits (should be 0 for public keys)
if (publicKeyBitString.value[0] !== 0x00) {
throw new Error(`Invalid BIT STRING - unexpected unused bits: ${publicKeyBitString.value[0]}`);
}
// For EC keys, validate point format
if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {
const pointData = publicKeyBitString.value.slice(1); // Skip unused bits byte
// Check for uncompressed point format (0x04)
if (pointData[0] !== 0x04) {
throw new Error(`Invalid EC point format: expected uncompressed (0x04), got 0x${pointData[0].toString(16)}`);
}
// Validate point size based on curve
const expectedSizes = {
'P-256': 65, // 1 + 32 + 32
'P-384': 97 // 1 + 48 + 48
};
// We already validated the curve above, so we can determine expected size
const curveOidString = EnhancedSecureCryptoUtils.oidToString(algIdentifier.children[1].value);
const curveName = curveOidString === '1.2.840.10045.3.1.7' ? 'P-256' : 'P-384';
const expectedSize = expectedSizes[curveName];
if (pointData.length !== expectedSize) {
throw new Error(`Invalid EC point size for ${curveName}: expected ${expectedSize}, got ${pointData.length}`);
}
}
// Additional validation: try to import the key
try {
if (!Array.isArray(keyData) || keyData.length === 0) {
throw new Error('Invalid key data format');
}
const keyBytes = new Uint8Array(keyData);
// Basic DER check
if (keyBytes[0] !== 0x30) {
throw new Error('Invalid DER structure - missing SEQUENCE tag');
}
if (keyBytes.length > 2000) {
throw new Error('Key data too long - possible attack');
}
// Try to import; await the promise
const alg = (expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH')
const algorithm = expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH'
? { name: expectedAlgorithm, namedCurve: 'P-384' }
: { name: expectedAlgorithm };
await crypto.subtle.importKey('spki', keyBytes.buffer, alg, false, expectedAlgorithm === 'ECDSA' ? ['verify'] : []);
EnhancedSecureCryptoUtils.secureLog.log('info', 'Key structure validation passed', { keyLen: keyBytes.length });
return true;
} catch (err) {
EnhancedSecureCryptoUtils.secureLog.log('error', 'Key structure validation failed', { short: err.message });
throw new Error('Invalid key structure');
const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];
await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);
} catch (importError) {
// Try P-256 as fallback for EC keys
if (expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH') {
try {
const algorithm = { name: expectedAlgorithm, namedCurve: 'P-256' };
const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];
await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);
} catch (fallbackError) {
throw new Error(`Key import validation failed: ${fallbackError.message}`);
}
} else {
throw new Error(`Key import validation failed: ${importError.message}`);
}
}
EnhancedSecureCryptoUtils.secureLog.log('info', 'Key structure validation passed', {
keyLen: keyBytes.length,
algorithm: expectedAlgorithm,
asn1Valid: true,
oidValid: true,
importValid: true
});
return true;
} catch (err) {
EnhancedSecureCryptoUtils.secureLog.log('error', 'Key structure validation failed', {
error: err.message,
algorithm: expectedAlgorithm
});
throw new Error(`Invalid key structure: ${err.message}`);
}
}
// ASN.1 DER parser helper
static parseASN1(bytes, offset = 0) {
if (offset >= bytes.length) {
return null;
}
const tag = bytes[offset];
let lengthOffset = offset + 1;
if (lengthOffset >= bytes.length) {
throw new Error('Truncated ASN.1 structure');
}
let length = bytes[lengthOffset];
let valueOffset = lengthOffset + 1;
// Handle long form length
if (length & 0x80) {
const numLengthBytes = length & 0x7f;
if (numLengthBytes > 4) {
throw new Error('ASN.1 length too large');
}
length = 0;
for (let i = 0; i < numLengthBytes; i++) {
if (valueOffset + i >= bytes.length) {
throw new Error('Truncated ASN.1 length');
}
length = (length << 8) | bytes[valueOffset + i];
}
valueOffset += numLengthBytes;
}
if (valueOffset + length > bytes.length) {
throw new Error('ASN.1 structure extends beyond data');
}
const value = bytes.slice(valueOffset, valueOffset + length);
const node = {
tag: tag,
length: length,
value: value,
children: []
};
// Parse children for SEQUENCE and SET
if (tag === 0x30 || tag === 0x31) {
let childOffset = 0;
while (childOffset < value.length) {
const child = EnhancedSecureCryptoUtils.parseASN1(value, childOffset);
if (!child) break;
node.children.push(child);
childOffset = childOffset + 1 + child.lengthBytes + child.length;
}
}
// Export public key for transmission with signature
// Calculate how many bytes were used for length encoding
node.lengthBytes = valueOffset - lengthOffset;
return node;
}
// OID decoder helper
static oidToString(bytes) {
if (!bytes || bytes.length === 0) {
throw new Error('Empty OID');
}
const parts = [];
// First byte encodes first two components
const first = Math.floor(bytes[0] / 40);
const second = bytes[0] % 40;
parts.push(first);
parts.push(second);
// Decode remaining components
let value = 0;
for (let i = 1; i < bytes.length; i++) {
value = (value << 7) | (bytes[i] & 0x7f);
if (!(bytes[i] & 0x80)) {
parts.push(value);
value = 0;
}
}
return parts.join('.');
}
// Helper to validate and sanitize OID string
static validateOidString(oidString) {
// OID format: digits separated by dots
const oidRegex = /^[0-9]+(\.[0-9]+)*$/;
if (!oidRegex.test(oidString)) {
throw new Error(`Invalid OID format: ${oidString}`);
}
const parts = oidString.split('.').map(Number);
// First component must be 0, 1, or 2
if (parts[0] > 2) {
throw new Error(`Invalid OID first component: ${parts[0]}`);
}
// If first component is 0 or 1, second must be <= 39
if ((parts[0] === 0 || parts[0] === 1) && parts[1] > 39) {
throw new Error(`Invalid OID second component: ${parts[1]} (must be <= 39 for first component ${parts[0]})`);
}
return true;
}
// Export public key for transmission with signature
static async exportPublicKeyWithSignature(publicKey, signingKey, keyType = 'ECDH') {
try {
// Validate key type
@@ -1058,7 +1308,6 @@ class EnhancedSecureCryptoUtils {
const exported = await crypto.subtle.exportKey('spki', publicKey);
const keyData = Array.from(new Uint8Array(exported));
// Validate exported key structure
await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);
// Create signed key package
@@ -1118,7 +1367,6 @@ class EnhancedSecureCryptoUtils {
throw new Error('Signed key package is too old');
}
// Validate key structure
await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);
// Verify signature
@@ -1130,29 +1378,60 @@ class EnhancedSecureCryptoUtils {
throw new Error('Invalid signature on key package - possible MITM attack');
}
// Import the key
// Import the key with fallback support
const keyBytes = new Uint8Array(keyData);
const algorithm = keyType === 'ECDH' ?
{ name: 'ECDH', namedCurve: 'P-384' }
: { name: 'ECDSA', namedCurve: 'P-384' };
const keyUsages = keyType === 'ECDH' ? [] : ['verify'];
const publicKey = await crypto.subtle.importKey(
'spki',
keyBytes,
algorithm,
false, // Non-extractable
keyUsages
);
EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully', {
keyType,
signatureValid: true,
keyAge: Math.round(keyAge / 1000) + 's'
});
return publicKey;
// Try P-384 first
try {
const algorithm = keyType === 'ECDH' ?
{ name: 'ECDH', namedCurve: 'P-384' }
: { name: 'ECDSA', namedCurve: 'P-384' };
const keyUsages = keyType === 'ECDH' ? [] : ['verify'];
const publicKey = await crypto.subtle.importKey(
'spki',
keyBytes,
algorithm,
false, // Non-extractable
keyUsages
);
EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully (P-384)', {
keyType,
signatureValid: true,
keyAge: Math.round(keyAge / 1000) + 's'
});
return publicKey;
} catch (p384Error) {
// Fallback to P-256
EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', {
error: p384Error.message
});
const algorithm = keyType === 'ECDH' ?
{ name: 'ECDH', namedCurve: 'P-256' }
: { name: 'ECDSA', namedCurve: 'P-256' };
const keyUsages = keyType === 'ECDH' ? [] : ['verify'];
const publicKey = await crypto.subtle.importKey(
'spki',
keyBytes,
algorithm,
false, // Non-extractable
keyUsages
);
EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully (P-256 fallback)', {
keyType,
signatureValid: true,
keyAge: Math.round(keyAge / 1000) + 's'
});
return publicKey;
}
} catch (error) {
EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed public key import failed', {
error: error.message,
@@ -1168,7 +1447,6 @@ class EnhancedSecureCryptoUtils {
const exported = await crypto.subtle.exportKey('spki', publicKey);
const keyData = Array.from(new Uint8Array(exported));
// Validate exported key
await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');
EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key exported', { keySize: keyData.length });
@@ -1267,13 +1545,12 @@ class EnhancedSecureCryptoUtils {
securityRisk: 'HIGH - Potential MITM attack vector'
});
// REJECT the signed package if no verifying key provided
throw new Error('CRITICAL SECURITY ERROR: Signed key package received without a verification key. ' +
'This may indicate a possible MITM attack attempt. Import rejected for security reasons.');
}
// Validate key structure
// ОБНОВЛЕНО: Используем улучшенную валидацию
await EnhancedSecureCryptoUtils.validateKeyStructure(signedPackage.keyData, signedPackage.keyType || 'ECDH');
// MANDATORY signature verification when verifyingKey is provided
@@ -1322,7 +1599,7 @@ class EnhancedSecureCryptoUtils {
namedCurve: 'P-384'
},
false, // Non-extractable
[]
keyType === 'ECDSA' ? ['verify'] : []
);
// Use WeakMap to store metadata
@@ -1345,7 +1622,7 @@ class EnhancedSecureCryptoUtils {
namedCurve: 'P-256'
},
false, // Non-extractable
[]
keyType === 'ECDSA' ? ['verify'] : []
);
// Use WeakMap to store metadata