如何将 JWK 转换为 PEM

How to convert JWK to PEM

这个问题更通用,而不是针对特定语言,所以我将解释我的问题以及我在伪代码中尝试过的内容。

我正在尝试从 JWK 集生成 PEM public 密钥。 JWK 包括“e”(指数)和“n”(模数)变量。我的问题是,在不使用任何库且不使用 OpenSSL 命令行工具的情况下,将此 JWK 转换为 PEM 的确切步骤是什么。

JWK 供参考:

{
  "kty": "RSA",
  "alg": "RS512",
  "kid": "26887d3ee3293c526c0e6dd05f122df53aa3f13d7dad06d25e266fa6f51db79fb52422aaf79f121476237e98dcd6640350fee47fec70e783544ec9a36e4605bc",
  "use": "sig",
  "n": "14m79mVwIE0JxQdKrgXVf7dVcBS90U0TvG7Yf7dG4NJocz1PNUrKrzGhe_FryOe0JahL_sjA2_rKw7NBCpuVx_zSPFRw6kqjewGicjXGus5Fmlf3zDuqwV4BWIFHyQexMPOly0agFfcM0M0MgBULXjINgBs9MwnRv7JVfRoGqXHsNM45djFDd3o4liu4LPlge_DquZUFLNu-BYAyAlWkz0H2TepZhGrN9VEPmxzQkNzXc1R4MpZvbxrRRgaAA2z094ik3hk86JhfyFq-LDcueZhtshmrYZ95LWgMlQ7PixkeK1HkeEYMt20lmNzR8B8KabimYmibxA4Ay9gpRwfp-Q",
  "e": "AQAB"
}

我的大部分研究都来自 node-jwk-to-pem 库(可在此处找到:https://github.com/Brightspace/node-jwk-to-pem) and a Whosebug question which uses a JOSE library in PHP (which can be found here:

根据我通过阅读上面提到的库(加上其他一些没有完全提到分步过程的文章和问题)得出的结论,我发现第一步是转换整数的模数和指数(特别是 BigInt)。这通常会导致以下结果:

n = 27209154847141595039206198313134388207882403216639061643382959557246344691110642716248339592972939239825436848454712213431917464875221502188500634554426063986720722251339335488215426908010590565758292169712942346285217861588132983738839891250753593240057639347814778952277719613395670962156961452389927728643840215833830614315091417876959205843957512422401240879135352731575182574836052718961865690645602829768565458494497550672252951063585023601307115444743487394113997186698238507983094748342588645472362960665610355698438390751920697759620235642103374737421940385132232531739910444003185620313592808726865629407737
e = 65537

使用此信息,我将如何从指数和模数生成 PEM public 密钥?任何伪代码或建议都会令人难以置信!

A RSA public key 由模数 n 和指数 e 定义。

您可以使用 ne 字段直接从 JWK of type RSA 获取该信息:

The "n" (modulus) parameter contains the modulus value for the RSA public key. It is represented as a Base64urlUInt-encoded value.

The "e" (exponent) parameter contains the exponent value for the RSA public key. It is represented as a Base64urlUInt-encoded value.

要创建表示此 public 关键信息的 PEM 文件,您基本上需要构建其等效的 ASN.1 DER 编码表示,可能需要一些序言。在其原始形式中,它是这样定义的 RFC3447:

RSAPublicKey ::= SEQUENCE {
  modulus           INTEGER,  -- n
  publicExponent    INTEGER   -- e
}

ASN.1 定义表示任意数据结构的规则和类型,它在密码学中广泛用于此目的。

ASN.1 仅提供语法,但该信息的实际编码是使用不同的 encoding formats,尤其是 DER 执行的。

执行编码的方式非常重要:Let's Encrypt 提供 an excellent article about the subject. The great lapo.it/asn1js site 也提供了一个用于检查 ASN.1 DER 编码信息的绝佳工具。

例如,考虑从下面提到的 SO 问题中提取的以下模数和指数:

SEQUENCE (2 elements)
   INTEGER (2048 bit): EB506399F5C612F5A67A09C1192B92FAB53DB28520D859CE0EF6B7D83D40AA1C1DCE2C0720D15A0F531595CAD81BA5D129F91CC6769719F1435872C4BCD0521150A0263B470066489B918BFCA03CE8A0E9FC2C0314C4B096EA30717C03C28CA29E678E63D78ACA1E9A63BDB1261EE7A0B041AB53746D68B57B68BEF37B71382838C95DA8557841A3CA58109F0B4F77A5E929B1A25DC2D6814C55DC0F81CD2F4E5DB95EE70C706FC02C4FCA358EA9A82D8043A47611195580F89458E3DAB5592DEFE06CDE1E516A6C61ED78C13977AE9660A9192CA75CD72967FD3AFAFA1F1A2FF6325A5064D847028F1E6B2329E8572F36E708A549DDA355FC74A32FDD8DBA65
   INTEGER (24 bit): 010001

对应的ASN.1 DER编码如下:

30 82 01 0A      ;sequence (0x10A bytes long)
   02 82 01 01   ;integer (0x101 bytes long)
      00 EB506399F5C612F5A67A09C1192B92FAB53DB28520D859CE0EF6B7D83D40AA1C1DCE2C0720D15A0F531595CAD81BA5D129F91CC6769719F1435872C4BCD0521150A0263B470066489B918BFCA03CE8A0E9FC2C0314C4B096EA30717C03C28CA29E678E63D78ACA1E9A63BDB1261EE7A0B041AB53746D68B57B68BEF37B71382838C95DA8557841A3CA58109F0B4F77A5E929B1A25DC2D6814C55DC0F81CD2F4E5DB95EE70C706FC02C4FCA358EA9A82D8043A47611195580F89458E3DAB5592DEFE06CDE1E516A6C61ED78C13977AE9660A9192CA75CD72967FD3AFAFA1F1A2FF6325A5064D847028F1E6B2329E8572F36E708A549DDA355FC74A32FDD8DBA65
   02 03         ;integer (3 bytes long)
      010001

在 ASN.1 DER 中,每个元素都被编码为 type-length-value 三元组。

类型由标签标识,通常用一个字节编码。

例如,30 表示 SEQUENCE,而 02 对应 INTEGER 类型。

上述文章提供了 ASN.1 定义的不同类型的列表以及相应的标签。

length 有点棘手:如果关联值长达 1270x7F)字节,则由单个字节指示.如果字节数大于 127,那么第一个长度字节指定实际定义长度的字节数。请考虑阅读 this articleLength 部分,我认为它非常具有解释性。

然后,您包括实际的

在提供的示例中,30 82 01 0A 表示:

  • 30: A SEQUENCE.
  • 82:长度字节。 0x82 & 0x7F -> 0x02实际值长度用2个字节表示。
  • 01 0A:实际值长度,266字节长。

现在,值 02 82 01 01 00 EB506399F5C612F5A67A09C119...:

  • 02:一个INTEGER
  • 82:长度字节。同样,0x82 & 0x7F -> 0x02。实际值长度以2个字节表示。
  • 01 01:实际值长度,257字节长,2048位的模数用第一个字节0x00填充(参见aforementioned article中的Integer padding部分).
  • 00 EB506399F5C612F5A67A09C119: 填充模值。

最后,02 03 010001

  • 02: 一个INTEGER.
  • 03: 指数值是3字节长。
  • 010001: 指数值。

这个结果应该被编码为 base64 并包含在特殊的文本分隔符中,例如 -----BEGIN RSA PUBLIC KEY----------END RSA PUBLIC KEY-----

请注意,您有几个选项可以用 PEM 格式表示密钥,上面描述的 PKCS#1, and the more generic X.509, suitable for the representation of keys in different formats (the one of the form -----BEGIN PUBLIC KEY-----). Consider read this wonderful SO question 和相关答案,它不仅详细解释了 RSA public关键基础知识,但也有这些不同表示的注意事项。

考虑阅读 this and especially this other SO 问题,我认为它们也可以提供帮助。