Closed
Description
Our WebCryptoAPI implementation supports import of compressed point format EC keys in both raw
, and spki
forms. That is an optional to implement part of the API.
The specification however requires that when keys are exported they unconditionally use the uncompressed point format. This is currently not the case in Node.js when compressed point spki
was used to import the CryptoKey.
The following script demonstrates the issue.
Script
import * as assert from 'assert';
const { subtle } = globalThis.crypto;
const keyData = {
'P-256': {
uncompressed: Buffer.from([
48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63, 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172, 174, 93, 209, 206, 3, 117, 82, 212, 129, 69, 12, 227, 155, 77, 16, 149, 112, 27, 23, 91, 250, 179, 75, 142, 108, 9, 158, 24, 241, 193, 152, 53, 131, 97, 232,
]),
compressed: Buffer.from([
48, 57, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 34, 0, 2, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63, 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172, 174, 93, 209,
]),
},
'P-384': {
uncompressed: Buffer.from([
48, 118, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 3, 98, 0, 4, 33, 156, 20, 214, 102, 23, 179, 110, 198, 216, 133, 107, 56, 91, 115, 167, 77, 52, 79, 216, 174, 117, 239, 4, 100, 53, 221, 165, 78, 59, 68, 189, 95, 189, 235, 209, 208, 141, 214, 158, 45, 125, 193, 220, 33, 140, 180, 53, 189, 40, 19, 140, 199, 120, 51, 122, 132, 47, 107, 214, 27, 36, 14, 116, 36, 159, 36, 102, 124, 42, 88, 16, 167, 107, 252, 40, 224, 51, 95, 136, 166, 80, 29, 236, 1, 151, 109, 168, 90, 251, 0, 134, 156, 182, 172, 232,
]),
compressed: Buffer.from([
48, 70, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 3, 50, 0, 2, 33, 156, 20, 214, 102, 23, 179, 110, 198, 216, 133, 107, 56, 91, 115, 167, 77, 52, 79, 216, 174, 117, 239, 4, 100, 53, 221, 165, 78, 59, 68, 189, 95, 189, 235, 209, 208, 141, 214, 158, 45, 125, 193, 220, 33, 140, 180, 53,
]),
},
'P-521': {
uncompressed: Buffer.from([
48, 129, 155, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 3, 129, 134, 0, 4, 1, 86, 244, 121, 248, 223, 30, 32, 167, 255, 192, 76, 228, 32, 195, 225, 84, 174, 37, 25, 150, 190, 228, 47, 3, 75, 132, 212, 27, 116, 63, 52, 228, 95, 49, 27, 129, 58, 156, 222, 200, 205, 165, 155, 187, 189, 49, 212, 96, 179, 41, 37, 33, 231, 193, 183, 34, 229, 102, 124, 3, 219, 47, 174, 117, 63, 1, 80, 23, 54, 207, 226, 71, 57, 67, 32, 216, 228, 175, 194, 253, 57, 181, 169, 51, 16, 97, 184, 30, 34, 65, 40, 43, 158, 23, 137, 24, 34, 181, 183, 158, 5, 47, 69, 151, 181, 150, 67, 253, 57, 55, 156, 81, 189, 81, 37, 196, 244, 139, 195, 240, 37, 206, 60, 211, 105, 83, 40, 108, 203, 56, 251,
]),
compressed: Buffer.from([
48, 88, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 3, 68, 0, 3, 1, 86, 244, 121, 248, 223, 30, 32, 167, 255, 192, 76, 228, 32, 195, 225, 84, 174, 37, 25, 150, 190, 228, 47, 3, 75, 132, 212, 27, 116, 63, 52, 228, 95, 49, 27, 129, 58, 156, 222, 200, 205, 165, 155, 187, 189, 49, 212, 96, 179, 41, 37, 33, 231, 193, 183, 34, 229, 102, 124, 3, 219, 47, 174, 117, 63,
]),
},
};
const curves = Object.keys(keyData);
const testVectors = [
{
name: 'ECDSA',
privateUsages: ['sign'],
publicUsages: ['verify'],
},
{
name: 'ECDH',
privateUsages: ['deriveKey', 'deriveBits'],
publicUsages: [],
},
];
async function testRoundTripSpki({ name, publicUsages }, namedCurve) {
const { compressed, uncompressed } = keyData[namedCurve];
const key = await subtle.importKey(
'spki',
compressed,
{ name, namedCurve },
true,
publicUsages
);
const spki = await subtle.exportKey('spki', key);
assert.deepStrictEqual(
Buffer.from(spki).toString('hex'),
uncompressed.toString('hex')
);
}
async function testRoundTripRaw({ name, publicUsages }, namedCurve) {
let { compressed, uncompressed } = keyData[namedCurve];
let i = uncompressed.indexOf(Buffer.from([0x00, 0x04])) + 1;
uncompressed = uncompressed.subarray(i);
i =
compressed.indexOf(Buffer.from([0x00, 0x02])) + 1 ||
compressed.indexOf(Buffer.from([0x00, 0x03])) + 1;
compressed = compressed.subarray(i);
const key = await subtle.importKey(
'raw',
compressed,
{ name, namedCurve },
true,
publicUsages
);
const raw = await subtle.exportKey('raw', key);
assert.strictEqual(
Buffer.from(raw).toString('hex'),
uncompressed.toString('hex')
);
}
for (const namedCurve of curves) {
for (const vector of testVectors) {
await testRoundTripSpki(vector, namedCurve).then(
() =>
console.log(namedCurve, 'spki'.padEnd(4), vector.name.padEnd(5), '✅'),
() =>
console.log(namedCurve, 'spki'.padEnd(4), vector.name.padEnd(5), '❌')
);
await testRoundTripRaw(vector, namedCurve).then(
() =>
console.log(namedCurve, 'raw'.padEnd(4), vector.name.padEnd(5), '✅'),
() =>
console.log(namedCurve, 'raw'.padEnd(4), vector.name.padEnd(5), '❌')
);
console.log('\n');
}
}
P-256 spki ECDSA ❌
P-256 raw ECDSA ✅
P-256 spki ECDH ❌
P-256 raw ECDH ✅
P-384 spki ECDSA ❌
P-384 raw ECDSA ✅
P-384 spki ECDH ❌
P-384 raw ECDH ✅
P-521 spki ECDSA ❌
P-521 raw ECDSA ✅
P-521 spki ECDH ❌
P-521 raw ECDH ✅
I'm opening this issue so that I have an issue tracker link to add to the WPT status file once it gets updated.