Crypto parity: RSA key generation lengths, RSA-PSS keys and encrypted private keys (#2103)

This commit is contained in:
Chris Tate
2026-10-25 09:45:21 -05:00
committed by GitHub
25 changed files with 1967 additions and 86 deletions
@@ -31,22 +31,25 @@
`checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
RSA, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
RSA, RSA-PSS, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
`generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback.
RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
RSA keys generate with modulus lengths from 512 to 8192 bits, an odd length from 2048 bits yielding one bit less as in Node, and odd public exponents up to 4294967295; longer moduli that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points.
Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error.
They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`.
`crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm.
RSA-PSS keys follow their RSASSA-PSS-params as Node does: PSS padding only, the key's digest and MGF1 digest, and salts of at least the key's length, with OpenSSL's errors; their details name the parameters, they export as PKCS#8 and SubjectPublicKeyInfo only, and digests other than SHA-1 and SHA-2 in their options throw a coded SC2020 error.
Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export.
X509Certificate supports `publicKey`, `verify()`, `checkIssued()`, `checkPrivateKey()`, `checkHost()`, `checkEmail()`, `checkIP()`, `toLegacyObject()`, `toString()`, and `toJSON()`, and `String()`, template literals, and `+` give its PEM text as `toString()` does.
`publicKey`, `verify()`, and `checkPrivateKey()` are refused in wasm32-wasi builds, and an Ed25519 certificate's `verify()` needs the `ed25519` literal like the other Ed25519 operations.
Own-key walks over a certificate (`Object.keys()`, `Object.entries()`, spread, `for...in`, and `hasOwnProperty`) are refused, since Node keeps its fields as prototype getters.
`JSON.stringify()` and `console.log()` of a certificate, a certificate stored in an `unknown` or `any` slot, and its methods on a union-typed value are also refused.
Sign and Verify values accumulate `update()` data and are not writable streams.
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
Private keys export encrypted (`cipher`, `passphrase`) with aes-128, aes-192, and aes-256 in CBC and ECB modes and with des-ede3-cbc, PKCS#8 as PBES2 with PBKDF2-HMAC-SHA256 and PKCS#1 and SEC1 as traditional PEM, and the ciphers OpenSSL refuses for a form throw its errors; a cipher option can also be an OID; other ciphers Node encrypts with throw a coded SC2020 error.
Encrypted private keys import with a `passphrase` from PBES2 with HMAC-SHA PRFs and the CBC and ECB ciphers, and from traditional PEM with the CBC ciphers, with Node's errors for a missing or wrong passphrase; other schemes and ciphers throw a coded SC2020 error once a passphrase is given.
The other key types (`dsa`, `ed448`, `x448`, and the rest) and wasm32-wasi builds throw a coded SC2020 error or are refused.
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
`publicEncrypt`/`privateDecrypt` remain unsupported.
The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript.
+27 -4
View File
@@ -2914,6 +2914,8 @@ declare module "crypto" {
export interface AsymmetricKeyExportOptions<T extends KeyFormat> {
type: "pkcs1" | "spki" | "pkcs8" | "sec1";
format: T;
cipher?: string | undefined;
passphrase?: string | Buffer | undefined;
}
export interface AsymmetricKeyDetails {
modulusLength?: number;
@@ -2938,6 +2940,7 @@ declare module "crypto" {
format?: "pem" | "der" | undefined;
type?: "pkcs1" | "pkcs8" | "sec1" | undefined;
encoding?: string | undefined;
passphrase?: string | Buffer | undefined;
}
export interface PublicKeyInput {
key: string | Buffer;
@@ -2974,12 +2977,22 @@ declare module "crypto" {
modulusLength: number;
publicExponent?: number | undefined;
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF };
privateKeyEncoding: {
type: "pkcs1" | "pkcs8";
format: PrivF;
cipher?: string | undefined;
passphrase?: string | undefined;
};
}
export interface ECKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
namedCurve: string;
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF };
privateKeyEncoding: {
type: "sec1" | "pkcs8";
format: PrivF;
cipher?: string | undefined;
passphrase?: string | undefined;
};
}
export function generateKeyPairSync(
type: "rsa",
@@ -3007,7 +3020,12 @@ declare module "crypto" {
): KeyPairKeyObjectResult;
export interface ED25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
publicKeyEncoding: { type: "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
privateKeyEncoding: {
type: "pkcs8";
format: PrivF;
cipher?: string | undefined;
passphrase?: string | undefined;
};
}
export function generateKeyPairSync(
type: "ed25519",
@@ -3020,7 +3038,12 @@ declare module "crypto" {
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
export interface X25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
publicKeyEncoding: { type: "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
privateKeyEncoding: {
type: "pkcs8";
format: PrivF;
cipher?: string | undefined;
passphrase?: string | undefined;
};
}
export function generateKeyPairSync(
type: "x25519",
+7 -4
View File
@@ -884,10 +884,11 @@ const RUNTIME_FEATURE_RULES = {
mod.functions.some(
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
),
// The legacy url functions, crypto.createCipheriv and
// ecdh.setPublicKey emit their deprecation warnings through the
// dyn-async unit (WebCrypto's encrypt and decrypt link scr_cipher.c
// too); scr_wasm.c boxes its handles and promises through it.
// The legacy url functions, crypto.createCipheriv,
// ecdh.setPublicKey and generateKeyPair's rsa-pss options emit their
// deprecation warnings through the dyn-async unit (WebCrypto's
// encrypt and decrypt link scr_cipher.c too); scr_wasm.c boxes its
// handles and promises through it.
fns: [
...DYN_ASYNC_LIB_FNS,
...READABLE_DYN_ASYNC_LIB_FNS,
@@ -898,6 +899,8 @@ const RUNTIME_FEATURE_RULES = {
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"crypto.ecdhNew",
"crypto.generateKeyPairCb",
"crypto.generateKeyPairSync",
],
prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."],
expr: (node, tables) =>
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8165-rsa-keygen-lengths.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8166-rsa-pss-keys.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8167-encrypted-private-keys.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8178-rsa-pss-auto-min-salt.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8183-key-passphrase-encoding.cjs"
],
"diags": []
}
+3 -42
View File
@@ -12,6 +12,7 @@
* compared in constant time. A cipher used as a stream reads and writes
* through its own native Transform, which the stream unit builds on first
* use, as Node's LazyTransform is. All arguments are borrowed. */
#include "scr_cipher_names.h"
#include "scr_runtime.h"
#include <stdio.h>
@@ -114,52 +115,12 @@ static const ScCipherSpec sc_specs[] = {
static const struct {
const char *name;
unsigned iv_len;
} sc_legacy[] = {
{"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8},
{"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0},
{"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8},
{"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0},
{"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0},
{"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16},
{"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8},
};
} sc_legacy[] = {SCR_CIPHER_LEGACY_ENTRIES};
/* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5). Names in this list that
* have no implementation throw a coded SC2020 error instead of Node's
* ERR_CRYPTO_UNKNOWN_CIPHER. */
static const char *const sc_known[] = {
"aes-128-cbc", "aes-128-cbc-cts", "aes-128-ccm", "aes-128-cfb", "aes-128-cfb1", "aes-128-cfb8",
"aes-128-ctr", "aes-128-ecb", "aes-128-gcm", "aes-128-gcm-siv", "aes-128-ocb", "aes-128-ofb",
"aes-128-siv", "aes-128-wrap", "aes-128-wrap-inv", "aes-128-wrap-pad", "aes-128-wrap-pad-inv",
"aes-128-xts", "aes-192-cbc", "aes-192-cbc-cts", "aes-192-ccm", "aes-192-cfb", "aes-192-cfb1",
"aes-192-cfb8", "aes-192-ctr", "aes-192-ecb", "aes-192-gcm", "aes-192-gcm-siv", "aes-192-ocb",
"aes-192-ofb", "aes-192-siv", "aes-192-wrap", "aes-192-wrap-inv", "aes-192-wrap-pad",
"aes-192-wrap-pad-inv", "aes-256-cbc", "aes-256-cbc-cts", "aes-256-ccm", "aes-256-cfb",
"aes-256-cfb1", "aes-256-cfb8", "aes-256-ctr", "aes-256-ecb", "aes-256-gcm", "aes-256-gcm-siv",
"aes-256-ocb", "aes-256-ofb", "aes-256-siv", "aes-256-wrap", "aes-256-wrap-inv",
"aes-256-wrap-pad", "aes-256-wrap-pad-inv", "aes-256-xts", "aes128", "aes128-wrap",
"aes128-wrap-inv", "aes128-wrap-pad", "aes128-wrap-pad-inv", "aes192", "aes192-wrap",
"aes192-wrap-inv", "aes192-wrap-pad", "aes192-wrap-pad-inv", "aes256", "aes256-wrap",
"aes256-wrap-inv", "aes256-wrap-pad", "aes256-wrap-pad-inv", "aria-128-cbc", "aria-128-ccm",
"aria-128-cfb", "aria-128-cfb1", "aria-128-cfb8", "aria-128-ctr", "aria-128-ecb", "aria-128-gcm",
"aria-128-ofb", "aria-192-cbc", "aria-192-ccm", "aria-192-cfb", "aria-192-cfb1", "aria-192-cfb8",
"aria-192-ctr", "aria-192-ecb", "aria-192-gcm", "aria-192-ofb", "aria-256-cbc", "aria-256-ccm",
"aria-256-cfb", "aria-256-cfb1", "aria-256-cfb8", "aria-256-ctr", "aria-256-ecb", "aria-256-gcm",
"aria-256-ofb", "aria128", "aria192", "aria256", "camellia-128-cbc", "camellia-128-cbc-cts",
"camellia-128-cfb", "camellia-128-cfb1", "camellia-128-cfb8", "camellia-128-ctr",
"camellia-128-ecb", "camellia-128-ofb", "camellia-192-cbc", "camellia-192-cbc-cts",
"camellia-192-cfb", "camellia-192-cfb1", "camellia-192-cfb8", "camellia-192-ctr",
"camellia-192-ecb", "camellia-192-ofb", "camellia-256-cbc", "camellia-256-cbc-cts",
"camellia-256-cfb", "camellia-256-cfb1", "camellia-256-cfb8", "camellia-256-ctr",
"camellia-256-ecb", "camellia-256-ofb", "camellia128", "camellia192", "camellia256", "chacha20",
"chacha20-poly1305", "des-ede", "des-ede-cbc", "des-ede-cfb", "des-ede-ecb", "des-ede-ofb",
"des-ede3", "des-ede3-cbc", "des-ede3-cfb", "des-ede3-cfb1", "des-ede3-cfb8", "des-ede3-ecb",
"des-ede3-ofb", "des3", "des3-wrap", "id-aes128-ccm", "id-aes128-gcm", "id-aes128-wrap",
"id-aes128-wrap-pad", "id-aes192-ccm", "id-aes192-gcm", "id-aes192-wrap", "id-aes192-wrap-pad",
"id-aes256-ccm", "id-aes256-gcm", "id-aes256-wrap", "id-aes256-wrap-pad",
"id-smime-alg-cms3deswrap", "sm4", "sm4-cbc", "sm4-ccm", "sm4-cfb", "sm4-cfb128", "sm4-ctr",
"sm4-ecb", "sm4-gcm", "sm4-ofb", "sm4-ofb128", "sm4-xts",
};
static const char *const sc_known[] = {SCR_CIPHER_KNOWN_NAMES};
enum { SC_NO_TAG = -1 };
enum { SC_TAG_UNKNOWN, SC_TAG_SET_BY_USER, SC_TAG_COMPUTED };
+100
View File
@@ -0,0 +1,100 @@
/* crypto.getCiphers() on Node 24.22 (OpenSSL 3.5), the names a cipher
* option can give. The cipher and pki units each include it: names
* without an implementation there throw a coded SC2020 error instead of
* Node's ERR_CRYPTO_UNKNOWN_CIPHER. */
#ifndef SCR_CIPHER_NAMES_H
#define SCR_CIPHER_NAMES_H
#define SCR_CIPHER_KNOWN_NAMES \
"aes-128-cbc", "aes-128-cbc-cts", "aes-128-ccm", "aes-128-cfb", "aes-128-cfb1", "aes-128-cfb8", \
"aes-128-ctr", "aes-128-ecb", "aes-128-gcm", "aes-128-gcm-siv", "aes-128-ocb", "aes-128-ofb", \
"aes-128-siv", "aes-128-wrap", "aes-128-wrap-inv", "aes-128-wrap-pad", "aes-128-wrap-pad-inv", \
"aes-128-xts", "aes-192-cbc", "aes-192-cbc-cts", "aes-192-ccm", "aes-192-cfb", "aes-192-cfb1", \
"aes-192-cfb8", "aes-192-ctr", "aes-192-ecb", "aes-192-gcm", "aes-192-gcm-siv", "aes-192-ocb", \
"aes-192-ofb", "aes-192-siv", "aes-192-wrap", "aes-192-wrap-inv", "aes-192-wrap-pad", \
"aes-192-wrap-pad-inv", "aes-256-cbc", "aes-256-cbc-cts", "aes-256-ccm", "aes-256-cfb", \
"aes-256-cfb1", "aes-256-cfb8", "aes-256-ctr", "aes-256-ecb", "aes-256-gcm", "aes-256-gcm-siv", \
"aes-256-ocb", "aes-256-ofb", "aes-256-siv", "aes-256-wrap", "aes-256-wrap-inv", \
"aes-256-wrap-pad", "aes-256-wrap-pad-inv", "aes-256-xts", "aes128", "aes128-wrap", \
"aes128-wrap-inv", "aes128-wrap-pad", "aes128-wrap-pad-inv", "aes192", "aes192-wrap", \
"aes192-wrap-inv", "aes192-wrap-pad", "aes192-wrap-pad-inv", "aes256", "aes256-wrap", \
"aes256-wrap-inv", "aes256-wrap-pad", "aes256-wrap-pad-inv", "aria-128-cbc", "aria-128-ccm", \
"aria-128-cfb", "aria-128-cfb1", "aria-128-cfb8", "aria-128-ctr", "aria-128-ecb", "aria-128-gcm", \
"aria-128-ofb", "aria-192-cbc", "aria-192-ccm", "aria-192-cfb", "aria-192-cfb1", "aria-192-cfb8", \
"aria-192-ctr", "aria-192-ecb", "aria-192-gcm", "aria-192-ofb", "aria-256-cbc", "aria-256-ccm", \
"aria-256-cfb", "aria-256-cfb1", "aria-256-cfb8", "aria-256-ctr", "aria-256-ecb", "aria-256-gcm", \
"aria-256-ofb", "aria128", "aria192", "aria256", "camellia-128-cbc", "camellia-128-cbc-cts", \
"camellia-128-cfb", "camellia-128-cfb1", "camellia-128-cfb8", "camellia-128-ctr", \
"camellia-128-ecb", "camellia-128-ofb", "camellia-192-cbc", "camellia-192-cbc-cts", \
"camellia-192-cfb", "camellia-192-cfb1", "camellia-192-cfb8", "camellia-192-ctr", \
"camellia-192-ecb", "camellia-192-ofb", "camellia-256-cbc", "camellia-256-cbc-cts", \
"camellia-256-cfb", "camellia-256-cfb1", "camellia-256-cfb8", "camellia-256-ctr", \
"camellia-256-ecb", "camellia-256-ofb", "camellia128", "camellia192", "camellia256", "chacha20", \
"chacha20-poly1305", "des-ede", "des-ede-cbc", "des-ede-cfb", "des-ede-ecb", "des-ede-ofb", \
"des-ede3", "des-ede3-cbc", "des-ede3-cfb", "des-ede3-cfb1", "des-ede3-cfb8", "des-ede3-ecb", \
"des-ede3-ofb", "des3", "des3-wrap", "id-aes128-ccm", "id-aes128-gcm", "id-aes128-wrap", \
"id-aes128-wrap-pad", "id-aes192-ccm", "id-aes192-gcm", "id-aes192-wrap", "id-aes192-wrap-pad", \
"id-aes256-ccm", "id-aes256-gcm", "id-aes256-wrap", "id-aes256-wrap-pad", \
"id-smime-alg-cms3deswrap", "sm4", "sm4-cbc", "sm4-ccm", "sm4-cfb", "sm4-cfb128", "sm4-ctr", \
"sm4-ecb", "sm4-gcm", "sm4-ofb", "sm4-ofb128", "sm4-xts",
/* The ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does
* not load, with their IV lengths: OpenSSL knows their names, and using
* them fails with its "unsupported" error. */
#define SCR_CIPHER_LEGACY_ENTRIES \
{"des", 8}, {"des-cbc", 8}, {"des-ecb", 0}, {"des-cfb", 8}, {"des-cfb1", 8}, {"des-cfb8", 8}, \
{"des-ofb", 8}, {"desx", 8}, {"desx-cbc", 8}, {"bf", 8}, {"bf-cbc", 8}, {"bf-ecb", 0}, \
{"bf-cfb", 8}, {"bf-ofb", 8}, {"blowfish", 8}, {"cast", 8}, {"cast-cbc", 8}, {"cast5-cbc", 8}, \
{"cast5-ecb", 0}, {"cast5-cfb", 8}, {"cast5-ofb", 8}, {"rc2", 8}, {"rc2-cbc", 8}, {"rc2-ecb", 0}, \
{"rc2-cfb", 8}, {"rc2-ofb", 8}, {"rc2-40-cbc", 8}, {"rc2-64-cbc", 8}, {"rc4", 0}, {"rc4-40", 0}, \
{"rc4-hmac-md5", 0}, {"seed", 16}, {"seed-cbc", 16}, {"seed-ecb", 0}, {"seed-cfb", 16}, \
{"seed-ofb", 16}, {"idea", 8}, {"idea-cbc", 8}, {"idea-ecb", 0}, {"idea-cfb", 8}, {"idea-ofb", 8},
/* The OID texts OpenSSL's providers register as cipher aliases, with the
* name each fetches (crypto.getCipherInfo(oid).name on Node 24.22). */
#define SCR_CIPHER_OID_ALIASES \
{"2.16.840.1.101.3.4.1.1", "aes-128-ecb"}, {"2.16.840.1.101.3.4.1.2", "aes-128-cbc"}, \
{"2.16.840.1.101.3.4.1.3", "aes-128-ofb"}, {"2.16.840.1.101.3.4.1.4", "aes-128-cfb"}, \
{"2.16.840.1.101.3.4.1.5", "id-aes128-wrap"}, {"2.16.840.1.101.3.4.1.6", "id-aes128-gcm"}, \
{"2.16.840.1.101.3.4.1.7", "id-aes128-ccm"}, {"2.16.840.1.101.3.4.1.8", "id-aes128-wrap-pad"}, \
{"2.16.840.1.101.3.4.1.21", "aes-192-ecb"}, {"2.16.840.1.101.3.4.1.22", "aes-192-cbc"}, \
{"2.16.840.1.101.3.4.1.23", "aes-192-ofb"}, {"2.16.840.1.101.3.4.1.24", "aes-192-cfb"}, \
{"2.16.840.1.101.3.4.1.25", "id-aes192-wrap"}, {"2.16.840.1.101.3.4.1.26", "id-aes192-gcm"}, \
{"2.16.840.1.101.3.4.1.27", "id-aes192-ccm"}, {"2.16.840.1.101.3.4.1.28", "id-aes192-wrap-pad"}, \
{"2.16.840.1.101.3.4.1.41", "aes-256-ecb"}, {"2.16.840.1.101.3.4.1.42", "aes-256-cbc"}, \
{"2.16.840.1.101.3.4.1.43", "aes-256-ofb"}, {"2.16.840.1.101.3.4.1.44", "aes-256-cfb"}, \
{"2.16.840.1.101.3.4.1.45", "id-aes256-wrap"}, {"2.16.840.1.101.3.4.1.46", "id-aes256-gcm"}, \
{"2.16.840.1.101.3.4.1.47", "id-aes256-ccm"}, {"2.16.840.1.101.3.4.1.48", "id-aes256-wrap-pad"}, \
{"1.2.410.200046.1.1.1", "aria-128-ecb"}, {"1.2.410.200046.1.1.2", "aria-128-cbc"}, \
{"1.2.410.200046.1.1.3", "aria-128-cfb"}, {"1.2.410.200046.1.1.4", "aria-128-ofb"}, \
{"1.2.410.200046.1.1.5", "aria-128-ctr"}, {"1.2.410.200046.1.1.6", "aria-192-ecb"}, \
{"1.2.410.200046.1.1.7", "aria-192-cbc"}, {"1.2.410.200046.1.1.8", "aria-192-cfb"}, \
{"1.2.410.200046.1.1.9", "aria-192-ofb"}, {"1.2.410.200046.1.1.10", "aria-192-ctr"}, \
{"1.2.410.200046.1.1.11", "aria-256-ecb"}, {"1.2.410.200046.1.1.12", "aria-256-cbc"}, \
{"1.2.410.200046.1.1.13", "aria-256-cfb"}, {"1.2.410.200046.1.1.14", "aria-256-ofb"}, \
{"1.2.410.200046.1.1.15", "aria-256-ctr"}, {"1.2.410.200046.1.1.34", "aria-128-gcm"}, \
{"1.2.410.200046.1.1.35", "aria-192-gcm"}, {"1.2.410.200046.1.1.36", "aria-256-gcm"}, \
{"1.2.410.200046.1.1.37", "aria-128-ccm"}, {"1.2.410.200046.1.1.38", "aria-192-ccm"}, \
{"1.2.410.200046.1.1.39", "aria-256-ccm"}, {"0.3.4401.5.3.1.9.1", "camellia-128-ecb"}, \
{"0.3.4401.5.3.1.9.3", "camellia-128-ofb"}, {"0.3.4401.5.3.1.9.4", "camellia-128-cfb"}, \
{"0.3.4401.5.3.1.9.9", "camellia-128-ctr"}, {"0.3.4401.5.3.1.9.21", "camellia-192-ecb"}, \
{"0.3.4401.5.3.1.9.23", "camellia-192-ofb"}, {"0.3.4401.5.3.1.9.24", "camellia-192-cfb"}, \
{"0.3.4401.5.3.1.9.29", "camellia-192-ctr"}, {"0.3.4401.5.3.1.9.41", "camellia-256-ecb"}, \
{"0.3.4401.5.3.1.9.43", "camellia-256-ofb"}, {"0.3.4401.5.3.1.9.44", "camellia-256-cfb"}, \
{"0.3.4401.5.3.1.9.49", "camellia-256-ctr"}, {"1.2.156.10197.1.104.1", "sm4-ecb"}, \
{"1.2.156.10197.1.104.2", "sm4-cbc"}, {"1.2.156.10197.1.104.3", "sm4-ofb"}, \
{"1.2.156.10197.1.104.4", "sm4-cfb"}, {"1.2.156.10197.1.104.7", "sm4-ctr"}, \
{"1.2.156.10197.1.104.8", "sm4-gcm"}, {"1.2.156.10197.1.104.9", "sm4-ccm"}, \
{"1.2.156.10197.1.104.10", "sm4-xts"}, {"1.2.392.200011.61.1.1.1.2", "camellia-128-cbc"}, \
{"1.2.392.200011.61.1.1.1.3", "camellia-192-cbc"}, \
{"1.2.392.200011.61.1.1.1.4", "camellia-256-cbc"}, {"1.2.840.113549.3.2", "rc2-cbc"}, \
{"1.2.840.113549.3.4", "rc4"}, {"1.2.840.113549.3.7", "des-ede3-cbc"}, \
{"1.2.840.113549.1.9.16.3.6", "id-smime-alg-cms3deswrap"}, {"1.3.14.3.2.6", "des-ecb"}, \
{"1.3.14.3.2.7", "des-cbc"}, {"1.3.14.3.2.8", "des-ofb"}, {"1.3.14.3.2.9", "des-cfb"}, \
{"1.3.14.3.2.17", "des-ede"}, {"1.3.111.2.1619.0.1.1", "aes-128-xts"}, \
{"1.3.111.2.1619.0.1.2", "aes-256-xts"}, {"1.3.6.1.4.1.3029.1.2", "bf-cbc"}, \
{"1.3.6.1.4.1.188.7.1.1.2", "idea-cbc"}, {"1.2.410.200004.1.3", "seed-ecb"}, \
{"1.2.410.200004.1.4", "seed-cbc"}, {"1.2.410.200004.1.5", "seed-cfb"}, \
{"1.2.410.200004.1.6", "seed-ofb"}, {"1.2.840.113533.7.66.10", "cast5-cbc"},
#endif
+5
View File
@@ -8145,6 +8145,11 @@ static bool scr_digest_alg(const char *name, size_t len, ScrDigestAlg *out) {
return false;
}
bool scr_digest_known(const char *name, size_t len) {
ScrDigestAlg alg;
return scr_digest_alg(name, len, &alg);
}
/* createHash() and crypto.hash() also accept "dss1" in any case. */
static bool scr_hash_alg(const char *name, size_t len, ScrDigestAlg *out) {
if (scr_digest_alg(name, len, out)) return true;
+3
View File
@@ -63,6 +63,9 @@ bool scr_lib_fork_info(double *target, uintptr_t *read_handle,
* direct startup never filters user arguments. */
bool scr_lib_should_collapse_reexec_arg(ScrStr *cmd, ScrArr *args);
ScrStr *scr_process_platform(void); /* +1 interned ("darwin", "linux", ...) */
/* Whether OpenSSL, as Node 24 links it, names a digest `name` (its names,
* aliases and object identifiers, case-insensitively). */
bool scr_digest_known(const char *name, size_t len);
ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */
bool scr_stdio_write(int fd, const void *data, size_t len);
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -380,7 +380,8 @@ static ScrPromise *sw_generate_key(const ScrDyn *args) {
scr_throw_domex("OperationError", "algorithm.publicExponent must be odd");
goto done;
}
if (bits > 16384 || value < 3) {
/* The pki unit's numbers hold moduli up to 8192 bits. */
if (bits > 8192 || value < 3) {
scr_wc_unsupported("generateKey", "this RSA modulus length or public exponent");
goto done;
}
+40
View File
@@ -0,0 +1,40 @@
// RSA key generation within OpenSSL's limits: any modulus length from
// 512 bits (an odd length from 2048 bits yields one bit less, as
// OpenSSL's SP 800-56B generation does) and odd exponents up to 2^32 - 1.
"use strict";
const crypto = require("crypto");
const cases = [
[512, 65537],
[513, 65537],
[1023, 3],
[1024, 0x10001],
[2049, 65537],
[768, 4294967295],
[520, 17],
];
for (const [modulusLength, publicExponent] of cases) {
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength, publicExponent });
const details = privateKey.asymmetricKeyDetails;
const data = Buffer.from("rsa key lengths");
const signature = crypto.sign("sha256", data, privateKey);
console.log(
modulusLength,
details.modulusLength,
details.publicExponent,
publicKey.asymmetricKeyDetails.modulusLength,
signature.length,
crypto.verify("sha256", data, publicKey, signature),
);
}
for (const [modulusLength, publicExponent] of [[511, 65537], [1024, 1], [1024, 4], [1024, 0]]) {
try {
crypto.generateKeyPairSync("rsa", { modulusLength, publicExponent });
console.log(modulusLength, publicExponent, "generated");
} catch (error) {
console.log(modulusLength, publicExponent, error.message);
}
}
crypto.generateKeyPair("rsa", { modulusLength: 515, publicExponent: 3 }, (error, publicKey) => {
console.log("async", error, publicKey.asymmetricKeyDetails.modulusLength);
});
+104
View File
@@ -0,0 +1,104 @@
// RSA-PSS keys (id-RSASSA-PSS): generation with and without
// RSASSA-PSS-params, their details, SubjectPublicKeyInfo and PKCS#8 in
// OpenSSL's encoding, import of both, and the restrictions on signatures:
// PSS padding only, the key's digest and MGF1 digest, and at least its
// salt length. The deprecated hash and mgf1Hash options run in a child
// process, whose DEP0154 reports are printed with process ids normalized.
"use strict";
const { spawnSync } = require("child_process");
const c = require("crypto");
const fmt = (v) => JSON.stringify(v, (k, x) => (typeof x === "bigint" ? `${x}n` : x));
const tryIt = (label, fn) => {
try {
const r = fn();
console.log(label, typeof r === "string" ? r.split("\n")[0] : Buffer.isBuffer(r) ? `buffer ${r.length > 0}` : fmt(r));
} catch (e) {
console.log(label, e.name, e.code, e.message);
}
};
const algOf = (spki) => {
// The AlgorithmIdentifier: the SEQUENCE after the outer header.
const start = spki[1] & 0x80 ? 2 + (spki[1] & 0x7f) : 2;
const len = spki[start + 1];
return spki.subarray(start, start + 2 + len).toString("hex");
};
const report = (label, options) => tryIt(label, () => c.generateKeyPairSync("rsa-pss", { modulusLength: 512, ...options }).publicKey.asymmetricKeyDetails);
function main() {
const cases = {
none: {},
sha256: { hashAlgorithm: "sha256" },
full: { hashAlgorithm: "sha256", mgf1HashAlgorithm: "sha384", saltLength: 16 },
sha1: { hashAlgorithm: "sha1" },
salt: { saltLength: 10 },
s512: { hashAlgorithm: "sha512", saltLength: 0 },
mgf: { mgf1HashAlgorithm: "sha256" },
};
for (const [name, options] of Object.entries(cases)) {
const { publicKey, privateKey } = c.generateKeyPairSync("rsa-pss", { modulusLength: ["full", "sha1", "salt", "mgf"].includes(name) ? 512 : 1024, ...options });
const spki = publicKey.export({ format: "der", type: "spki" });
const pkcs8 = privateKey.export({ format: "der", type: "pkcs8" });
console.log(name, publicKey.asymmetricKeyType, fmt(publicKey.asymmetricKeyDetails), fmt(privateKey.asymmetricKeyDetails));
console.log(name, "spki alg", algOf(spki), "pkcs8 alg", pkcs8.indexOf(Buffer.from(algOf(spki), "hex")) > 0);
tryIt(`${name} pkcs1`, () => privateKey.export({ format: "pem", type: "pkcs1" }));
tryIt(`${name} jwk`, () => publicKey.export({ format: "jwk" }));
tryIt(`${name} pem`, () => privateKey.export({ format: "pem", type: "pkcs8" }));
const reimported = c.createPublicKey(publicKey.export({ format: "pem", type: "spki" }));
const fromPrivate = c.createPublicKey(privateKey.export({ format: "pem", type: "pkcs8" }));
const privateAgain = c.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" });
console.log(name, reimported.equals(publicKey), fromPrivate.equals(publicKey), privateAgain.asymmetricKeyType);
const data = Buffer.from("rsa-pss");
const signature = c.sign(null, data, privateAgain);
console.log(name, c.verify(null, data, publicKey, signature), c.verify(null, data, reimported, signature));
}
const data = Buffer.from("restricted");
const { publicKey, privateKey } = c.generateKeyPairSync("rsa-pss", {
modulusLength: 512,
hashAlgorithm: "sha256",
saltLength: 16,
});
tryIt("sign sha1", () => c.sign("sha1", data, privateKey));
tryIt("sign pkcs1", () => c.sign("sha256", data, { key: privateKey, padding: c.constants.RSA_PKCS1_PADDING }));
tryIt("sign salt 8", () => c.sign("sha256", data, { key: privateKey, saltLength: 8 }));
tryIt("verify salt 8", () => c.verify("sha256", data, { key: publicKey, saltLength: 8 }, Buffer.alloc(64)));
for (const saltLength of [16, 20, 30]) {
const signature = c.sign("sha256", data, { key: privateKey, saltLength });
console.log(
saltLength,
c.verify("sha256", data, publicKey, signature),
c.verify("sha256", data, { key: publicKey, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature),
);
}
const signer = c.createSign("sha256").update(data);
console.log("createVerify", c.createVerify("sha256").update(data).verify(publicKey, signer.sign(privateKey)));
const free = c.generateKeyPairSync("rsa-pss", { modulusLength: 1024 });
const freeSignature = c.sign("sha512", data, free.privateKey);
console.log("free sha512", c.verify("sha512", data, free.publicKey, freeSignature));
report("bad digest", { hashAlgorithm: "nope" });
report("bad mgf1", { mgf1HashAlgorithm: "nope" });
report("negative salt", { saltLength: -1 });
report("upper", { hashAlgorithm: "SHA256" });
report("alias", { hashAlgorithm: "RSA-SHA256" });
}
if (process.argv[2] === "deprecated") {
report("hash", { hash: "sha256" });
report("conflict", { hashAlgorithm: "sha256", hash: "sha1" });
report("mgf1Hash", { hashAlgorithm: "sha256", mgf1Hash: "sha384" });
report("hash type", { hash: 5 });
// Node drops deprecations while noDeprecation is set when it reports
// them, so the flag is set once the reports above are out.
setTimeout(() => {
process.noDeprecation = true;
report("quiet", { hash: "sha512" });
console.log("end of turn");
});
} else {
main();
const child = spawnSync(process.execPath, [process.argv[1], "deprecated"], { encoding: "utf8" });
console.log(`status: ${child.status}`);
console.log(child.stdout);
console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:<pid>)"));
}
@@ -0,0 +1,114 @@
// Encrypted private keys: PKCS#8 as PBES2 and PKCS#1 and SEC1 as
// traditional PEM with the CBC and ECB ciphers, import with a passphrase,
// generateKeyPair with a cipher, and OpenSSL's errors for missing and
// wrong passphrases, unknown ciphers, and ciphers a form refuses.
"use strict";
const c = require("crypto");
const show = (e) =>
`${e.name} ${e.code} ${e.message}${e.library ? ` [${e.library}: ${e.reason}]` : ""}${
e.opensslErrorStack ? ` ${JSON.stringify(e.opensslErrorStack)}` : ""
}`;
const tryIt = (label, fn) => {
try {
console.log(label, fn());
} catch (e) {
console.log(label, show(e));
}
};
// The PEM label and headers, without the random IV.
const head = (pem) =>
pem
.split("\n")
.filter((l) => l.startsWith("-----BEGIN") || l.includes(":"))
.map((l) => l.replace(/,[0-9A-F]+$/, ""))
.join(" | ");
const ec = c.generateKeyPairSync("ec", { namedCurve: "P-256" });
const rsa = c.generateKeyPairSync("rsa", { modulusLength: 1024 });
const same = (key, pair) => c.createPublicKey(key).equals(pair.publicKey);
for (const cipher of ["aes-128-cbc", "aes-192-cbc", "aes-256-cbc", "aes-128-ecb", "aes-256-ecb", "des-ede3-cbc", "aes256", "AES-128-CBC", "2.16.840.1.101.3.4.1.42"]) {
for (const format of ["pem", "der"]) {
tryIt(`pkcs8 ${format} ${cipher}`, () => {
const out = ec.privateKey.export({ type: "pkcs8", format, cipher, passphrase: "top secret" });
const key = c.createPrivateKey({ key: out, format, type: format === "der" ? "pkcs8" : undefined, passphrase: "top secret" });
return `${format === "pem" ? head(out) : Buffer.isBuffer(out)} ${key.asymmetricKeyType} ${same(key, ec)}`;
});
}
}
for (const cipher of ["aes-128-cbc", "aes-256-cbc", "des-ede3-cbc", "des3"]) {
tryIt(`sec1 ${cipher}`, () => {
const out = ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: Buffer.from("pw") });
return `${head(out)} ${same(c.createPrivateKey({ key: out, passphrase: "pw" }), ec)}`;
});
tryIt(`pkcs1 ${cipher}`, () => {
const out = rsa.privateKey.export({ type: "pkcs1", format: "pem", cipher, passphrase: "pw" });
return `${head(out)} ${same(c.createPrivateKey({ key: out, passphrase: Buffer.from("pw") }), rsa)}`;
});
}
tryIt("empty passphrase", () => {
const out = rsa.privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-128-cbc", passphrase: "" });
return same(c.createPrivateKey({ key: out, passphrase: "" }), rsa);
});
// Reading encrypted keys.
const p8pem = ec.privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase: "pw" });
const p8der = ec.privateKey.export({ type: "pkcs8", format: "der", cipher: "aes-256-cbc", passphrase: "pw" });
const sec1 = String(ec.privateKey.export({ type: "sec1", format: "pem", cipher: "aes-128-cbc", passphrase: "pw" }));
tryIt("public from encrypted", () => c.createPublicKey({ key: p8pem, passphrase: "pw" }).equals(ec.publicKey));
tryIt("public from traditional", () => c.createPublicKey({ key: sec1, passphrase: "pw" }).type);
tryIt("pkcs8 pem no passphrase", () => c.createPrivateKey(p8pem));
tryIt("pkcs8 der no passphrase", () => c.createPrivateKey({ key: p8der, format: "der", type: "pkcs8" }));
tryIt("sec1 no passphrase", () => c.createPrivateKey(sec1));
tryIt("public no passphrase", () => c.createPublicKey(p8pem));
tryIt("pkcs8 pem wrong", () => c.createPrivateKey({ key: p8pem, passphrase: "nope" }));
tryIt("pkcs8 der wrong", () => c.createPrivateKey({ key: p8der, format: "der", type: "pkcs8", passphrase: "nope" }));
tryIt("sec1 wrong", () => c.createPrivateKey({ key: sec1, passphrase: "nope" }));
tryIt("passphrase type", () => c.createPrivateKey({ key: p8pem, passphrase: 5 }));
tryIt("dek unknown", () => c.createPrivateKey({ key: sec1.replace(/DEK-Info: AES-128-CBC/, "DEK-Info: NOPE-CBC"), passphrase: "pw" }));
tryIt("dek legacy", () =>
c.createPrivateKey({ key: sec1.replace(/DEK-Info: AES-128-CBC,([0-9A-F]{16})[0-9A-F]+/, "DEK-Info: DES-CBC,$1"), passphrase: "pw" }),
);
tryIt("dek legacy no passphrase", () =>
c.createPrivateKey(sec1.replace(/DEK-Info: AES-128-CBC,([0-9A-F]{16})[0-9A-F]+/, "DEK-Info: DES-CBC,$1")),
);
// Export options.
const opts = (extra) => ({ type: "pkcs8", format: "pem", ...extra });
tryIt("unknown cipher", () => ec.privateKey.export(opts({ cipher: "nope", passphrase: "x" })));
tryIt("cipher without passphrase", () => ec.privateKey.export(opts({ cipher: "aes-128-cbc" })));
tryIt("passphrase without cipher", () => ec.privateKey.export(opts({ passphrase: "x" })));
tryIt("cipher type", () => ec.privateKey.export(opts({ cipher: 5, passphrase: "x" })));
tryIt("sec1 der", () => ec.privateKey.export({ type: "sec1", format: "der", cipher: "aes-128-cbc", passphrase: "x" }));
tryIt("pkcs1 der", () => rsa.privateKey.export({ type: "pkcs1", format: "der", cipher: "aes-128-cbc", passphrase: "x" }));
// OpenSSL refuses these ciphers for PKCS#8, and the last ones for
// traditional PEM too (the P-256 key's PKCS#8 and SEC1 lengths are not
// multiples of 8, which key wrap needs).
for (const cipher of ["aes-128-gcm", "aes-256-ccm", "aes-128-ctr", "chacha20-poly1305", "aes-128-xts", "des-ede3-cfb", "2.16.840.1.101.3.4.1.6"])
tryIt(`refused pkcs8 ${cipher}`, () => ec.privateKey.export(opts({ cipher, passphrase: "x" })).length > 0);
for (const cipher of ["aes-128-wrap", "id-aes256-wrap", "des-cbc", "rc4"]) {
tryIt(`refused pkcs8 ${cipher}`, () => ec.privateKey.export(opts({ cipher, passphrase: "x" })).length > 0);
tryIt(`refused sec1 ${cipher}`, () => ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: "x" }));
}
for (const cipher of ["aes-128-ecb", "aes-256-ecb", "des-ede3"])
tryIt(`refused sec1 ${cipher}`, () => ec.privateKey.export({ type: "sec1", format: "pem", cipher, passphrase: "x" }));
// generateKeyPair with a cipher.
const encoding = (cipher, type = "pkcs8") => ({
namedCurve: "P-256",
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type, format: "pem", cipher, passphrase: "gen" },
});
tryIt("keygen sync", () => {
const { publicKey, privateKey } = c.generateKeyPairSync("ec", encoding("aes-256-cbc"));
return `${head(privateKey)} ${c.createPublicKey({ key: privateKey, passphrase: "gen" }).equals(c.createPublicKey(publicKey))}`;
});
tryIt("keygen unknown", () => c.generateKeyPairSync("ec", encoding("nope")));
tryIt("keygen gcm", () => c.generateKeyPairSync("ec", encoding("aes-128-gcm")));
tryIt("keygen sec1 ecb", () => c.generateKeyPairSync("ec", encoding("aes-128-ecb", "sec1")));
// One job at a time: Node's jobs finish in either order.
c.generateKeyPair("ec", encoding("des-ede3-cbc", "sec1"), (err, publicKey, privateKey) => {
console.log("keygen async", err, head(privateKey));
console.log("keygen async key", c.createPublicKey({ key: privateKey, passphrase: "gen" }).equals(c.createPublicKey(publicKey)));
c.generateKeyPair("ec", encoding("aes-128-ccm"), (err) => console.log("keygen async ccm", show(err)));
});
@@ -0,0 +1,59 @@
// Verifying with RSA_PSS_SALTLEN_AUTO against an RSA-PSS key whose
// parameters set a minimum salt length recovers the salt length from the
// signature and does not enforce that minimum, as in Node: a valid
// signature with a shorter salt verifies. The default salt length (the
// key's minimum) accepts only that length. The keys use
// an MGF1 digest equal to the message digest and a different one. The
// stricter public key is the generated one with its minimum salt length
// raised from 16 to 32 in its DER.
const c = require("node:crypto");
const data = Buffer.from("restricted salt");
function raiseMinimumSalt(publicKey) {
const der = publicKey.export({ format: "der", type: "spki" });
const from = Buffer.from([0xa2, 0x03, 0x02, 0x01, 0x10]);
const at = der.indexOf(from);
if (at < 0) throw new Error("no saltLength field");
der[at + 4] = 0x20;
return c.createPublicKey({ key: der, format: "der", type: "spki" });
}
function attempt(label, fn) {
try {
console.log(label, fn());
} catch (e) {
console.log(label, "throws", e.code, e.message);
}
}
for (const mgf1HashAlgorithm of ["sha256", "sha384"]) {
const { privateKey, publicKey } = c.generateKeyPairSync("rsa-pss", {
modulusLength: 1024,
hashAlgorithm: "sha256",
mgf1HashAlgorithm,
saltLength: 16,
});
const strict = raiseMinimumSalt(publicKey);
const { mgf1HashAlgorithm: mgf1, saltLength: minimum } = strict.asymmetricKeyDetails;
console.log(mgf1HashAlgorithm, "strict key", mgf1, minimum);
for (const saltLength of [16, 20, 32, 40]) {
const signature = c.sign("sha256", data, { key: privateKey, saltLength });
const label = `${mgf1HashAlgorithm} salt ${saltLength}`;
attempt(`${label} auto, key minimum 16:`, () =>
c.verify("sha256", data, { key: publicKey, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature),
);
attempt(`${label} auto, key minimum 32:`, () =>
c.verify("sha256", data, { key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature),
);
attempt(`${label} default, key minimum 32:`, () => c.verify("sha256", data, strict, signature));
attempt(`${label} digest length, key minimum 32:`, () =>
c.verify("sha256", data, { key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_DIGEST }, signature),
);
const v = c.createVerify("sha256");
v.update(data);
attempt(`${label} createVerify auto, key minimum 32:`, () =>
v.verify({ key: strict, saltLength: c.constants.RSA_PSS_SALTLEN_AUTO }, signature),
);
}
}
@@ -0,0 +1,28 @@
// A string passphrase is decoded with the key's encoding, as in Node:
// createPrivateKey's { encoding } and the encoding option of an encrypted
// export both apply to it.
const c = require("node:crypto");
const { privateKey } = c.generateKeyPairSync("ec", { namedCurve: "P-256" });
const pass = "pässwörd";
const hex = Buffer.from(pass).toString("hex");
const pem = privateKey.export({ format: "pem", type: "pkcs8", cipher: "aes-256-cbc", passphrase: hex, encoding: "hex" });
const der = privateKey.export({ format: "der", type: "pkcs8", cipher: "aes-128-cbc", passphrase: pass });
function attempt(label, options) {
try {
const key = c.createPrivateKey(options);
console.log(label, key.asymmetricKeyType, key.equals(privateKey));
} catch (e) {
console.log(label, "throws", e.code);
}
}
attempt("utf8 passphrase", { key: pem, passphrase: pass });
attempt("hex passphrase, encoding hex", { key: Buffer.from(pem), passphrase: hex, encoding: "hex" });
attempt("base64 passphrase, encoding base64", { key: Buffer.from(pem), passphrase: Buffer.from(pass).toString("base64"), encoding: "base64" });
attempt("latin1 passphrase, encoding latin1", { key: Buffer.from(pem), passphrase: Buffer.from(pass).toString("latin1"), encoding: "latin1" });
attempt("hex passphrase, no encoding", { key: pem, passphrase: hex });
attempt("Buffer passphrase, encoding hex", { key: Buffer.from(pem), passphrase: Buffer.from(pass), encoding: "hex" });
attempt("der, hex passphrase, encoding hex", { key: der, format: "der", type: "pkcs8", passphrase: hex, encoding: "hex" });
attempt("hex key string, encoding hex", { key: Buffer.from(pem).toString("hex"), passphrase: hex, encoding: "hex" });
@@ -0,0 +1 @@
{ "plain": 11640, "san": 77506 }
@@ -0,0 +1 @@
{ "plain": 11459, "san": 74658 }
@@ -0,0 +1 @@
{ "plain": 3144, "san": 8504 }
@@ -0,0 +1 @@
{ "plain": 5740, "san": 60808 }
@@ -0,0 +1 @@
{ "plain": 3665, "san": 67315 }
+32
View File
@@ -870,6 +870,38 @@ console.log(\`\${register(7)}\`);
});
});
// An RSA-PSS key whose parameters restrict it to a digest the runtime does
// not sign with (sha512-224 and sha512-256, which OpenSSL reads) throws a
// coded SC2020 error naming the digest, instead of importing the key
// without the restrictions it carries.
describe("RSA-PSS keys restricted to unimplemented digests", () => {
test("importing one throws SC2020 naming the digest", async () => {
const r = await compileAndRun(
"pss-restricted-digest",
`import { createPublicKey } from "node:crypto";
const keys = [
"-----BEGIN PUBLIC KEY-----\\nMIGQMEEGCSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAIFBQChHDAaBgkqhkiG9w0B\\nAQgwDQYJYIZIAWUDBAIFBQCiAwIBCANLADBIAkEArMDQESqjeWDLqFa1E5SzsMl4\\n6tyUsEqcDKSwRUvPilv1AUudf/w6qaXlPnAxBmFjZYihdQSZlk0czRLOX3o7gwID\\nAQAB\\n-----END PUBLIC KEY-----\\n",
"-----BEGIN PUBLIC KEY-----\\nMIGQMEEGCSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAIBBQChHDAaBgkqhkiG9w0B\\nAQgwDQYJYIZIAWUDBAIGBQCiAwIBCANLADBIAkEAuOa9NkzXzMDelHCIEkf0mnKe\\nnQdalNUm9vZtIMwpDJcn5U0Vbhhd6MTfRPhSo5nW71Oh5eU7v5M3PMYOHS7cMQID\\nAQAB\\n-----END PUBLIC KEY-----\\n",
];
for (const key of keys) {
try {
console.log(createPublicKey(key).asymmetricKeyType);
} catch (error) {
const e = error as Error & { code?: string };
console.log(e.name, e.code, e.message);
}
}
`,
);
expect(r.exitCode).toBe(0);
expect(r.stdout).toBe(
"Error SC2020 'RSA-PSS keys restricted to sha512-224' is part of the standard library types but has no scriptc lowering yet\n" +
"Error SC2020 'RSA-PSS keys restricted to sha512-256' is part of the standard library types but has no scriptc lowering yet\n",
);
expect(r.stderr).toBe("");
});
});
// A cipher name Node lists in crypto.getCiphers() but the runtime does not
// implement throws a coded SC2020 error naming the cipher when the name is
// computed; literal names are refused at compile time (node-types-fenced).