denoland / denoland/celld

KeyObject.toCryptoKey() ignores requested usages, breaking jose RS256 signing

Open
#204 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
4.6k
Forks
171
PR merge metrics
No merged PRs in 30d

Description

Signing an RS256 JWT with a valid RSA private key imported through `node:crypto.createPrivateKey()` fails with:

```text
TypeError: CryptoKey does not support this operation, its usages must include sign.
```

The private key parses successfully. The failure occurs during local signing.

### Reduced reproduction

```js
import { createPrivateKey, generateKeyPairSync } from "node:crypto";
import { SignJWT } from "jose";

export default {
async fetch() {
const { privateKey } = generateKeyPairSync("rsa", {
modulusLength: 2048,
});

const key = createPrivateKey(
privateKey.export({ type: "pkcs1", format: "pem" }),
);

const converted = key.toCryptoKey(
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["sign"],
);

console.log(converted.usages); // Expected: ["sign"]

const jwt = await new SignJWT({})
.setProtectedHeader({ alg: "RS256" })
.sign(key);

return new Response(jwt);
},
};
```

### Expected behavior

`toCryptoKey()` honors the requested algorithm, extractability, and key usages. The converted key supports `"sign"`, and JWT signing succeeds.

The equivalent signing path succeeds in Node.js and Deno 2.7.10.

### Actual behavior

Celld constructs imported asymmetric keys with an empty usage list. `AsymmetricKeyObject.toCryptoKey()` ignores its arguments and returns the original handle unchanged.

jose internally calls this method when signing with a `KeyObject`, then rejects the resulting `CryptoKey` because its usages do not include `"sign"`.

### Impact

GitHub App authentication fails before an installation token can be requested. This prevents sandbox commands from obtaining their GitHub credentials despite having a valid private key and configuration.

### Workaround

Export the validated key as PKCS#8 and explicitly import it through WebCrypto before signing:

```js
import { importPKCS8, SignJWT } from "jose";

const signingKey = await importPKCS8(
key.export({ type: "pkcs8", format: "pem" }).toString(),
"RS256",
);

const jwt = await new SignJWT({})
.setProtectedHeader({ alg: "RS256" })
.sign(signingKey);
```

This bypasses `KeyObject.toCryptoKey()`. Regression tests and a Deno smoke check pass; the workaround has not yet been verified end-to-end on deployed Celld.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at AsymmetricKeyObject.toCryptoKey(), the entry point identified in the report, and review the existing regression tests and Deno smoke check. Done means the requested algorithm, extractability, and usages are honored, the converted key includes "sign", and the RS256 signing reproduction succeeds.

Written by the indexing model from the issue text.

Assessment

Tech stack
deno, javascript, node.js, rust
Domain
cryptography
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.