From e79a993248868aa8a877b6489cbf05975c68b75c Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Sat, 8 Jul 2023 13:58:01 -0700 Subject: [PATCH] fix: add helpful error in case private key is in OpenSSH format (#74) --- README.md | 40 +++++++++++++++--------------------- lib/get-token-node.js | 6 ++++++ lib/get-token.js | 11 ++++++++-- test/node.test.js | 48 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 3dc1d97..985b9cd 100644 --- a/README.md +++ b/README.md @@ -5,48 +5,40 @@ [![@latest](https://img.shields.io/npm/vuniversal-github-app-jwt.svg)](https://www.npmjs.com/packageuniversal-github-app-jwt) [![Build Status](https://github.com/gr2m/universal-github-app-jwt/workflows/Test/badge.svg)](https://github.com/gr2m/universal-github-app-jwt/actions?query=workflow%3ATest+branch%3Amaster) -⚠ The private keys provide by GitHub are in `PKCS#1` format, but the WebCrypto API only supports `PKCS#8`. You can see the difference in the first line, `PKCS#1` format starts with `-----BEGIN RSA PRIVATE KEY-----` while `PKCS#8` starts with `-----BEGIN PRIVATE KEY-----`. You can convert one format to the other using `oppenssl`: +⚠ The private keys provide by GitHub are in `PKCS#1` format, but the WebCrypto API only supports `PKCS#8`. And neither Node nor the WEbCrypto API supports private keys in the `OpenSSH` format. You can see the difference in the first line, `PKCS#1` format starts with `-----BEGIN RSA PRIVATE KEY-----` while `PKCS#8` starts with `-----BEGIN PRIVATE KEY-----`, and `OpenSSH` starts with `-----BEGIN OPENSSH PRIVATE KEY-----`. + +You can convert `PKCS#1` to `PKCS#8` using `oppenssl`: ``` openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.key ``` -It's also possible to convert the formats with JavaScript, e.g. using [node-rsa](https://github.com/rzcoder/node-rsa), but it turns a 4kb to a 200kb+ built. I'm looking for help to create a minimal `PKCS#1` to `PKCS#8` convert library that I can recommend people to use before passing the private key to `githubAppJwt`. Please create an issue if you'd like to help. - -The way it works with `node-rsa` is this - -```js -const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- -... ------END RSA PRIVATE KEY-----`; - -const key = new NodeRSA(PRIVATE_KEY); -const privateKeyPkcs8 = key.exportKey("pkcs8-private-pem"); +You can convert `OpenSSH` to `PKCS#8` using `ssh-keygen`: -// privateKeyPkcs8 is now -// -----BEGIN PRIVATE KEY----- -// ... -// -----END PRIVATE KEY----- +``` +cp private-key.pem private-key-pkcs8.key && ssh-keygen -m PKCS8 -N "" -f private-key-pkcs8.key ``` -When using a node, a conversion is not necessary, the implementation is agnostic to either format. - -However, if you got the error `Private Key is in PKCS#1 format, but only PKCS#8 is supported.` inside Node.js, it is possible that your bundler or your app framework incorrectly bundled the web version instead of the node version ([example](https://github.com/backstage/backstage/issues/9959)). +It's also possible to convert the formats with JavaScript, e.g. using [node-rsa](https://github.com/rzcoder/node-rsa), but it turns a 4kb to a 200kb+ built. I'm looking for help to create a minimal `PKCS#1` to `PKCS#8` convert library that I can recommend people to use before passing the private key to `githubAppJwt`. Please create an issue if you'd like to help. The same to convert `OpenSSH` to `PKCS#8`. -You can also convert `PKCS#1` to `PKCS#8` in Node.js using the built-in `crypto` module: +You can convert `PKCS#1` to `PKCS#8` in Node.js using the built-in `crypto` module: ```js -const crypto = require('crypto'); +const crypto = require("crypto"); const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----`; const privateKeyPkcs8 = crypto.createPrivateKey(PRIVATE_KEY).export({ - type: 'pkcs8', - format: 'pem', -}) + type: "pkcs8", + format: "pem", +}); ``` +When using a node, a conversion is not necessary, the implementation is agnostic to either `PKCS` format. + +However, if you got the error `Private Key is in PKCS#1 format, but only PKCS#8 is supported.` inside Node.js, it is possible that your bundler or your app framework incorrectly bundled the web version instead of the node version ([example](https://github.com/backstage/backstage/issues/9959)). + ## Usage diff --git a/lib/get-token-node.js b/lib/get-token-node.js index 2b7e7b9..6f0b2ae 100644 --- a/lib/get-token-node.js +++ b/lib/get-token-node.js @@ -7,6 +7,12 @@ import jsonwebtoken from "jsonwebtoken"; * @returns {Promise} */ export async function getToken({ privateKey, payload }) { + if (privateKey.includes("-----BEGIN OPENSSH PRIVATE KEY-----")) { + throw new Error( + "[universal-github-app-jwt] Private Key is in OpenSSH format, but only PKCS is supported. See https://github.com/gr2m/universal-github-app-jwt#readme" + ); + } + return jsonwebtoken.sign(payload, privateKey, { algorithm: "RS256", }); diff --git a/lib/get-token.js b/lib/get-token.js index b1988b5..177ee44 100644 --- a/lib/get-token.js +++ b/lib/get-token.js @@ -13,9 +13,16 @@ import { */ export async function getToken({ privateKey, payload }) { // WebCrypto only supports PKCS#8, unfortunately - if (/BEGIN RSA PRIVATE KEY/.test(privateKey)) { + if (privateKey.includes("-----BEGIN RSA PRIVATE KEY-----")) { throw new Error( - "[universal-github-app-jwt] Private Key is in PKCS#1 format, but only PKCS#8 is supported. See https://github.com/gr2m/universal-github-app-jwt#readme" + "[universal-github-app-jwt] Private Key is in PKCS#1 format, but only PKCS#8 is supported by WebCrypto. See https://github.com/gr2m/universal-github-app-jwt#readme" + ); + } + + // WebCrypto does not support OpenSSH, unfortunately + if (privateKey.includes("-----BEGIN OPENSSH PRIVATE KEY-----")) { + throw new Error( + "[universal-github-app-jwt] Private Key is in OpenSSH format, but only PKCS#8 is supported by WebCrypto. See https://github.com/gr2m/universal-github-app-jwt#readme" ); } diff --git a/test/node.test.js b/test/node.test.js index 097ddbd..d957cb0 100644 --- a/test/node.test.js +++ b/test/node.test.js @@ -62,6 +62,37 @@ SrOlxDpE+88rX/uDQ0ieaM8x5s/Qrp5I/HT3/j0npZMP74RqM3sWGS1RJjG6R1Zo KhwBAlMM1g/Qrzwbiu0LYhjLkkWf1JbPmiHH//S77N39H9BFTMs7Dg3vb8e75Qbo w23mINkUK1qlFoq3o69IHDLz -----END PRIVATE KEY-----`; + +// openssh version of the key above +// see https://github.com/gr2m/universal-github-app-jwt/issues/72 +const PRIVATEY_KEY_OPENSSH = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnHlWu2 +/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kEp6RD++7sGb +zbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQrO64Z8tyXQPtVPb/ +YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPSye+Qjtius0Md4Nie4+X8kw +VI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSXgzQbeMmiqFFkwrG1+zx6E7H7jqIQ +9B6bvWKXGwAAA7hJqTyJSak8iQAAAAdzc2gtcnNhAAABAQDVzv73Pk9p3s56N5yxDRu7dq +jM3e2KE+aWOee51v0bccQJ2ceVa7b9rWAH0lblMFD4BWm6b06THsp+qR8Eov2ZweBIvTJY +x2Mx806o22tCoqU3iQSnpEP77uwZvNt9n1qvCuUP8dIMBYZ0YYlNI4Ezqkd6HIuZILcMmh +H8JO7S9MZNZdCs7rhny3JdA+1U9v9hetxSFsGnyLvZv1eTLR8Po+QR5s7LQinnxdUCfZQ8 +2U05I9LJ75CO2K6zQx3g2J7j5fyTBUjZCOTd1KbSzD/vbQomR1UOatrJ6WO0oHHoC01CxJ +eDNBt4yaKoUWTCsbX7PHoTsfuOohD0Hpu9YpcbAAAAAwEAAQAAAQA/JAQTy+jz4QKlASta +9f4MnHw5FAkET+A22R7fuOfCZRR5vEC+lbLSs5K6fJmA0YX32NFiZ2CS/fLa+OLaIbA26h +RsJsNpiGh45duC7ll8sj+MaQBg6ZfSHLrI1wcS0C6dpuACl21qc29Di1Ja33NWaKD3JNrs ++8ZTCGTrS5Tj4y89g7no1XtuCXMMB860FijnwNdUf/XudA1bHoZ0h/8OOkO+t8uCNBNwSK +FDLKJRCs4dsnQAMs5IGmxkAVLmpmaLTX7yaA13gotSw/MjQlsk5WKSWSQw16KAVORALAr4 +TdbEkaGl1q5TEgwe6/xPmkMirmuCjjar5qCbgttb0inhAAAAgQDOYPZ2JGmhibqKjjLFm0 +qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx09/49J6WTD++EajN7FhktUSYxukdWaCocAQJT +DNYP0K88G4rtC2IYy5JFn9SWz5ohx//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapR +aKt6OvSBwy8wAAAIEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3 +xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNzc/eYQ6 +VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkAAACBAOTgJiN8LebPnJYN +fKkuwSkdg7jhVq8zVXB2F4Y8fLzklldN9zst8r3VAQIbvQNS1KqcMjGvrpyIMZr5lDEBHc +h0DHIAbFbNdHUf4NsRozC3MI+pJ/sa9yVSGZ20U+dGLj4X/p7NUnHYT+rqaPSNP7EbCLjf +ehm+uSScfeIEOJPTAAAAAAEC +-----END OPENSSH PRIVATE KEY-----`; + // see https://runkit.com/gr2m/reproducable-jwt const BEARER = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOi0zMCwiZXhwIjo1NzAsImlzcyI6MX0.q3foRa78U3WegM5PrWLEh5N0bH1SD62OqW66ZYzArp95JBNiCbo8KAlGtiRENCIfBZT9ibDUWy82cI4g3F09mdTq3bD1xLavIfmTksIQCz5EymTWR5v6gL14LSmQdWY9lSqkgUG0XCFljWUglEP39H4yeHbFgdjvAYg3ifDS12z9oQz2ACdSpvxPiTuCC804HkPVw8Qoy0OSXvCkFU70l7VXCVUxnuhHnk8-oCGcKUspmeP6UdDnXk-Aus-eGwDfJbU2WritxxaXw6B4a3flTPojkYLSkPBr6Pi0H2-mBsW_Nvs0aLPVLKobQd4gqTkosX3967DoAG8luUMhrnxe8Q"; @@ -96,6 +127,23 @@ test("README example for app auth with private key in PKCS#8 format", async (t) }); }); +test("Throw error if key is OpenSSH", async (t) => { + MockDate.set(0); + + try { + await githubAppJwt({ + id: APP_ID, + privateKey: PRIVATEY_KEY_OPENSSH, + }); + t.fail("should throw"); + } catch (error) { + t.is( + error.message, + "[universal-github-app-jwt] Private Key is in OpenSSH format, but only PKCS is supported. See https://github.com/gr2m/universal-github-app-jwt#readme" + ); + } +}); + test("Include the time difference in the expiration and issued_at field", async (t) => { MockDate.set(0);