如何使用 RSA 私钥和 crypto.subtle 进行签名?

问题描述 投票:0回答:1

我有生成 RSA 签名的 Python 代码,总结如下:

import rsa
private_key = rsa.PrivateKey(key[0], key[1], key[2], key[3], key[4])
signature = rsa.sign(data.encode(), private_key, 'SHA-256')

密钥以

key
数组中包含 5 个项目的数组形式提供,一切正常。

我需要在 JavaScript 中执行相同的操作,最好使用 crypto.subtle,但我不知道如何加载私钥,从

key
数组中的这 5 项开始。

这个答案开始,我尝试了下面的代码。它创建一个私钥,转换为 jwk,并显示结果。我可以在这里看到所有组件的插槽:

decrypted = This text will be encoded UTF8 and may contain special characters like § and €.
jwk = {
    "alg": "RSA-OAEP-256",
    "d": "SlJj0ExIomKmmBhG8q8SM1s2sWG6gdQMjs6MEeluRT_1c2v79cq2Dum5y_-UBl8x8TUKPKSLpCLs-GXkiVKgHXrFlqoN-OYQArG2EUWzuODwczdYPhhupBXwR3oX4g41k_BsYfQfZBVzBFEJdWrIDLyAUFWNlfdGIj2BTiAoySfyqmamvmW8bsvc8coiGlZ28UC85_Xqx9wOzjeGoRkCH7PcTMlc9F7SxSthwX_k1VBXmNOHa-HzGOgO_W3k1LDqJbq2wKjZTW3iVEg2VodjxgBLMm0MueSGoI6IuaZSPMyFEM3gGvC2-cDBI2SL_amhiTUa_VDlTVw_IKbSuar9uQ",
    "dp": "iE6VAxJknM4oeakBiL6JTdXEReY-RMu7e4F2518_lJmoe5CaTCL3cnzFTgFyQAYIvD0MIgSzNMkl6Ni6QEY1y1fIpTVIIAZLWAzZLXPA6yTIJbWsmo9xzXdiIJQ-a433NnClkYDne_xpSnB2kxJ263mIX0drFq1i8STsqDH7lVs",
    "dq": "VqUJsxXqpTQt8Sjxo-UE3y21UM9U2me0_iHQ2DE9eA8rw-D6ADVRZLLgyi4aD-HOR0dqP2J_IuUJfn3xrkmhPhLTH9l5Ud38s0jya2NxHMPpwx17uB0Vuktvk1KMgDKuwgBfiHG-meqI5hF4-RUjPSIsbOKJoxt8zCWSvG-b8tE",
    "e": "AQAB",
    "ext": true,
    "key_ops": [
        "decrypt"
    ],
    "kty": "RSA",
    "n": "unF5aDa6HCfLMMI_MZLT5hDk304CU-ypFMFiBjowQdUMQKYHZ-fklB7GpLxCatxYJ_hZ7rjfHH3Klq20_Y1EbYDRopyTSfkrTzPzwsX4Ur_l25CtdQldhHCTMgwf_Ev_buBNobfzdZE-Dhdv5lQwKtjI43lDKvAi5kEet2TFwfJcJrBiRJeEcLfVgWTXGRQn7gngWKykUu5rS83eAU1xH9FLojQfyia89_EykiOO7_3UWwd-MATZ9HLjSx2_Lf3g2jr81eifEmYDlri_OZp4OhZu-0Bo1LXloCTe-vmIQ2YCX7EatUOuyQMt2Vwx4uV-d_A3DP6PtMGBKpF8St4iGw",
    "p": "3e-jND6OS6ofGYUN6G4RapHzuRAV8ux1C9eXMOdZFbcBehn_ydhzR48LIPTW9HiRE00um27lXfW5_POCaEUvfOp1UxTWeHZ4xICo40PBo383ZKW1MbES1oiMbjkEqSFGRnTItnLU07bKbzLA7I0UWHWCEAnv0g7HRxk973FAsm8",
    "q": "1w8-olZ2POBYeYgw1a0DkeJWKMQi_4pAgyYwustZo0dHlRXQT0OI9XQ0j1PZWoQS28tFcmoEAg6f5MUDpdM9swS0SOCPI1Lc_f_Slus3u1O3UCezk37pneSPezskDhvV2cClJEYH8m_zwDAUlEi4KLIt_H_jgtyDd6pbxxc78RU",
    "qi": "s9Fu1JsTak-C84codMY-vuApuaxZVs5xADysbzTVPfxb9Q97Ve3KcwSPPNDb05pV5DC9Q334PEVcnpi_CPqKHhZ2rXT2Ls6jV8OcxzM5A30MpyHZ40Aes1I4zIsMIGb77BvIcCxLZPRU7z6DMsAG-JmbkAUJBZ-R7gtmjmY5LXQ"
}

我希望调整 jwk 能让我插入我的值(参见上面的 Python 代码),导入密钥,并使用

sign
功能。但我对 jwk 所做的任何更改都会导致我无法导入密钥。例如,我无法从
decrypt
切换到
sign

// See https://stackoverflow.com/a/62967202/24558

// PEM encoded PKCS#8 key
const privateKey = 
`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6cXloNrocJ8sw
wj8xktPmEOTfTgJT7KkUwWIGOjBB1QxApgdn5+SUHsakvEJq3Fgn+FnuuN8cfcqW
rbT9jURtgNGinJNJ+StPM/PCxfhSv+XbkK11CV2EcJMyDB/8S/9u4E2ht/N1kT4O
F2/mVDAq2MjjeUMq8CLmQR63ZMXB8lwmsGJEl4Rwt9WBZNcZFCfuCeBYrKRS7mtL
zd4BTXEf0UuiNB/KJrz38TKSI47v/dRbB34wBNn0cuNLHb8t/eDaOvzV6J8SZgOW
uL85mng6Fm77QGjUteWgJN76+YhDZgJfsRq1Q67JAy3ZXDHi5X538DcM/o+0wYEq
kXxK3iIbAgMBAAECggEASlJj0ExIomKmmBhG8q8SM1s2sWG6gdQMjs6MEeluRT/1
c2v79cq2Dum5y/+UBl8x8TUKPKSLpCLs+GXkiVKgHXrFlqoN+OYQArG2EUWzuODw
czdYPhhupBXwR3oX4g41k/BsYfQfZBVzBFEJdWrIDLyAUFWNlfdGIj2BTiAoySfy
qmamvmW8bsvc8coiGlZ28UC85/Xqx9wOzjeGoRkCH7PcTMlc9F7SxSthwX/k1VBX
mNOHa+HzGOgO/W3k1LDqJbq2wKjZTW3iVEg2VodjxgBLMm0MueSGoI6IuaZSPMyF
EM3gGvC2+cDBI2SL/amhiTUa/VDlTVw/IKbSuar9uQKBgQDd76M0Po5Lqh8ZhQ3o
bhFqkfO5EBXy7HUL15cw51kVtwF6Gf/J2HNHjwsg9Nb0eJETTS6bbuVd9bn884Jo
RS986nVTFNZ4dnjEgKjjQ8GjfzdkpbUxsRLWiIxuOQSpIUZGdMi2ctTTtspvMsDs
jRRYdYIQCe/SDsdHGT3vcUCybwKBgQDXDz6iVnY84Fh5iDDVrQOR4lYoxCL/ikCD
JjC6y1mjR0eVFdBPQ4j1dDSPU9lahBLby0VyagQCDp/kxQOl0z2zBLRI4I8jUtz9
/9KW6ze7U7dQJ7OTfumd5I97OyQOG9XZwKUkRgfyb/PAMBSUSLgosi38f+OC3IN3
qlvHFzvxFQKBgQCITpUDEmSczih5qQGIvolN1cRF5j5Ey7t7gXbnXz+Umah7kJpM
IvdyfMVOAXJABgi8PQwiBLM0ySXo2LpARjXLV8ilNUggBktYDNktc8DrJMgltaya
j3HNd2IglD5rjfc2cKWRgOd7/GlKcHaTEnbreYhfR2sWrWLxJOyoMfuVWwKBgFal
CbMV6qU0LfEo8aPlBN8ttVDPVNpntP4h0NgxPXgPK8Pg+gA1UWSy4MouGg/hzkdH
aj9ifyLlCX598a5JoT4S0x/ZeVHd/LNI8mtjcRzD6cMde7gdFbpLb5NSjIAyrsIA
X4hxvpnqiOYRePkVIz0iLGziiaMbfMwlkrxvm/LRAoGBALPRbtSbE2pPgvOHKHTG
Pr7gKbmsWVbOcQA8rG801T38W/UPe1XtynMEjzzQ29OaVeQwvUN9+DxFXJ6Yvwj6
ih4Wdq109i7Oo1fDnMczOQN9DKch2eNAHrNSOMyLDCBm++wbyHAsS2T0VO8+gzLA
BviZm5AFCQWfke4LZo5mOS10
-----END PRIVATE KEY-----`;

importPrivateKeyAndDecrypt();
    
async function importPrivateKeyAndDecrypt() {

    // A ciphertext produced with the first code
    const ciphertextB64 = "q/g0YQ+CbFwCb9QxAeKk/X8vjUUKpBGCVe6OvFoBlTfRF24BQlWpLFhxVQv+Gn29CzAXfSJjU+C8taYXQ4wofyOaRx0etkATDbmIV1gVdxNnqVKTx2RSj1L3uACZ3aWYIGRjtaBMBNAW81mPEjxEWCvRW3uI/rOn3LAc4N05CkofOnsIpaafgcEjhZoTxp1Dpkm328bwRJ3g1Dn+vQk6JBiAXSiF7GHvMvnD6q+CQiO1dcv0lrrXlibE8/P2LHWpqQ9g5xWWUHl70q2WB+IxLgX9OkqX8XQ1GHjP5EaQFfo1HerBpa+Uf5DaienI/XT4n64DWM1S7t0dbhFDskc9HQ==";

    try {
        const priv = await importPrivateKey(privateKey);
        const decrypted = await decryptRSA(priv, str2ab(atob(ciphertextB64)));
        say `decrypted = ${decrypted}`
        
        const jwk = await crypto.subtle.exportKey('jwk', priv)
        say `jwk = ${JSON.stringify(jwk, null, 4)}`



        // jwk.ext = false
        // jwk.alg = 'RSASSA-PKCS1-v1_5'
        // const priv2 = await crypto.subtle.importKey('jwk', jwk, {
        //     name: "RSASSA-PKCS1-v1_5",
        //     hash: "SHA-256",     // SHA-1, SHA-256, SHA-384, or SHA-512
        //     publicExponent: new Uint8Array([1, 0, 1]), // 0x03 or 0x010001
        //     modulusLength: 2048, // 1024, 2048, or 4096
        // }, true, ['decrypt'])
        // say `after`
        // const jwk2 = await crypto.subtle.exportKey('jwk', priv2)
        // say `jwk2 = ${JSON.stringify(jwk, null, 4)}`
        // const decrypted2 = await decryptRSA(priv2, str2ab(atob(ciphertextB64)));
        // say `decrypted = ${decrypted2}`

    } catch (e) {
        say `${e}`
    }
}

async function importPrivateKey(pkcs8Pem) {     
    return await crypto.subtle.importKey(
        "pkcs8",
        getPkcs8Der(pkcs8Pem),
        {
            name: 'RSA-OAEP',
            hash: 'SHA-256',
        },
        true,
        ['decrypt']
    );
}

async function decryptRSA(key, ciphertext) {
    let decrypted = await crypto.subtle.decrypt(
        {
            name: "RSA-OAEP"
        },
        key,
        ciphertext
    );
    return new TextDecoder().decode(decrypted);
}

function getPkcs8Der(pkcs8Pem){
    const pemHeader = "-----BEGIN PRIVATE KEY-----";
    const pemFooter = "-----END PRIVATE KEY-----";
    var pemContents = pkcs8Pem.substring(pemHeader.length, pkcs8Pem.length - pemFooter.length);
    var binaryDerString = window.atob(pemContents);
    return str2ab(binaryDerString); 
}

//
// Helper
//
    
// https://stackoverflow.com/a/11058858
function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}
    
function ab2str(buf) {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
}
javascript cryptography rsa
1个回答
0
投票

使用

rsa.PrivateKey()
的文档,可以识别
key[0]
key[4]
的五个参数:按此顺序,它们是模数 (
n
)、公共指数 (
e
)、私有指数 (
d
),以及两个素数
p
q
以及
n = p * q

WebCrypto 不支持通过这种方式导入私钥。但是,正如您已经意识到的那样,私钥可以作为 JWK 导入,这与 Python 代码中的导入非常接近。
JWK 包含 Base64url 编码形式的参数

n
e
d
p
q
。另外,还需要参数
dp = d mod p-1
dq = d mod q-1
q_inv = q^-1 mod p

请注意,WebCrypto 不提供对 Base64url 或模算术(例如模逆)的支持,因此必须自己实现或必须使用其他库。
在以下实现中,根据 RSA 的要求,将

BigInt
应用于大整数。为了确定模逆,使用 bigint-mod-arith 的功能,从
BigInt
Uint8Array
的转换,以及进一步的 Base64url 编码,使用 s1r-J/browser-base64url 的功能应用 -arraybuffer.js 和来自这篇文章

通过这些实现,可以创建、导入 JWK 并将其用于签名,如下所示:

(async () => {

// Sample data for n, e, d, p, q imported from decimal integers
const n = BigInt("23536280960117078781071277796506388364632844470760889287043499897812352121486284917799618205473774110082986255111078948929716171007803508886970676224127450295642559080964190798895956419173396140605893662673557325803620649459356106588185328074303624698999369184931437392043701807154016276826947703489139066577610914955532141229640472629981914163258711276860837496743796618919012922407451955239609188448498501143346583070612890214114610395155257827751505466411422773826133218559917162211680737700993678145296589807650002517691621420203575296591550000452984882555949046114026781590107745032317312675291585975313969848859")
const e = BigInt("65537")
const d = BigInt("9382262539985942035117370835310273525276302879268630432030935728372487284645760310626287831576092110196042173349053779403830431169856213584267038716378986511034405846929055108124522963378015078098310449629166503450258471811735024255250342492655785209291827821785156504984080896469149873080916257284492090183134232671709250785165983314728570454111274522098371781165874751498087056242271462925111175964110913088917016069904769646771252836449225104991887781851938529428988142021234387304783760289871945413583159049359570528147608301067614991660860541652310473617313526348376112227247717281579081381208446321849438764473")
const p = BigInt("155848818230016115749752423910909717296251325660942142725149379121248574362967471982268814952228835353898234610957136620749647839486274159495856726744103519159395949128597219453194423432726936416070232282723469181220002341912380459799343589855993917183257748160693240120150007893839052471718988588887870321263")
const q = BigInt("151019951433831574850603517176599781805882023744929175175855523475079892352628220454545688743736195507788846749671107149711466014334137134060305215869022779903575705272635233063037001421670083870087620888835502764099951892566093068046644025208762390847987888334442622157531568813775934596672771679438667247893")

// Calculate the missing dp, dq and q_inv
const dp = d % (p-1n)
const dq = d % (q-1n)
const qi = modInv(q, p) //  q^-1 mod p

// Create the JWK: convert BigInt to Uint8Array and Base64url encode    
const jwk = {
    "kty":"RSA",
    "n":ab2base64url(bnToBuf(n)),
    "e":ab2base64url(bnToBuf(e)),
    "d":ab2base64url(bnToBuf(d)),
    "p":ab2base64url(bnToBuf(p)),
    "q":ab2base64url(bnToBuf(q)),
    "dp":ab2base64url(bnToBuf(dp)),
    "dq":ab2base64url(bnToBuf(dq)),
    "qi":ab2base64url(bnToBuf(qi))
}

// Import key        
const privateKey = await window.crypto.subtle.importKey(
    "jwk",
    jwk,
    {
        name: "RSASSA-PKCS1-v1_5",
        hash: "SHA-256",
    },
    true,
    ["sign"],
);

// Sign data
const plaintext = new TextEncoder().encode("The quick brown fox jumps over the lazy dog");
const signature = await window.crypto.subtle.sign(
    "RSASSA-PKCS1-v1_5",
    privateKey,
    plaintext,
);
console.log("signature (hex):", ab2hex(signature));
  
//
// Helper
//
  
//https://github.com/juanelas/bigint-mod-arith
function modInv (a, n) {
    const egcd = eGcd(toZn(a, n), n);
    if (egcd.g !== 1n) throw new RangeError(`${a.toString()} does not have inverse modulo ${n.toString()}`); // modular inverse does not exist
    else return toZn(egcd.x, n);
}

function eGcd(a, b) {
    let x = 0n, y = 1n, u = 1n, v = 0n;
    while (a !== 0n) {
        const q = b / a, r = b % a, m = x - (u * q), n = y - (v * q);
        b = a, a = r, x = u, y = v, u = m, v = n;
    }
    return {g: b, x, y}
}

function toZn (a, n) {
    if (n <= 0n) throw new RangeError('n must be > 0');
    const aZn = a % n;
    return (aZn < 0n) ? aZn + n : aZn;
}

// https://coolaj86.com/articles/convert-js-bigints-to-typedarrays/ 
function bnToBuf(bn) {
    var hex = BigInt(bn).toString(16);
    if (hex.length % 2) hex = '0' + hex;
    var len = hex.length / 2;
    var u8 = new Uint8Array(len);
    var i = 0, j = 0;
    while (i < len) {
        u8[i] = parseInt(hex.slice(j, j+2), 16);
        i += 1, j += 2;
    }
    return u8;
}

// https://gist.github.com/s1r-J/fdc368b818be78ca58878085c89bd82b
function ab2base64url(ab) {
    const str = String.fromCharCode.apply(null, new Uint8Array(ab))
    return base642base64url(window.btoa(str));  
}

function base642base64url(base64) {
    return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/g, '')
}

// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/exportKey#pkcs_8_export
function ab2str(buf) {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
}

// from https://stackoverflow.com/a/40031979
function ab2hex(ab) { 
    return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}

})();

签名有效,可以验证,例如与 CyberChef 一起。


请注意,

BigInt
是一般密码学背景下的一个漏洞,因为实现是不是恒定时间的,因此定时攻击是可能的。对于这里的示例,BigInt
仅用于确定
q_inv
并生成JWK。
如果此隐含的漏洞不可接受,则必须应用满足您在密码学方面的要求的第三方库。其他库的实现也应同样小心。

© www.soinside.com 2019 - 2024. All rights reserved.