如何将JWK中的私钥加载到openSSL中?

8
我有一个JWK(来自 https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-signature-26#appendix-A.1):
 {"kty":"RSA",
       "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx
            HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs
            D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH
            SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV
            MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8
            NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
       "e":"AQAB",
       "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I
            jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0
            BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn
            439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT
            CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh
            BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ"
      }

我需要将它加载到一个openSSL的rsa结构中,以便我可以将其提供给EVP_SignFinal调用。 "d"是什么格式?PEM格式还是二进制格式?我应该如何将其加载到rsa结构中?


显然,你需要对d的值进行base64解码 - 但接下来呢? - Grahame Grieve
1
我不相信OpenSSL有PRSA。也许你在想另一个库? - jww
1
糟糕!“PRSA”不在Delphi头文件的翻译中。已经更正了问题。 - Grahame Grieve
你真的需要在这里使用OpenSSL吗?由于Base64编码/解码,库无法验证密钥(在我看来RSA_check_key是有问题的),以及没有p和q就无法执行任何私钥操作,因此OpenSSL可能会很麻烦。对于基本的东西,如编码转换、密钥验证和私钥操作,Crypto++可能是更好的选择。 - jww
您可以检查jose工具的源代码。看起来这里使用JWKS验证了jws的签名:https://github.com/latchset/jose/blob/master/lib/openssl/rsassa.c#L112 - Sergey Ponomarev
显示剩余2条评论
3个回答

10
我使用jwk-to-pem (https://github.com/Brightspace/node-jwk-to-pem#readme)完成了这个操作。在我的服务器的nodejs环境中,我遇到了一些无法正确运行jwk-to-pem的问题,所以我在这里在线进行了操作:

https://tonicdev.com/npm/jwk-to-pem

将此放入顶部的代码框中(这将使用您提供的值)。

var jwkToPem = require('jwk-to-pem');
var options = { private: false };
var jwk = {
    "kty":"RSA",
    "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
    "e":"AQAB",
    "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ"
}, pem = jwkToPem(jwk, options);
console.log(pem);

然后点击“-> 运行”按钮,您将获得以下结果:

-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd/wWJcyQoTbji9k0l8W2
6mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL+yRT+SFd2lZS+pCgNMs
D1W/YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb/7OMg0LOL+bSf63kpaSHSXndS5
z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxv
b3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxXFvUK+DWNmoudF8NAco9/h9iaGNj8q2et
hFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQIDAQAB
-----END RSA PUBLIC KEY-----

我能够使用这个方法将我的Let's Encrypt(letsencrypt.org)私钥从JWK格式转换为PEM格式。

要转换私钥,请将private选项的值更改为true,并将jwk变量的值更改为您特定的JWK格式密钥。

显然,您可以使用除console.log()JavaScript函数之外的其他方法输出pem变量的内容。


2
openssl rsa -inform PEM -pubin 命令无法解析该PEM密钥。这是什么格式? - Velkan
类似的工具可以将JWK转换为PEM格式,可参考以下链接: https://github.com/jpf/okta-jwks-to-pem https://github.com/RafPe/go-jwk-pem - Sergey Ponomarev
@Velkan:这是一个很少使用的算法特定格式(而不是通用的SPKI格式),可以通过https://www.openssl.org/docs/manmaster/man3/PEM_read_RSAPublicKey.html进行读取。使用`openssl rsa -RSAPublicKey_in [-text]命令(您无需指定-inform PEM`,因为它是默认值)。 - dave_thompson_085

8
“d”是什么格式?PEM格式还是二进制格式?
该格式为Base64URL编码或“Base 64编码与URL和文件名安全字母表” RFC 4648(请参见第5节,第7页的表2)。
如何将其加载到RSA结构中? 好的,所以OpenSSL确实很痛苦。要将其加载到RSA结构中,您需要将n、e和d从Base64URL转换为Base64。以下是我在Crypto++中的做法(您可以在OpenSSL中完成,但这会很困难):
string nz = "ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx"
            "HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs"
            "D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH"
            "SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV"
            "MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8"
            "NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ";

string ez = "AQAB";

string dz = "Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I"
            "jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0"
            "BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn"
            "439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT"
            "CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh"
            "BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ";

string nn, ee, dd;

// First, convert Base64URL encoding to Base64
std::replace(nz.begin(), nz.end(), '-', '+');
std::replace(ez.begin(), ez.end(), '-', '+');
std::replace(dz.begin(), dz.end(), '-', '+');
std::replace(nz.begin(), nz.end(), '_', '/');
std::replace(ez.begin(), ez.end(), '_', '/');
std::replace(dz.begin(), dz.end(), '_', '/');

// Now, Base64 decode
StringSource ss1(nz, true, new Base64Decoder(new StringSink(nn)));
StringSource ss2(ez, true, new Base64Decoder(new StringSink(ee)));
StringSource ss3(dz, true, new Base64Decoder(new StringSink(dd)));
< p > < em > < strong >编辑 :Crypto++现在有Base64URLEncoderBase64URLDecoder类,因此您不需要进行查找/替换操作。 < p >运行上面的代码后,< code > nn ,< code > ee 和< code > dd 是二进制字符串(即非ASCII字符)。从那里,您可以将它们加载到< code > Integer 中,并使用以下命令获取基数为10的字符串:
Integer n((byte*)nn.data(), nn.size());
Integer e((byte*)ee.data(), ee.size());
Integer d((byte*)dd.data(), dd.size());

cout << "N: " << endl << n << endl << endl;
cout << "E: " << endl << e << endl << endl;
cout << "D: " << endl << d << endl << endl;

$ ./cryptopp-test.exe

N: 
20446702916744654562596343388758805860065209639960173505037453331270270518732245
08977372301204320323609709562340204469011575537734525469644875960570778896584888
95017468362112062706438336639499925362469853626937363871851454247879222415857219
92924045675229348655595626434390043002821512765630397723028023792577935108185822
75369257422156693093780503115582009714681996492027000881132703628678639279359312
17624250488602118597634417704467037220158572506211078553986931332640811506974231
88751482418465308470313958250757758547155699749157985955379381294962058862159085
915015369381046959790476428631998204940879604226680285601.

E: 
65537.

D:
23583109899396195101799862623499368829246520235662137651186064319555667005065389
11356936879137503597382515919515633242482643314423192704128296593672966061810149
31632061789402182278402640746140338406535182197235078430096761014345948432406842
76746396884059179774424728049430754391920261073195321175575450790865379829879825
22396626690057355718157403493216553255260857777965627529169195827622139772389760
13057175483467867984218114225248961766503010944557397801270779301059273764049922
00150833924259148778478404572782464027609558833769999511998277062853834711506435
61410605789710883438795588594095047409018233862167884701.

OpenSSL需要 nedpq 用于私钥操作。 d mod p-1d mod q-1inv q mod p 是可选的。只有 ned,您需要解决缺失的参数(至少是 pq)。其中两个难点是 pq。以下是 Crypto++ 代码来解决它们(可以自由转换为 OpenSSL):

Integer p, q;

RSA_solve(n, e, d, p, q);

cout << "P: " << endl << p << endl << endl;
cout << "Q: " << endl << q << endl << endl;

而且:

void RSA_solve(const Integer& n, const Integer& e, const Integer& d, Integer& p, Integer& q)
{
    AutoSeededRandomPool prng;
    Integer g = 1;
    unsigned int SAFETY = 0;

STEP_1:
    const Integer k = e * d - 1;
    if(!k.IsEven())
        throw runtime_error("e * d - 1 is not even");

STEP_2:
    // g = 3, 5, 7, ...
    g += 2; while(!VerifyPrime(prng, g)) g += 2;
    Integer t = k;

STEP_3:
    if(SAFETY++ > 128)
        throw runtime_error("could not factor n");

    if(!t.IsEven())
        goto STEP_2;

    t /= 2;
    Integer x = a_exp_b_mod_c(g, t, n);

STEP_4:
    if(!(x > 1))
        goto STEP_3;

    Integer y = GCD(x-1, n);
    if(!(y > 1))
        goto STEP_3;

    p = std::max(y, n/y);
    q = std::min(y, n/y);

    Integer check = p * q;
    if(n != check)
        throw runtime_error("n != p * q");
}

这会导致:
P: 
15737705590244743839558616502896029191493197327877753279847020015603526753735923
90718294084119093232085749598005372477289597182368848096852332845373492076546615
30801859889389455120932077199406250387226339056140578989122526711937239401762061
949364440402067108084155200696015505170135950332209194782224750221639.

Q: 
12992175256740635899099334754006444501823007340248226099417932857332386190837921
12746269565434716649972371852989646481333243433270528522640603220881224011247812
49085873464824282666514908127141915943024862618996371026577302203267804867959037
802770797169483022132210859867700312376409633383772189122488119155159.

"

d mod p-1d mod q-1inv q mod p留给读者自己练习(但它们很容易,在Crypto ++中尤其如此)。您修改后的RSA_solve可能如下:

"
void RSA_solve(const Integer& n, const Integer& e, const Integer& d,
               Integer& p, Integer& q,
               Integer& dmodp1, Integer& dmodq1, Integer& invqmodp)

现在,使用Base 10(十进制)字符串切换到OpenSSL:
const char nz[] =
    "20446702916744654562596343388758805860065209639960173505037453331270270518732245"
    "08977372301204320323609709562340204469011575537734525469644875960570778896584888"
    "95017468362112062706438336639499925362469853626937363871851454247879222415857219"
    "92924045675229348655595626434390043002821512765630397723028023792577935108185822"
    "75369257422156693093780503115582009714681996492027000881132703628678639279359312"
    "17624250488602118597634417704467037220158572506211078553986931332640811506974231"
    "88751482418465308470313958250757758547155699749157985955379381294962058862159085"
    "915015369381046959790476428631998204940879604226680285601";

const char ez[] = "65537";

const char dz[] =
    "23583109899396195101799862623499368829246520235662137651186064319555667005065389"
    "11356936879137503597382515919515633242482643314423192704128296593672966061810149"
    "31632061789402182278402640746140338406535182197235078430096761014345948432406842"
    "76746396884059179774424728049430754391920261073195321175575450790865379829879825"
    "22396626690057355718157403493216553255260857777965627529169195827622139772389760"
    "13057175483467867984218114225248961766503010944557397801270779301059273764049922"
    "00150833924259148778478404572782464027609558833769999511998277062853834711506435"
    "61410605789710883438795588594095047409018233862167884701";

const char pz[] =
    "15737705590244743839558616502896029191493197327877753279847020015603526753735923"
    "90718294084119093232085749598005372477289597182368848096852332845373492076546615"
    "30801859889389455120932077199406250387226339056140578989122526711937239401762061"
    "949364440402067108084155200696015505170135950332209194782224750221639";

const char qz[] =
    "12992175256740635899099334754006444501823007340248226099417932857332386190837921"
    "12746269565434716649972371852989646481333243433270528522640603220881224011247812"
    "49085873464824282666514908127141915943024862618996371026577302203267804867959037"
    "802770797169483022132210859867700312376409633383772189122488119155159";

using BN_ptr = std::unique_ptr<BIGNUM, decltype(&::BN_free)>;
using RSA_ptr = std::unique_ptr<RSA, decltype(&::RSA_free)>;
using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)>;
using EVP_MD_CTX_ptr = std::unique_ptr<EVP_MD_CTX, decltype(&::EVP_MD_CTX_destroy)>;

#define UNUSED(x) ((void)x)

int main(int argc, char* argv[])
{
    UNUSED(argc); UNUSED(argv);

    int rc;
    long err;

    RSA_ptr rsa(RSA_new(), ::RSA_free);
    BIGNUM *n = NULL, *e = NULL, *d = NULL, *p = NULL, *q = NULL;

    rc = BN_dec2bn(&n, nz);
    if(rc == 0 || n == NULL) {
        cerr << "BN_dec2bn failed for n" << endl;
        exit(1);
    }
    rsa->n = n;

    rc = BN_dec2bn(&e, ez);
    if(rc == 0 || e == NULL) {
        cerr << "BN_dec2bn failed for e" << endl;
        exit(1);
    }
    rsa->e = e;

    rc = BN_dec2bn(&d, dz);
    if(rc == 0 || d == NULL) {
        cerr << "BN_dec2bn failed for d" << endl;
        exit(1);
    }
    rsa->d = d;

    rc = BN_dec2bn(&p, pz);
    if(rc == 0 || p == NULL) {
        cerr << "BN_dec2bn failed for p" << endl;
        exit(1);
    }
    rsa->p = p;

    rc = BN_dec2bn(&q, qz);
    if(rc == 0 || q == NULL) {
        cerr << "BN_dec2bn failed for q" << endl;
        exit(1);
    }
    rsa->q = q;

    [Exercise left to the reader]

    rc = RSA_check_key(rsa.get());
    err = ERR_get_error();
    if(rc != 1) {
        cerr << "RSA_check_key failed, error 0x" << std::hex << err << endl;
        exit(1);
    }

    [Continues at next question below]
    ...
}

以下是您需要在 <openssl dir>/crypto/rsa/rsa.h 中提供的 struct rsa 字段:
struct rsa_st
    {
    ...
    /* functional reference if 'meth' is ENGINE-provided */
    ENGINE *engine;
    BIGNUM *n;
    BIGNUM *e;
    BIGNUM *d;
    BIGNUM *p;
    BIGNUM *q;
    BIGNUM *dmp1;
    BIGNUM *dmq1;
    BIGNUM *iqmp;
    ...
    };

因此,我可以将其输入到EVP_SignFinal调用中...

EVP_SignFinal需要一个EVP_PKEY,而您拥有一个RSA。所以:

EVP_PKEY_ptr pkey(EVP_PKEY_new(), ::EVP_PKEY_free);

rc = EVP_PKEY_set1_RSA(pkey.get(), rsa.get());
err = ERR_get_error();
if(rc != 1) {
    cerr << "EVP_PKEY_set1_RSA failed, error 0x" << std::hex << err << endl;
    exit(1);
}

set1 表示对 RSA* 的引用计数被增加了,这是可以接受的。如果是 set0,你必须释放你的副本(也就是使用 rsa.release() 而不是 rsa.get()),以避免重复释放。

EVP_MD_CTX_ptr ctx(EVP_MD_CTX_create(), ::EVP_MD_CTX_destroy);
EVP_MD_CTX_init(ctx.get());

const EVP_MD* md = EVP_sha256();
rc = EVP_SignInit(ctx.get(), md);
err = ERR_get_error();
if(rc != 1) {
    cerr << "EVP_SignInit_ex failed, error 0x" << std::hex << err << endl;
    exit(1);
}

const char message[] = "Now is the time for all good men...";

rc = EVP_SignUpdate(ctx.get(), message, (unsigned int)sizeof(message));
err = ERR_get_error();
if(rc != 1) {
    cerr << "EVP_SignUpdate failed, error 0x" << std::hex << err << endl;
    exit(1);
}

const unsigned int req = std::max(EVP_MD_size(md), EVP_PKEY_size(pkey.get()));
unique_ptr<unsigned char[]> signature(new unsigned char[req]);
unsigned int size = req;

rc = EVP_SignFinal(ctx.get(), signature.get(), &size, pkey.get());
err = ERR_get_error();
if(rc != 1) {
    cerr << "EVP_SignFinal failed, error 0x" << std::hex << err << endl;
    exit(1);
}

size = std::min(size, (unsigned int)EVP_MD_size(md));

cout << "Signature: ";
for(unsigned i = 0; i < size; i++)
    cout << std::hex << (signature[i] & 0xFF);
cout << endl;

这里是上面使用的Crypto++代码的Pastebin链接: http://pastebin.com/9Rm7bxZp
这里是上面使用的OpenSSL代码的Pastebin链接: http://pastebin.com/aGVpj4FW
这里是OpenSSL程序的输出:
$ ./openssl-test.exe 
Signature: 78f2c9af23b9a2a42e3b57dec454fa43ea6627992f48d40a33da6a7c93f98b4

1
非常感谢您的问题,它包含了我所需要的关键信息 - 关于使用BN_BN2bin。然而,“OpenSSL需要n、e、d、p、q、d mod p-1、d mod q-1和inv q mod p”并不是真的 - 它只需要n、e和d(好吧,我可以正确地使用它们进行签名和验证)。 (幸运的是,我尝试了一下,而没有花费数小时来弄清楚您留下的“读者的简单练习”) - Grahame Grieve
@ Grahame - 你需要 nedpq。如果你没有 pq,那么 RSA_check_key 将会以错误代码 0x407b093 失败。在使用密钥之前,你必须验证它。 - jww
RSA_Check_Key:“此函数验证RSA密钥。它检查p和q是否实际上是质数,以及n = pq。它还检查de = 1 mod (p-1*q-1),并且dmp1、dmq1和iqmp设置正确或为NULL。” - 因此,我要做很多工作来填写一堆东西,以便检查我填写的内容是否正确 - 而且p和q是质数吗?难道不能安全地假设来自CA的签名证书实际上是质数吗? - Grahame Grieve
@Grahame - 嗯,我采取非常防御的姿态。我几乎不相信任何东西,并验证一切。我不在乎性能损失或额外的工作。这有点像Jon Bentley所说的:“如果它不必正确,我可以让它尽可能快”。 - jww

1

上面jwktopem示例的更正:

var options = { private: true };

其实很简单。如果包含d,那么它是一个私有的RSA密钥,而不是公共密钥。


这已经在现有答案中了,只是没有强调;请参见“转换私钥”段落。 - dave_thompson_085

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接