Skip to content

Commit c8021a2

Browse files
authored
fix: remove jsonwebtoken dependency (#75)
1 parent a466aa4 commit c8021a2

10 files changed

Lines changed: 150 additions & 187 deletions

File tree

README.md

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,10 @@
11
# universal-github-app-jwt
22

3-
> Calculate GitHub App bearer tokens for Node & modern browsers
3+
> Calculate GitHub App bearer tokens for Node, Deno, and modern browsers
44
5-
[![@latest](https://img.shields.io/npm/vuniversal-github-app-jwt.svg)](https://www.npmjs.com/packageuniversal-github-app-jwt)
5+
[![@latest](https://img.shields.io/npm/universal-github-app-jwt.svg)](https://www.npmjs.com/universal-github-app-jwt)
66
[![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)
77

8-
⚠ 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-----`.
9-
10-
You can convert `PKCS#1` to `PKCS#8` using `oppenssl`:
11-
12-
```
13-
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.key
14-
```
15-
16-
You can convert `OpenSSH` to `PKCS#8` using `ssh-keygen`:
17-
18-
```
19-
cp private-key.pem private-key-pkcs8.key && ssh-keygen -m PKCS8 -N "" -f private-key-pkcs8.key
20-
```
21-
22-
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`.
23-
24-
You can convert `PKCS#1` to `PKCS#8` in Node.js using the built-in `crypto` module:
25-
26-
```js
27-
const crypto = require("crypto");
28-
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
29-
...
30-
-----END RSA PRIVATE KEY-----`;
31-
32-
const privateKeyPkcs8 = crypto.createPrivateKey(PRIVATE_KEY).export({
33-
type: "pkcs8",
34-
format: "pem",
35-
});
36-
```
37-
38-
When using a node, a conversion is not necessary, the implementation is agnostic to either `PKCS` format.
39-
40-
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)).
41-
428
## Usage
439

4410
<table>
@@ -205,6 +171,57 @@ For a complete implementation of GitHub App authentication strategies, see [`@oc
205171
</tbody>
206172
</table>
207173

174+
<!-- do not remove this anchor, it's used in error messages -->
175+
176+
<a name="private-key-formats"></a>
177+
178+
## About Private Key formats
179+
180+
When downloading a `private-key.pem` file from GitHub, the format is in `PKCS#1` format. Unfortunately, the WebCrypto API only supports `PKCS#8`.
181+
182+
If you use 1Password to store a private key as an SSH key, it will be transformed to the `OpenSSH` format, which is also not supported by WebCrypto.
183+
184+
You can identify the format based on the the first line
185+
186+
| First Line | Format |
187+
| ------------------------------------- | ------- |
188+
| `-----BEGIN RSA PRIVATE KEY-----` | PKCS#1 |
189+
| `-----BEGIN PRIVATE KEY-----` | PKCS#8 |
190+
| `-----BEGIN OPENSSH PRIVATE KEY-----` | OpenSSH |
191+
192+
### Converting `PKCS#1` to `PKCS#8`
193+
194+
If you use Node.js, you can convert the format before passing it to `universal-github-app-jwt`:
195+
196+
```js
197+
import crypto from "node:crypto";
198+
import githubAppJwt from "universal-github-app-jwt";
199+
200+
const privateKeyPkcs8 = crypto.createPrivateKey(process.env.PRIVATE_KEY).export({
201+
type: "pkcs8",
202+
format: "pem",
203+
}
204+
205+
const { token, appId, expiration } = await githubAppJwt({
206+
id: process.env.APP_ID,
207+
privateKey: privateKeyPkcs8,
208+
});
209+
```
210+
211+
But we recommend to convert the format using `openssl` before passing it to your app.
212+
213+
```
214+
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.key
215+
```
216+
217+
### Converting `OpenSSH` to `PKCS#8`
218+
219+
```
220+
cp private-key.pem private-key-pkcs8.key && ssh-keygen -m PKCS8 -N "" -f private-key-pkcs8.key
221+
```
222+
223+
I'm looking for help to create a minimal `OpenSSH` to `PKCS` 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.
224+
208225
## License
209226
210227
[MIT](LICENSE)

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22

33
// @ts-ignore - #get-token is defined in "imports" in package.json
4-
import { getToken } from "#get-token";
4+
import { getToken } from "./lib/get-token.js";
55

66
/**
77
* @param {import(".").Options} options

lib/crypto-native.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const { subtle } = globalThis.crypto;
2+
3+
// no-op, unfortunately there is no way to transform from PKCS8 or OpenSSH to PKCS1 with WebCrypto
4+
function convertPrivateKey(privateKey) {
5+
return privateKey;
6+
}
7+
8+
export { subtle, convertPrivateKey };

lib/crypto-node.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// this can be removed once we only support Node 20+
2+
export * from "node:crypto";
3+
import { createPrivateKey } from "node:crypto";
4+
5+
import { isPkcs1 } from "./utils.js";
6+
7+
// no-op, unfortunately there is no way to transform from PKCS8 or OpenSSH to PKCS1 with WebCrypto
8+
export function convertPrivateKey(privateKey) {
9+
if (!isPkcs1(privateKey)) return privateKey;
10+
11+
return createPrivateKey(privateKey).export({
12+
type: "pkcs8",
13+
format: "pem",
14+
});
15+
}

lib/get-token-node.js

Lines changed: 0 additions & 19 deletions
This file was deleted.

lib/get-token.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
// we don't @ts-check here because it chokes crypto which is a global API in modern JS runtime environments
22

33
import {
4+
isPkcs1,
5+
isOpenSsh,
46
getEncodedMessage,
57
getDERfromPEM,
68
string2ArrayBuffer,
79
base64encode,
810
} from "./utils.js";
911

12+
import { subtle, convertPrivateKey } from "#crypto";
13+
1014
/**
1115
* @param {import('../internals').GetTokenOptions} options
1216
* @returns {Promise<string>}
1317
*/
1418
export async function getToken({ privateKey, payload }) {
19+
const convertedPrivateKey = convertPrivateKey(privateKey);
20+
1521
// WebCrypto only supports PKCS#8, unfortunately
16-
if (privateKey.includes("-----BEGIN RSA PRIVATE KEY-----")) {
22+
/* c8 ignore start */
23+
if (isPkcs1(convertedPrivateKey)) {
1724
throw new Error(
18-
"[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"
25+
"[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"
1926
);
2027
}
28+
/* c8 ignore stop */
2129

2230
// WebCrypto does not support OpenSSH, unfortunately
23-
if (privateKey.includes("-----BEGIN OPENSSH PRIVATE KEY-----")) {
31+
if (isOpenSsh(convertedPrivateKey)) {
2432
throw new Error(
25-
"[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"
33+
"[universal-github-app-jwt] Private Key is in OpenSSH format, but only PKCS#8 is supported. See https://github.com/gr2m/universal-github-app-jwt#readme"
2634
);
2735
}
2836

@@ -34,8 +42,8 @@ export async function getToken({ privateKey, payload }) {
3442
/** @type {import('../internals').Header} */
3543
const header = { alg: "RS256", typ: "JWT" };
3644

37-
const privateKeyDER = getDERfromPEM(privateKey);
38-
const importedKey = await crypto.subtle.importKey(
45+
const privateKeyDER = getDERfromPEM(convertedPrivateKey);
46+
const importedKey = await subtle.importKey(
3947
"pkcs8",
4048
privateKeyDER,
4149
algorithm,
@@ -46,7 +54,7 @@ export async function getToken({ privateKey, payload }) {
4654
const encodedMessage = getEncodedMessage(header, payload);
4755
const encodedMessageArrBuf = string2ArrayBuffer(encodedMessage);
4856

49-
const signatureArrBuf = await crypto.subtle.sign(
57+
const signatureArrBuf = await subtle.sign(
5058
algorithm.name,
5159
importedKey,
5260
encodedMessageArrBuf

lib/utils.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
// we don't @ts-check here because it chokes on atob and btoa which are available in all modern JS runtime environments
22

3+
/**
4+
* @param {string} privateKey
5+
* @returns {boolean}
6+
*/
7+
export function isPkcs1(privateKey) {
8+
return privateKey.includes("-----BEGIN RSA PRIVATE KEY-----");
9+
}
10+
11+
/**
12+
* @param {string} privateKey
13+
* @returns {boolean}
14+
*/
15+
export function isOpenSsh(privateKey) {
16+
return privateKey.includes("-----BEGIN OPENSSH PRIVATE KEY-----");
17+
}
18+
319
/**
420
* @param {string} str
521
* @returns {ArrayBuffer}
@@ -29,7 +45,6 @@ export function getDERfromPEM(pem) {
2945
}
3046

3147
/**
32-
*
3348
* @param {import('../internals').Header} header
3449
* @param {import('../internals').Payload} payload
3550
* @returns {string}
@@ -62,7 +77,6 @@ function fromBase64(base64) {
6277
}
6378

6479
/**
65-
*
6680
* @param {Record<string,unknown>} obj
6781
* @returns {string}
6882
*/

0 commit comments

Comments
 (0)