Skip to content

Commit

Permalink
fix: remove jsonwebtoken dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
gr2m committed Jul 9, 2023
1 parent a466aa4 commit 57aadbe
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 188 deletions.
89 changes: 53 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,10 @@
# universal-github-app-jwt

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

You can convert `OpenSSH` to `PKCS#8` using `ssh-keygen`:

```
cp private-key.pem private-key-pkcs8.key && ssh-keygen -m PKCS8 -N "" -f 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 same to convert `OpenSSH` to `PKCS#8`.

You can convert `PKCS#1` to `PKCS#8` in Node.js using the built-in `crypto` module:

```js
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",
});
```

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 Expand Up @@ -205,6 +171,57 @@ For a complete implementation of GitHub App authentication strategies, see [`@oc
</tbody>
</table>

<!-- do not remove this anchor, it's used in error messages -->

<a name="private-key-formats"></a>

## About Private Key formats

When downloading a `private-key.pem` file from GitHub, the format is in `PKCS#1` format. Unfortunately, the WebCrypto API only supports `PKCS#8`.

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.

You can identify the format based on the the first line

| First Line | Format |
| ------------------------------------- | ------- |
| `-----BEGIN RSA PRIVATE KEY-----` | PKCS#1 |
| `-----BEGIN PRIVATE KEY-----` | PKCS#8 |
| `-----BEGIN OPENSSH PRIVATE KEY-----` | OpenSSH |

### Converting `PKCS#1` to `PKCS#8`

If you use Node.js, you can convert the format before passing it to `universal-github-app-jwt`:

```js
import crypto from "node:crypto";
import githubAppJwt from "universal-github-app-jwt";

const privateKeyPkcs8 = crypto.createPrivateKey(process.env.PRIVATE_KEY).export({
type: "pkcs8",
format: "pem",
}

const { token, appId, expiration } = await githubAppJwt({
id: process.env.APP_ID,
privateKey: privateKeyPkcs8,
});
```
But we recommend to convert the format using `openssl` before storing it as environment variable.
```
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.key
```
### Converting `OpenSSH` to `PKCS#8`
```
cp private-key.pem private-key-pkcs8.key && ssh-keygen -m PKCS8 -N "" -f private-key-pkcs8.key
```
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.
## License
[MIT](LICENSE)
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check

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

/**
* @param {import(".").Options} options
Expand Down
8 changes: 8 additions & 0 deletions lib/crypto-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { subtle } = globalThis.crypto;

// no-op, unfortunately there is no way to transform from PKCS8 or OpenSSH to PKCS1 with WebCrypto
function convertPrivateKey(privateKey) {
return privateKey;
}

export { subtle, convertPrivateKey };
15 changes: 15 additions & 0 deletions lib/crypto-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// this can be removed once we only support Node 20+
export * from "node:crypto";
import { createPrivateKey } from "node:crypto";

import { isPkcs1 } from "./utils.js";

// no-op, unfortunately there is no way to transform from PKCS8 or OpenSSH to PKCS1 with WebCrypto
export function convertPrivateKey(privateKey) {
if (!isPkcs1(privateKey)) return privateKey;

return createPrivateKey(privateKey).export({
type: "pkcs8",
format: "pem",
});
}
19 changes: 0 additions & 19 deletions lib/get-token-node.js

This file was deleted.

22 changes: 15 additions & 7 deletions lib/get-token.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
// we don't @ts-check here because it chokes crypto which is a global API in modern JS runtime environments

import {
isPkcs1,
isOpenSsh,
getEncodedMessage,
getDERfromPEM,
string2ArrayBuffer,
base64encode,
} from "./utils.js";

import { subtle, convertPrivateKey } from "#crypto";

/**
* @param {import('../internals').GetTokenOptions} options
* @returns {Promise<string>}
*/
export async function getToken({ privateKey, payload }) {
const convertedPrivateKey = convertPrivateKey(privateKey);

// WebCrypto only supports PKCS#8, unfortunately
if (privateKey.includes("-----BEGIN RSA PRIVATE KEY-----")) {
if (isPkcs1(convertedPrivateKey)) {
throw new Error(
"[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"
"[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"
);
}

// WebCrypto does not support OpenSSH, unfortunately
if (privateKey.includes("-----BEGIN OPENSSH PRIVATE KEY-----")) {
/* c8 ignore start */
if (isOpenSsh(convertedPrivateKey)) {
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"
"[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"
);
}
/* c8 ignore stop */

const algorithm = {
name: "RSASSA-PKCS1-v1_5",
Expand All @@ -34,8 +42,8 @@ export async function getToken({ privateKey, payload }) {
/** @type {import('../internals').Header} */
const header = { alg: "RS256", typ: "JWT" };

const privateKeyDER = getDERfromPEM(privateKey);
const importedKey = await crypto.subtle.importKey(
const privateKeyDER = getDERfromPEM(convertedPrivateKey);
const importedKey = await subtle.importKey(
"pkcs8",
privateKeyDER,
algorithm,
Expand All @@ -46,7 +54,7 @@ export async function getToken({ privateKey, payload }) {
const encodedMessage = getEncodedMessage(header, payload);
const encodedMessageArrBuf = string2ArrayBuffer(encodedMessage);

const signatureArrBuf = await crypto.subtle.sign(
const signatureArrBuf = await subtle.sign(
algorithm.name,
importedKey,
encodedMessageArrBuf
Expand Down
20 changes: 17 additions & 3 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
// we don't @ts-check here because it chokes on atob and btoa which are available in all modern JS runtime environments
// @ts-check

/**
* @param {string} privateKey
* @returns {boolean}
*/
export function isPkcs1(privateKey) {
return privateKey.includes("-----BEGIN RSA PRIVATE KEY-----");
}

/**
* @param {string} privateKey
* @returns {boolean}
*/
export function isOpenSsh(privateKey) {
return privateKey.includes("-----BEGIN OPENSSH PRIVATE KEY-----");
}

/**
* @param {string} str
Expand Down Expand Up @@ -29,7 +45,6 @@ export function getDERfromPEM(pem) {
}

/**
*
* @param {import('../internals').Header} header
* @param {import('../internals').Payload} payload
* @returns {string}
Expand Down Expand Up @@ -62,7 +77,6 @@ function fromBase64(base64) {
}

/**
*
* @param {Record<string,unknown>} obj
* @returns {string}
*/
Expand Down
Loading

0 comments on commit 57aadbe

Please sign in to comment.