KeyObject.toCryptoKey() ignores requested usages, breaking jose RS256 signing
- 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