Skip to content

Commit

Permalink
fix: add helpful error in case private key is in OpenSSH format (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
gr2m committed Jul 8, 2023
1 parent 72ddd29 commit e79a993
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 26 deletions.
40 changes: 16 additions & 24 deletions README.md
Expand Up @@ -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

<table>
Expand Down
6 changes: 6 additions & 0 deletions lib/get-token-node.js
Expand Up @@ -7,6 +7,12 @@ import jsonwebtoken from "jsonwebtoken";
* @returns {Promise<string>}
*/
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",
});
Expand Down
11 changes: 9 additions & 2 deletions lib/get-token.js
Expand Up @@ -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"
);
}

Expand Down
48 changes: 48 additions & 0 deletions test/node.test.js
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit e79a993

Please sign in to comment.