JavaScript SubtleCrypto — есть ли способ преобразовать ключи ECDSA в немного более «портативный» формат и желательно более короткий?

Я использую браузер, встроенный в библиотеку SubtleCrypto в javascript, для генерации открытых и закрытых ключей как таковых:

let keyPair = await crypto.subtle.generateKey(
    {
        name: "ECDSA",
        namedCurve: "P-521",
    },
    true,
    ['sign', 'verify']
)
console.info(keyPair)
let exportedPublicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey)
let exportedPrivateKey = await crypto.subtle.exportKey("jwk", keyPair.privateKey)
console.info(exportedPublicKey)
console.info(exportedPrivateKey)

Это генерирует следующее (просто тестовый ключ):

Есть ли способ преобразовать экспортированные ключи в немного более «портативный» формат? Что-то, что не является JSON, не очень длинное, может быть, в шестнадцатеричном формате? Может сжатый формат?

Я попытался передать «raw» вместо «jwk» для функции exportKey, и это возвращает ошибку "Operation is not supported", потому что, вероятно, не поддерживается для этого формата.

Например, у Eth-Crypto есть publicKey.compress() способ сжать открытый ключ в достаточно короткую строку: '03a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b'

https://github.com/pubkey/eth-crypto#createidentity

Точно так же клавиши Шнорра тоже довольно короткие.

🤔 А знаете ли вы, что...
JavaScript поддерживает работу с куки и хранилищем веб-браузера для сохранения данных на клиентской стороне.


1
117
1

Ответ:

Решено

Ограничения WebCrypto:
WebCrypto не поддерживает экспорт необработанных ключей для закрытых ключей EC. В качестве обходного пути ключ можно экспортировать как JWK и извлечь параметр d.
Для открытых ключей EC поддерживается экспорт в виде необработанного ключа. Экспортированный ключ несжатый. Преобразование в сжатый ключ должно быть реализовано пользователем (при необходимости).
Кроме того, открытый необработанный ключ (несжатый или сжатый), конечно, также может быть создан из координат x и y закрытого или открытого ключа, экспортированного как JWK.

Несжатый/сжатый открытый ключ:
Открытый ключ EC соответствует точке EC (x, y).
Несжатый открытый ключ представляет собой конкатенацию маркерного байта 0x04, координаты x и координаты y (в следующем порядке): 0x04|x|y. Для P-521 он имеет длину 1 + 66 + 66 = 133 байта.
Сжатый ключ представляет собой конкатенацию байта маркера и координаты x (в указанном порядке). Байт маркера равен 0x02 для четного y: 0x02|x и 0x03 для нечетного y: 0x03|x. Для P-521 сжатый ключ имеет длину 1 + 66 = 67 байт.
Для полноты сжатый ключ содержит полную информацию для восстановления точки EC и y соответственно: по координате x сжатого ключа два возможных значения y определяются с использованием уравнения кривой. Байт маркера определяет, какое из двух значений является искомым значением y.

Возможная реализация экспорта необработанных ключей с помощью WebCrypto:

(async () => {

var keyPair = await crypto.subtle.generateKey(
    {
        name: "ECDSA",
        namedCurve: "P-521",
    },
    true,
    ['sign', 'verify']
);

rawUncomp.innerHTML = "public/uncompressed (hex):<br>" + ab2hex(await exportRawPublicKey(keyPair.publicKey, true));
rawComp.innerHTML = "public/compressed (hex):<br>" + ab2hex(await exportRawPublicKey(keyPair.publicKey, false));
rawPriv.innerHTML = "private (hex):<br>" + ab2hex(await exportRawPrivateKey(keyPair.privateKey));

// Export raw public key (compressed or uncompressed) as ArrayBuffer
async function exportRawPublicKey(publicKey, uncompressed){
    var key = await crypto.subtle.exportKey("raw", publicKey);
    if (!uncompressed) {
        var keyUncompressed = Array.from(new Uint8Array(key));
        var keySize = (keyUncompressed.length - 1)/2;
        var keyCompressed =[];
        keyCompressed.push(keyUncompressed[2 * keySize] % 2 ? 3 : 2);
        keyCompressed.push(...keyUncompressed.slice(1, keySize + 1));
        key = new Uint8Array(keyCompressed).buffer;
    }
    return key;     
}

// Export raw private key as ArrayBuffer
async function exportRawPrivateKey(privateKey){
    var keyJwk = await crypto.subtle.exportKey("jwk", privateKey);
    var rawKeyB64 = toBase64(keyJwk.d);
    return b642ab(rawKeyB64);
}

//
// Helper
//

// Base64url -> Base64
function toBase64(input) {
    input = input.replace(/-/g, '+').replace(/_/g, '/');
    return input + " = ".repeat(3 - (3 + input.length) % 4);
}

// Base64 -> ArrayBuffer
function b642ab(b64){
    return Uint8Array.from(window.atob(b64), c => c.charCodeAt(0));
}

// ArrayBuffer -> hex
function ab2hex(ab) { 
    return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}

})();
<div  style = "font-family:'Courier New', monospace;" id = "rawUncomp"></div>
<div  style = "font-family:'Courier New', monospace;" id = "rawComp"></div>
<div  style = "font-family:'Courier New', monospace;" id = "rawPriv"></div>