Commit 5498ee6eb57 for nodejs
commit 5498ee6eb57bf3576972b95a43522e08fde23708
Author: Filip Skokan <panva.ip@gmail.com>
Date: Sun Aug 30 17:05:51 2026 +0200
crypto: add X.509 generation with CSR support
Expose callback-based generateX509Certificate() and synchronous
generateX509CertificateSync(), producing X509Certificate objects for
self-signed and issuer-signed X.509 v3 certificates. Accept grouped
distinguished names or exact subject DER, public keys, serial numbers,
validity dates, and an explicit signing digest. Reuse crypto.sign() key
inputs and RSA options, including OpenSSL STORE-backed keys.
A PKCS#10 request can supply the subject and public key instead.
Require a synchronous selectExtensions callback to accept, replace, or
amend the requested extensions. Verify the request signature before
signing the certificate, in the selected sync or async mode.
Provide typed Basic Constraints, Key Usage, Extended Key Usage, SAN,
SKI, and AKI, plus arbitrary raw extensions. Add none implicitly and
leave raw extension semantics to the caller. Copy mutable inputs
before queueing asynchronous work. Certificate trust and CA
authorization remain the caller's responsibility.
Fixes: https://github.com/nodejs/node/issues/51169
Assisted-by: Codex
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/67245
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 15a1bd2de9d..ba800fa816c 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -1742,16 +1742,13 @@ std::optional<X509NameInspection> InspectName(const X509_NAME* name) {
return output;
}
+template <typename GetExtension>
std::optional<std::vector<X509RawExtension>> InspectExtensions(
- const X509* certificate) {
- if (certificate == nullptr) return std::nullopt;
- const int count = X509_get_ext_count(certificate);
- if (count < 0) return std::nullopt;
-
+ OPENSSL_SIZE_T count, GetExtension get_extension) {
std::vector<X509RawExtension> output;
output.reserve(static_cast<size_t>(count));
- for (int i = 0; i < count; i++) {
- const X509_EXTENSION* extension = X509_get_ext(certificate, i);
+ for (OPENSSL_SIZE_T i = 0; i < count; i++) {
+ const X509_EXTENSION* extension = get_extension(i);
if (extension == nullptr) return std::nullopt;
auto* mutable_extension = const_cast<X509_EXTENSION*>(extension);
@@ -1770,6 +1767,15 @@ std::optional<std::vector<X509RawExtension>> InspectExtensions(
}
} // namespace
+std::optional<std::vector<X509RawExtension>> inspectX509Extensions(
+ const STACK_OF(X509_EXTENSION) * extensions) {
+ if (extensions == nullptr) return std::nullopt;
+ return InspectExtensions(sk_X509_EXTENSION_num(extensions),
+ [extensions](OPENSSL_SIZE_T index) {
+ return sk_X509_EXTENSION_value(extensions, index);
+ });
+}
+
std::optional<int64_t> X509View::getVersion() const {
MarkPopErrorOnReturn mark_pop_error_on_return;
if (cert_ == nullptr) return std::nullopt;
@@ -1793,7 +1799,13 @@ std::optional<X509NameInspection> X509View::inspectIssuerName() const {
std::optional<std::vector<X509RawExtension>> X509View::inspectExtensions()
const {
MarkPopErrorOnReturn mark_pop_error_on_return;
- return InspectExtensions(cert_);
+ if (cert_ == nullptr) return std::nullopt;
+ const int count = X509_get_ext_count(cert_);
+ if (count < 0) return std::nullopt;
+ return InspectExtensions(
+ static_cast<OPENSSL_SIZE_T>(count), [this](OPENSSL_SIZE_T index) {
+ return X509_get_ext(cert_, static_cast<int>(index));
+ });
}
bool X509View::isIssuedBy(const X509View& issuer) const {
@@ -1803,10 +1815,39 @@ bool X509View::isIssuedBy(const X509View& issuer) const {
const_cast<X509*>(cert_)) == X509_V_OK;
}
-bool X509View::checkPrivateKey(const EVPKeyPointer& pkey) const {
- ClearErrorOnReturn clearErrorOnReturn;
- if (cert_ == nullptr || pkey == nullptr) return false;
- return X509_check_private_key(const_cast<X509*>(cert_), pkey.get()) == 1;
+X509View::CheckKeyResult X509View::checkPrivateKey(
+ const EVPKeyPointer& pkey) const {
+ MarkPopErrorOnReturn mark_pop_error_on_return;
+ if (cert_ == nullptr || pkey == nullptr) {
+ return CheckKeyResult::OPERATION_FAILED;
+ }
+
+ EVPKeyPointer certificate_key(X509_get_pubkey(const_cast<X509*>(cert_)));
+ if (!certificate_key) return CheckKeyResult::OPERATION_FAILED;
+#if NCRYPTO_USE_BORINGSSL
+ if (EVP_PKEY_is_opaque(certificate_key.get()) ||
+ EVP_PKEY_is_opaque(pkey.get())) {
+ return CheckKeyResult::UNSUPPORTED_OPERATION;
+ }
+#endif
+#if NCRYPTO_USE_OPENSSL_PROVIDER
+ const int result = EVP_PKEY_eq(certificate_key.get(), pkey.get());
+#elif NCRYPTO_USE_BORINGSSL
+ const int result = EVP_PKEY_cmp(certificate_key.get(), pkey.get());
+#else
+#error "Unsupported crypto backend"
+#endif
+ switch (result) {
+ case 1:
+ return CheckKeyResult::MATCH;
+ case 0:
+ case -1:
+ return CheckKeyResult::NO_MATCH;
+ case -2:
+ return CheckKeyResult::UNSUPPORTED_OPERATION;
+ default:
+ return CheckKeyResult::OPERATION_FAILED;
+ }
}
bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const {
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index 7b972258ad7..97e0945eca0 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -70,6 +70,16 @@
#define NCRYPTO_HAS_ASN1_BIT_STRING_GET_LENGTH 0
#endif
+// BoringSSL's X.509 signer can encode only its restricted RSA-PSS parameter
+// profile. Other supported backends accept the full runtime signing profile.
+#if NCRYPTO_USE_BORINGSSL
+#define NCRYPTO_HAS_X509_RSA_PSS_FLEXIBLE_PARAMETERS 0
+#elif NCRYPTO_USE_OPENSSL_PROVIDER
+#define NCRYPTO_HAS_X509_RSA_PSS_FLEXIBLE_PARAMETERS 1
+#else
+#error "Unsupported crypto backend"
+#endif
+
#if NCRYPTO_USE_OPENSSL_PROVIDER
#include <openssl/core_names.h>
#include <openssl/encoder.h>
@@ -1683,8 +1693,18 @@ struct X509RawExtension final {
std::vector<unsigned char> value;
};
+std::optional<std::vector<X509RawExtension>> inspectX509Extensions(
+ const STACK_OF(X509_EXTENSION) * extensions);
+
class X509View final {
public:
+ enum class CheckKeyResult {
+ MATCH, // The requested comparison passed.
+ NO_MATCH, // It completed and produced an ordinary mismatch.
+ UNSUPPORTED_OPERATION, // The backend cannot compare these keys.
+ OPERATION_FAILED, // The backend failed for another reason.
+ };
+
static X509View From(const SSLPointer& ssl);
static X509View From(const SSLCtxPointer& ctx);
@@ -1727,7 +1747,7 @@ class X509View final {
bool isCA() const;
bool isIssuedBy(const X509View& other) const;
- bool checkPrivateKey(const EVPKeyPointer& pkey) const;
+ CheckKeyResult checkPrivateKey(const EVPKeyPointer& pkey) const;
bool checkPublicKey(const EVPKeyPointer& pkey) const;
std::optional<std::string> getFingerprint(const Digest& method) const;
diff --git a/doc/api/crypto.md b/doc/api/crypto.md
index b14c4876884..71061e7965b 100644
--- a/doc/api/crypto.md
+++ b/doc/api/crypto.md
@@ -5249,6 +5249,262 @@ minimal control over our ability to interrupt the generation process,
it is not recommended to generate overly large primes, as doing so may make
the process unresponsive.
+### `crypto.generateX509Certificate(options, callback)`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1.2 - Release candidate
+
+* `options` {Object}
+ * `csr` {string|ArrayBuffer|Buffer|TypedArray|DataView} A PEM string, or a
+ PEM- or DER-encoded PKCS#10 certificate signing request. It cannot be
+ combined with `subject` or `publicKey`.
+ * `subject`
+ {Array\<Object|Object\[]>|ArrayBuffer|Buffer|TypedArray|DataView}
+ A distinguished name in structured or DER form. Required unless `csr` is
+ supplied. See [Distinguished name inputs][].
+ * `publicKey` {KeyObject|string|ArrayBuffer|Buffer|TypedArray|DataView|Object}
+ The public key input accepted by [`crypto.createPublicKey()`][]. Required
+ unless `csr` is supplied.
+ * `issuer` {X509Certificate} The issuer certificate. When omitted, the
+ certificate is self-signed.
+ * `serialNumber` {bigint} A positive serial number from `1n` through
+ `(1n << 159n) - 1n`.
+ * `notBefore` {Date} The start of the certificate's validity period.
+ * `notAfter` {Date} The end of the certificate's validity period.
+ * `signature` {Object} The signing configuration.
+ * `key` {KeyObject|string|URL|ArrayBuffer|Buffer|TypedArray|DataView|Object}
+ A private key input accepted by [`crypto.sign()`][]. Its `padding` and
+ `saltLength` RSA options apply to certificate signing; signature encoding
+ and context options do not apply.
+ * `digest` {string|null} An explicit digest name. `null` is accepted
+ only by signature algorithms with an intrinsic digest, such as Ed25519.
+ * `extensions` {Object\[]} An ordered list of extensions. It cannot be used
+ with `csr`. **Default:** `[]`. See [Extension inputs][].
+ * `selectExtensions` {Function} A synchronous policy callback that receives
+ the requested extensions and returns the extensions to include. It is
+ required when `csr` is supplied and cannot otherwise be used.
+ * `requestedExtensions` {Object\[]}
+ * Returns: {Object\[]}
+* `callback` {Function}
+ * `err` {Error}
+ * `certificate` {X509Certificate}
+
+Generates and signs an X.509 v3 certificate. Validation errors are thrown
+synchronously, and mutable input data is copied before asynchronous work.
+Certificate request signature verification and signing errors are passed to
+`callback`. The function is compatible with [`util.promisify()`][]. Supported
+key and digest combinations depend on the configured crypto backend.
+
+Dates are accepted from 1950 through 9999 and truncated to whole seconds.
+After truncation, `notAfter` must be later than `notBefore`. No extensions are
+added implicitly.
+
+For a self-signed certificate, the subject must not be empty and
+`signature.key` must match the certificate public key. With an `issuer`, the
+issuer subject must not be empty and its public key must match `signature.key`.
+If the backend cannot compare the keys, the caller must ensure they match.
+The returned certificate is standalone; the supplied issuer and its certificate
+chain are not attached to the result through `issuerCertificate`.
+
+This function does not validate a certification path or determine whether the
+certificate is trusted.
+
+When `csr` is supplied, the request supplies the certificate subject and public
+key. The configured crypto backend parses the request, after which Node.js
+invokes `options.selectExtensions` synchronously before verifying the request
+signature. The callback therefore receives unauthenticated request data and
+must apply the issuer's policy. A thrown exception propagates synchronously,
+and returning a `Promise` is not supported. The request signature is verified
+before the certificate is signed, so an invalid proof of possession cannot
+produce a certificate.
+
+Requested extensions are never copied implicitly. The policy callback receives
+fresh raw extension objects in request order. Each object has `type: 'raw'`, a
+canonical dotted `oid` {string}, `critical` {boolean}, and `value` {Buffer}
+containing the `extnValue` bytes. It may return those objects, filter them, or
+replace them with any forms described in [Extension inputs][]. The returned
+list is validated in the same way as `options.extensions`. For an
+empty-subject request, the returned list must contain a typed,
+critical, nonempty Subject Alternative Name. Echoing a raw Subject Alternative
+Name does not satisfy this certificate construction check.
+
+```mjs
+import {
+ generateX509Certificate,
+ X509Certificate,
+} from 'node:crypto';
+import { readFileSync } from 'node:fs';
+
+const allowedExtensionOids = new Set([
+ '2.5.29.15', // Key Usage
+ '2.5.29.17', // Subject Alternative Name
+]);
+
+generateX509Certificate({
+ csr: readFileSync('request.csr'),
+ issuer: new X509Certificate(readFileSync('issuer-cert.pem')),
+ serialNumber: 1n,
+ notBefore: new Date('2026-01-01T00:00:00Z'),
+ notAfter: new Date('2027-01-01T00:00:00Z'),
+ signature: {
+ key: readFileSync('issuer-key.pem'),
+ digest: 'sha256',
+ },
+ selectExtensions(requestedExtensions) {
+ return requestedExtensions.filter(({ oid }) =>
+ allowedExtensionOids.has(oid));
+ },
+}, (err, certificate) => {
+ if (err) throw err;
+ console.log(certificate.toString());
+});
+```
+
+#### Distinguished name inputs
+
+In its structured form, `subject` {Array\<Object|Object\[]>} is an ordered array
+whose entries are attribute objects or nonempty arrays of attribute objects.
+Each entry in the outer array forms one relative distinguished name (RDN). An
+attribute has `name` {string} and `value` {string}. `name` is a canonical dotted
+OID or one of `commonName`/`CN`,
+`countryName`/`C`, `localityName`/`L`, `stateOrProvinceName`/`ST`,
+`organizationName`/`O`, `organizationalUnitName`/`OU`, `serialNumber`,
+`streetAddress`, `postalCode`, `domainComponent`/`DC`, `userId`/`UID`,
+`emailAddress`/`E`, `title`, `surname`/`SN`, or `givenName`/`GN`.
+
+```js
+const subject = [
+ { name: 'countryName', value: 'US' },
+ { name: 'organizationName', value: 'Example Inc.' },
+ [
+ { name: 'commonName', value: 'example.com' },
+ { name: 'serialNumber', value: '1234' },
+ ],
+];
+```
+
+The final nested array places `commonName` and `serialNumber` in the same RDN.
+
+RDN sequence and grouping are preserved. DER canonical ordering applies inside
+a multi-valued RDN. The crypto backend selects and validates each attribute's
+ASN.1 string type according to its OID.
+
+Alternatively, `subject` may be an
+{ArrayBuffer|Buffer|TypedArray|DataView} containing the complete DER encoding
+of exactly one X.509 `Name`, including its outer `SEQUENCE` tag and length.
+The input is copied synchronously. If accepted by
+the configured crypto backend, the encoded name is preserved byte-for-byte,
+including its RDN grouping, attribute order, and ASN.1 string types. This form
+can reproduce an existing name exactly, for example by using
+`subject: certificate.subjectName.raw`. The `subjectName.rdns` view is not a
+generation input format and does not include ASN.1 value types. The caller is
+responsible for ensuring that the supplied encoding is canonical DER.
+
+#### Extension inputs
+
+`options.extensions` and the value returned by `options.selectExtensions` are
+ordered arrays of extension objects. Each extension object has one of the
+following forms. Except where noted, all forms accept an optional `critical`
+{boolean}, which defaults to `false`.
+
+```js
+const extensions = [
+ {
+ type: 'keyUsage',
+ critical: true,
+ usages: ['digitalSignature'],
+ },
+ {
+ type: 'subjectAltName',
+ names: [
+ { type: 'dns', value: 'example.com' },
+ { type: 'ip', value: '192.0.2.1' },
+ ],
+ },
+];
+```
+
+* Basic Constraints: `type: 'basicConstraints'`, `ca` {boolean}, and optional
+ `pathLength` {number}, which must be a non-negative integer.
+* Key Usage: `type: 'keyUsage'` and nonempty `usages` {string\[]}. A usage is
+ `digitalSignature`, `contentCommitment`, `keyEncipherment`,
+ `dataEncipherment`, `keyAgreement`, `keyCertSign`, `crlSign`, `encipherOnly`,
+ or `decipherOnly`.
+* Extended Key Usage: `type: 'extendedKeyUsage'` and nonempty `usages`
+ {string\[]}. A usage is a canonical dotted OID or `serverAuth`, `clientAuth`,
+ `codeSigning`, `emailProtection`, `timeStamping`, `ocspSigning`, or
+ `anyExtendedKeyUsage`.
+* Subject Alternative Name: `type: 'subjectAltName'` and nonempty `names`
+ {Object\[]}. Each name has `type` set to `'dns'`, `'ip'`, `'email'`, or
+ `'uri'`, and a string `value`. IP values must be IPv4 or IPv6 addresses and
+ must not contain an IPv6 zone identifier. The other values must be nonempty
+ ASCII strings without NUL and are not otherwise validated.
+* Subject Key Identifier: `type: 'subjectKeyIdentifier'` and `value: 'auto'` or
+ `value` {ArrayBuffer|TypedArray|Buffer|DataView}. A binary value contains the
+ key identifier bytes, not DER. `'auto'` uses [RFC 7093][] method 1: the
+ leftmost 160 bits of the SHA-256 digest of the `subjectPublicKey` BIT STRING
+ contents. This typed form is always noncritical.
+* Authority Key Identifier: `type: 'authorityKeyIdentifier'` and
+ `value: 'auto'` or `value`
+ {ArrayBuffer|TypedArray|Buffer|DataView}. A binary value contains the key
+ identifier bytes and populates only the extension's `keyIdentifier` field.
+ When `options.issuer` is supplied, `'auto'` asks the configured crypto backend
+ to decode and copy its Subject Key Identifier. If the extension is absent,
+ the identifier is instead derived from the issuer public key using
+ [RFC 7093][] method 1. The operation fails if the backend cannot decode a
+ single Subject Key Identifier. When `options.issuer` is omitted, `'auto'`
+ copies an explicit typed Subject Key Identifier in the same extension list,
+ or derives the identifier from the certificate public key. This typed form
+ is always noncritical.
+* Raw: `type: 'raw'`, `oid` {string}, and `value`
+ {ArrayBuffer|TypedArray|Buffer|DataView}. `value` is copied into `extnValue`.
+ Raw values are not parsed or semantically validated and may use an OID that
+ also has a typed form. The caller is responsible for their DER syntax and
+ semantics. The selected crypto backend may reject a raw encoding. If it
+ reports success without retaining the extension, generation throws
+ `ERR_CRYPTO_UNSUPPORTED_OPERATION` instead of returning an altered
+ certificate.
+
+Duplicate extension OIDs are rejected. Structural checks use only typed forms:
+a Key Usage containing `keyCertSign` requires CA Basic Constraints, and
+`pathLength` requires CA Basic Constraints and a Key Usage that includes
+`keyCertSign`. An empty subject is permitted only for an issuer-signed
+certificate and requires an explicitly critical, nonempty typed Subject
+Alternative Name extension.
+
+For an existing certificate `source`, the following fields can be included in
+generation options to copy its encoded subject and raw extension contents:
+
+```js
+const copiedFields = {
+ subject: source.subjectName.raw,
+ extensions: source.extensions.map((extension) => ({
+ type: 'raw',
+ ...extension,
+ })),
+};
+```
+
+This copies the name encoding and extension values, not the original
+certificate as a whole. Generation validation still applies, including the
+rejection of duplicate extension OIDs exposed by inspection.
+
+### `crypto.generateX509CertificateSync(options)`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1.2 - Release candidate
+
+* `options` {Object} See [`crypto.generateX509Certificate()`][].
+* Returns: {X509Certificate}
+
+Synchronous version of [`crypto.generateX509Certificate()`][].
+
### `crypto.getCipherInfo(nameOrNid[, options])`
<!-- YAML
@@ -7734,6 +7990,8 @@ See the [list of SSL OP Flags][] for details.
[CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532
[Caveats]: #support-for-weak-or-compromised-algorithms
[Crypto constants]: #crypto-constants
+[Distinguished name inputs]: #distinguished-name-inputs
+[Extension inputs]: #extension-inputs
[FIPS mode]: #fips-mode
[FIPS module configuration file]: https://docs.openssl.org/3.0/man5/fips_config/
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed
@@ -7755,6 +8013,7 @@ See the [list of SSL OP Flags][] for details.
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
[RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt
[RFC 5280]: https://www.rfc-editor.org/rfc/rfc5280.txt
+[RFC 7093]: https://www.rfc-editor.org/rfc/rfc7093.txt
[RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt
[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt
[RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt
@@ -7793,6 +8052,7 @@ See the [list of SSL OP Flags][] for details.
[`crypto.createVerify()`]: #cryptocreateverifyalgorithm-options
[`crypto.generateKey()`]: #cryptogeneratekeytype-options-callback
[`crypto.generateKeyPair()`]: #cryptogeneratekeypairtype-options-callback
+[`crypto.generateX509Certificate()`]: #cryptogeneratex509certificateoptions-callback
[`crypto.getCiphers()`]: #cryptogetciphers
[`crypto.getCurves()`]: #cryptogetcurves
[`crypto.getDiffieHellman()`]: #cryptogetdiffiehellmangroupname
diff --git a/lib/crypto.js b/lib/crypto.js
index 6e6c3d0552f..401343650e5 100644
--- a/lib/crypto.js
+++ b/lib/crypto.js
@@ -132,6 +132,20 @@ const {
decapsulate,
} = require('internal/crypto/kem');
+let x509Generation;
+function lazyX509Generation() {
+ x509Generation ??= require('internal/crypto/x509_generation');
+ return x509Generation;
+}
+
+function generateX509Certificate(options, callback) {
+ return lazyX509Generation().generateX509Certificate(options, callback);
+}
+
+function generateX509CertificateSync(options) {
+ return lazyX509Generation().generateX509CertificateSync(options);
+}
+
let webcrypto;
function lazyWebCrypto() {
webcrypto ??= require('internal/crypto/webcrypto');
@@ -223,6 +237,8 @@ module.exports = {
generateKeyPairSync,
generateKey,
generateKeySync,
+ generateX509Certificate,
+ generateX509CertificateSync,
privateDecrypt,
privateEncrypt,
publicDecrypt,
diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js
index 3830ecc0b12..b94755ac7d8 100644
--- a/lib/internal/crypto/sig.js
+++ b/lib/internal/crypto/sig.js
@@ -81,12 +81,12 @@ Sign.prototype.update = function update(data, encoding) {
return this;
};
-function getPadding(options) {
- return getIntOption('padding', options);
+function getPadding(options, name = 'options') {
+ return getIntOption('padding', options, name);
}
-function getSaltLength(options) {
- let saltLength = getIntOption('saltLength', options);
+function getSaltLength(options, name = 'options') {
+ let saltLength = getIntOption('saltLength', options, name);
if (options.padding === constants.RSA_PKCS1_PSS_PADDING && saltLength === undefined) {
saltLength = constants.RSA_PSS_SALTLEN_MAX_SIGN;
}
@@ -119,13 +119,13 @@ function getContext(options) {
return options.context;
}
-function getIntOption(name, options) {
- const value = options[name];
+function getIntOption(option, options, name) {
+ const value = options[option];
if (value !== undefined) {
if (value === value >> 0) {
return value;
}
- throw new ERR_INVALID_ARG_VALUE(`options.${name}`, value);
+ throw new ERR_INVALID_ARG_VALUE(`${name}.${option}`, value);
}
return undefined;
}
@@ -321,4 +321,6 @@ module.exports = {
signOneShot,
Verify,
verifyOneShot,
+ getPadding,
+ getSaltLength,
};
diff --git a/lib/internal/crypto/x509_generation.js b/lib/internal/crypto/x509_generation.js
new file mode 100644
index 00000000000..dd890f4870a
--- /dev/null
+++ b/lib/internal/crypto/x509_generation.js
@@ -0,0 +1,613 @@
+'use strict';
+
+const {
+ ArrayIsArray,
+ ArrayPrototypePush,
+ Date,
+ DatePrototypeGetTime,
+ DatePrototypeToISOString,
+ FunctionPrototypeCall,
+ MathFloor,
+ Number,
+ NumberIsFinite,
+ NumberParseInt,
+ ObjectEntries,
+ ObjectPrototypeHasOwnProperty,
+ RegExpPrototypeSymbolReplace,
+ SafeSet,
+ StringPrototypeCharCodeAt,
+ StringPrototypeIncludes,
+ StringPrototypeIsWellFormed,
+ StringPrototypeSlice,
+ Uint8Array,
+} = primordials;
+
+const { Buffer } = require('buffer');
+
+const {
+ parseX509CertificateRequest,
+ X509GenerateJob,
+ kCryptoJobAsync,
+ kCryptoJobSync,
+} = internalBinding('crypto');
+
+const {
+ preparePrivateKey,
+ preparePublicOrPrivateKey,
+} = require('internal/crypto/keys');
+const {
+ getPadding,
+ getSaltLength,
+} = require('internal/crypto/sig');
+
+const {
+ X509Certificate,
+ isX509Certificate,
+} = require('internal/crypto/x509');
+
+const {
+ isAnyArrayBuffer,
+ isArrayBufferView,
+} = require('internal/util/types');
+const { isIP } = require('internal/net');
+
+const {
+ validateArray,
+ validateBoolean,
+ validateFunction,
+ validateInteger,
+ validateObject,
+ validateString,
+} = require('internal/validators');
+
+const {
+ codes: {
+ ERR_INCOMPATIBLE_OPTION_PAIR,
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ ERR_MISSING_OPTION,
+ ERR_OUT_OF_RANGE,
+ },
+} = require('internal/errors');
+
+const kGeneralNameDns = 0;
+const kGeneralNameIp = 1;
+const kGeneralNameRfc822 = 2;
+const kGeneralNameUri = 4;
+
+const kExtensionRaw = 0;
+const kExtensionBasicConstraints = 1;
+const kExtensionKeyUsage = 2;
+const kExtensionExtendedKeyUsage = 3;
+const kExtensionSubjectAltName = 4;
+const kExtensionSubjectKeyIdentifier = 5;
+const kExtensionAuthorityKeyIdentifier = 6;
+
+const kNameAttributeNames = {
+ '__proto__': null,
+ '2.5.4.3': ['commonName', 'CN'],
+ '2.5.4.6': ['countryName', 'C'],
+ '2.5.4.7': ['localityName', 'L'],
+ '2.5.4.8': ['stateOrProvinceName', 'ST'],
+ '2.5.4.10': ['organizationName', 'O'],
+ '2.5.4.11': ['organizationalUnitName', 'OU'],
+ '2.5.4.5': 'serialNumber',
+ '2.5.4.9': 'streetAddress',
+ '2.5.4.17': 'postalCode',
+ '0.9.2342.19200300.100.1.25': ['domainComponent', 'DC'],
+ '0.9.2342.19200300.100.1.1': ['userId', 'UID'],
+ '1.2.840.113549.1.9.1': ['emailAddress', 'E'],
+ '2.5.4.12': 'title',
+ '2.5.4.4': ['surname', 'SN'],
+ '2.5.4.42': ['givenName', 'GN'],
+};
+const kNameAttributes = { '__proto__': null };
+const nameAttributeEntries = ObjectEntries(kNameAttributeNames);
+for (let i = 0; i < nameAttributeEntries.length; i++) {
+ const { 0: oid, 1: names } = nameAttributeEntries[i];
+ if (ArrayIsArray(names)) {
+ for (let j = 0; j < names.length; j++)
+ kNameAttributes[names[j]] = oid;
+ } else {
+ kNameAttributes[names] = oid;
+ }
+}
+
+const kKeyUsageBits = {
+ '__proto__': null,
+ 'digitalSignature': 0,
+ 'contentCommitment': 1,
+ 'keyEncipherment': 2,
+ 'dataEncipherment': 3,
+ 'keyAgreement': 4,
+ 'keyCertSign': 5,
+ 'crlSign': 6,
+ 'encipherOnly': 7,
+ 'decipherOnly': 8,
+};
+
+const kExtendedKeyUsageOids = {
+ '__proto__': null,
+ 'serverAuth': '1.3.6.1.5.5.7.3.1',
+ 'clientAuth': '1.3.6.1.5.5.7.3.2',
+ 'codeSigning': '1.3.6.1.5.5.7.3.3',
+ 'emailProtection': '1.3.6.1.5.5.7.3.4',
+ 'timeStamping': '1.3.6.1.5.5.7.3.8',
+ 'ocspSigning': '1.3.6.1.5.5.7.3.9',
+ 'anyExtendedKeyUsage': '2.5.29.37.0',
+};
+
+function getRequired(object, property, name) {
+ const value = object[property];
+ if (value === undefined &&
+ !ObjectPrototypeHasOwnProperty(object, property)) {
+ throw new ERR_MISSING_OPTION(name);
+ }
+ return value;
+}
+
+function snapshotBinary(value, name, expectedTypes = [
+ 'ArrayBuffer', 'SharedArrayBuffer', 'Buffer', 'TypedArray', 'DataView',
+]) {
+ if (!isAnyArrayBuffer(value) && !isArrayBufferView(value)) {
+ throw new ERR_INVALID_ARG_TYPE(name, expectedTypes, value);
+ }
+ const view = isAnyArrayBuffer(value) ?
+ new Uint8Array(value) :
+ new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
+ return Buffer.from(view);
+}
+
+function snapshotCertificateRequest(value) {
+ if (typeof value === 'string') return Buffer.from(value);
+ return snapshotBinary(value, 'options.csr', [
+ 'string',
+ 'ArrayBuffer',
+ 'SharedArrayBuffer',
+ 'Buffer',
+ 'TypedArray',
+ 'DataView',
+ ]);
+}
+
+function validateUnicode(value, name) {
+ validateString(value, name);
+ if (!StringPrototypeIsWellFormed(value)) {
+ throw new ERR_INVALID_ARG_VALUE(
+ name, value, 'contains an unpaired surrogate');
+ }
+}
+
+function validateAsciiWithoutNul(value, name) {
+ validateString(value, name);
+ for (let i = 0; i < value.length; i++) {
+ const character = StringPrototypeCharCodeAt(value, i);
+ if (character === 0 || character > 0x7f) {
+ throw new ERR_INVALID_ARG_VALUE(
+ name, value, 'must contain only ASCII characters without NUL');
+ }
+ }
+}
+
+function validateGeneralNameText(value, name) {
+ validateAsciiWithoutNul(value, name);
+ if (value.length === 0) {
+ throw new ERR_INVALID_ARG_VALUE(name, value, 'must not be empty');
+ }
+}
+
+function normalizeNameAttribute(attribute, name) {
+ validateObject(attribute, name);
+ const attributeName = getRequired(attribute, 'name', `${name}.name`);
+ const value = getRequired(attribute, 'value', `${name}.value`);
+ validateString(attributeName, `${name}.name`);
+
+ let oid = attributeName;
+ if (ObjectPrototypeHasOwnProperty(kNameAttributes, attributeName)) {
+ oid = kNameAttributes[attributeName];
+ }
+ validateUnicode(value, `${name}.value`);
+
+ return [oid, value];
+}
+
+function normalizeDistinguishedName(name, path) {
+ if (!ArrayIsArray(name)) {
+ if (!isAnyArrayBuffer(name) && !isArrayBufferView(name)) {
+ throw new ERR_INVALID_ARG_TYPE(
+ path,
+ [
+ 'Array',
+ 'ArrayBuffer',
+ 'SharedArrayBuffer',
+ 'Buffer',
+ 'TypedArray',
+ 'DataView',
+ ],
+ name);
+ }
+ return snapshotBinary(name, path);
+ }
+
+ validateArray(name, path);
+ const rdns = [];
+ for (let i = 0; i < name.length; i++) {
+ const entryPath = `${path}[${i}]`;
+ const entry = name[i];
+ const attributes = ArrayIsArray(entry) ? entry : [entry];
+ validateArray(attributes, entryPath, 1);
+ const normalized = [];
+ for (let j = 0; j < attributes.length; j++) {
+ ArrayPrototypePush(normalized, normalizeNameAttribute(
+ attributes[j], ArrayIsArray(entry) ? `${entryPath}[${j}]` : entryPath));
+ }
+ ArrayPrototypePush(rdns, normalized);
+ }
+ return rdns;
+}
+
+function serialBytes(value) {
+ if (typeof value !== 'bigint')
+ throw new ERR_INVALID_ARG_TYPE('options.serialNumber', 'bigint', value);
+ const maximum = (1n << 159n) - 1n;
+ if (value < 1n || value > maximum) {
+ throw new ERR_OUT_OF_RANGE(
+ 'options.serialNumber', '>= 1n and <= (1n << 159n) - 1n', value);
+ }
+ const bytes = [];
+ for (; value !== 0n; value >>= 8n)
+ ArrayPrototypePush(bytes, Number(value & 0xffn));
+ const output = Buffer.allocUnsafe(bytes.length);
+ for (let i = 0; i < bytes.length; i++)
+ output[i] = bytes[bytes.length - i - 1];
+ return output;
+}
+
+function formatTime(value, name) {
+ let milliseconds;
+ try {
+ milliseconds = DatePrototypeGetTime(value);
+ } catch {
+ throw new ERR_INVALID_ARG_TYPE(name, 'Date', value);
+ }
+ if (!NumberIsFinite(milliseconds))
+ throw new ERR_INVALID_ARG_VALUE(name, value, 'must be a valid Date');
+ milliseconds = MathFloor(milliseconds / 1000) * 1000;
+ const iso = DatePrototypeToISOString(new Date(milliseconds));
+ if (iso[0] === '+' || iso[0] === '-')
+ throw new ERR_OUT_OF_RANGE(name, 'between 1950 and 9999', value);
+ const year = NumberParseInt(StringPrototypeSlice(iso, 0, 4), 10);
+ if (year < 1950 || year > 9999)
+ throw new ERR_OUT_OF_RANGE(name, 'between 1950 and 9999', value);
+ // ASN1_TIME_set_string_X509() chooses UTCTime or GeneralizedTime.
+ const time = RegExpPrototypeSymbolReplace(
+ /[-:T]/g,
+ StringPrototypeSlice(iso, 0, 19),
+ '',
+ );
+ return { milliseconds, value: `${time}Z` };
+}
+
+function normalizeGeneralName(generalName, name) {
+ validateObject(generalName, name);
+ const type = getRequired(generalName, 'type', `${name}.type`);
+ const value = getRequired(generalName, 'value', `${name}.value`);
+ validateString(type, `${name}.type`);
+
+ switch (type) {
+ case 'dns':
+ validateGeneralNameText(value, `${name}.value`);
+ return [kGeneralNameDns, value];
+ case 'ip': {
+ validateString(value, `${name}.value`);
+ if (StringPrototypeIncludes(value, '%')) {
+ throw new ERR_INVALID_ARG_VALUE(
+ `${name}.value`, value, 'must not include a zone identifier');
+ }
+ const family = isIP(value);
+ if (family !== 4 && family !== 6) {
+ throw new ERR_INVALID_ARG_VALUE(
+ `${name}.value`, value, 'must be an IPv4 or IPv6 address');
+ }
+ return [kGeneralNameIp, value];
+ }
+ case 'email':
+ validateGeneralNameText(value, `${name}.value`);
+ return [kGeneralNameRfc822, value];
+ case 'uri':
+ validateGeneralNameText(value, `${name}.value`);
+ return [kGeneralNameUri, value];
+ default:
+ throw new ERR_INVALID_ARG_VALUE(`${name}.type`, type);
+ }
+}
+
+function normalizeGeneralNames(names, path) {
+ validateArray(names, path, 1);
+ const normalized = [];
+ for (let i = 0; i < names.length; i++) {
+ ArrayPrototypePush(
+ normalized, normalizeGeneralName(names[i], `${path}[${i}]`));
+ }
+ return normalized;
+}
+
+function getCritical(extension, name) {
+ const critical = extension.critical;
+ if (critical !== undefined) validateBoolean(critical, `${name}.critical`);
+ return critical === true;
+}
+
+function normalizeKeyUsage(usages, path) {
+ validateArray(usages, path, 1);
+ const seen = new SafeSet();
+ let mask = 0;
+ for (let i = 0; i < usages.length; i++) {
+ const usage = usages[i];
+ validateString(usage, `${path}[${i}]`);
+ if (!ObjectPrototypeHasOwnProperty(kKeyUsageBits, usage))
+ throw new ERR_INVALID_ARG_VALUE(`${path}[${i}]`, usage);
+ if (seen.has(usage)) {
+ throw new ERR_INVALID_ARG_VALUE(
+ `${path}[${i}]`, usage, 'must not be duplicated');
+ }
+ seen.add(usage);
+ mask |= 1 << kKeyUsageBits[usage];
+ }
+ return mask;
+}
+
+function normalizeExtendedKeyUsage(usages, path) {
+ validateArray(usages, path, 1);
+ const seen = new SafeSet();
+ const normalized = [];
+ for (let i = 0; i < usages.length; i++) {
+ const value = usages[i];
+ validateString(value, `${path}[${i}]`);
+ const oid = ObjectPrototypeHasOwnProperty(kExtendedKeyUsageOids, value) ?
+ kExtendedKeyUsageOids[value] : value;
+ if (seen.has(oid)) {
+ throw new ERR_INVALID_ARG_VALUE(
+ `${path}[${i}]`, value, 'must not be duplicated');
+ }
+ seen.add(oid);
+ ArrayPrototypePush(normalized, oid);
+ }
+ return normalized;
+}
+
+function normalizeExtensions(extensions, arrayPath = 'options.extensions') {
+ if (extensions === undefined) extensions = [];
+ validateArray(extensions, arrayPath);
+ const output = [];
+
+ for (let i = 0; i < extensions.length; i++) {
+ const path = `${arrayPath}[${i}]`;
+ const extension = extensions[i];
+ validateObject(extension, path);
+ const type = getRequired(extension, 'type', `${path}.type`);
+ validateString(type, `${path}.type`);
+ let normalized;
+
+ switch (type) {
+ case 'basicConstraints': {
+ const ca = getRequired(extension, 'ca', `${path}.ca`);
+ validateBoolean(ca, `${path}.ca`);
+ const pathLength = extension.pathLength;
+ if (pathLength !== undefined)
+ validateInteger(pathLength, `${path}.pathLength`, 0);
+ normalized = [
+ kExtensionBasicConstraints,
+ getCritical(extension, path),
+ ca,
+ pathLength,
+ ];
+ break;
+ }
+ case 'keyUsage':
+ normalized = [
+ kExtensionKeyUsage,
+ getCritical(extension, path),
+ normalizeKeyUsage(
+ getRequired(extension, 'usages', `${path}.usages`),
+ `${path}.usages`),
+ ];
+ break;
+ case 'extendedKeyUsage':
+ normalized = [
+ kExtensionExtendedKeyUsage,
+ getCritical(extension, path),
+ normalizeExtendedKeyUsage(
+ getRequired(extension, 'usages', `${path}.usages`),
+ `${path}.usages`),
+ ];
+ break;
+ case 'subjectAltName': {
+ const critical = getCritical(extension, path);
+ const names = normalizeGeneralNames(
+ getRequired(extension, 'names', `${path}.names`),
+ `${path}.names`);
+ normalized = [kExtensionSubjectAltName, critical, names];
+ break;
+ }
+ case 'subjectKeyIdentifier':
+ case 'authorityKeyIdentifier': {
+ const value = getRequired(extension, 'value', `${path}.value`);
+ normalized = [
+ type === 'subjectKeyIdentifier' ?
+ kExtensionSubjectKeyIdentifier :
+ kExtensionAuthorityKeyIdentifier,
+ false,
+ value === 'auto' ? undefined : snapshotBinary(value, `${path}.value`),
+ ];
+ break;
+ }
+ case 'raw': {
+ const oid = getRequired(extension, 'oid', `${path}.oid`);
+ validateString(oid, `${path}.oid`);
+ normalized = [
+ kExtensionRaw,
+ oid,
+ getCritical(extension, path),
+ snapshotBinary(
+ getRequired(extension, 'value', `${path}.value`), `${path}.value`),
+ ];
+ break;
+ }
+ default:
+ throw new ERR_INVALID_ARG_VALUE(`${path}.type`, type);
+ }
+
+ ArrayPrototypePush(output, normalized);
+ }
+ return output;
+}
+
+function normalizeOptions(options, mode) {
+ validateObject(options, 'options');
+ let csr;
+ let subject;
+ let publicKeyData;
+ let publicKeyFormat;
+ let publicKeyType;
+ let publicKeyPassphrase;
+ let publicKeyNamedCurve;
+ if (options.csr !== undefined) {
+ if (options.subject !== undefined) {
+ throw new ERR_INCOMPATIBLE_OPTION_PAIR('csr', 'subject');
+ }
+ if (options.publicKey !== undefined) {
+ throw new ERR_INCOMPATIBLE_OPTION_PAIR('csr', 'publicKey');
+ }
+ if (options.extensions !== undefined) {
+ throw new ERR_INCOMPATIBLE_OPTION_PAIR('csr', 'extensions');
+ }
+ csr = snapshotCertificateRequest(options.csr);
+ } else {
+ if (options.selectExtensions !== undefined) {
+ throw new ERR_MISSING_OPTION('options.csr');
+ }
+ subject = normalizeDistinguishedName(
+ getRequired(options, 'subject', 'options.subject'), 'options.subject');
+ ({
+ data: publicKeyData,
+ format: publicKeyFormat,
+ type: publicKeyType,
+ passphrase: publicKeyPassphrase,
+ namedCurve: publicKeyNamedCurve,
+ } = preparePublicOrPrivateKey(
+ getRequired(options, 'publicKey', 'options.publicKey'),
+ 'options.publicKey'));
+ }
+ const issuer = options.issuer;
+ if (issuer !== undefined && !isX509Certificate(issuer)) {
+ throw new ERR_INVALID_ARG_TYPE(
+ 'options.issuer', 'X509Certificate', issuer);
+ }
+
+ const serial = serialBytes(
+ getRequired(options, 'serialNumber', 'options.serialNumber'));
+ const notBefore = formatTime(
+ getRequired(options, 'notBefore', 'options.notBefore'),
+ 'options.notBefore');
+ const notAfterValue = getRequired(
+ options, 'notAfter', 'options.notAfter');
+ const notAfter = formatTime(notAfterValue, 'options.notAfter');
+ if (notAfter.milliseconds <= notBefore.milliseconds) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.notAfter', notAfterValue, 'must be after options.notBefore');
+ }
+
+ const signature = getRequired(options, 'signature', 'options.signature');
+ validateObject(signature, 'options.signature');
+ const signingKey = getRequired(
+ signature, 'key', 'options.signature.key');
+ const {
+ data: signingKeyData,
+ format: signingKeyFormat,
+ type: signingKeyType,
+ passphrase: signingKeyPassphrase,
+ namedCurve: signingKeyNamedCurve,
+ } = preparePrivateKey(signingKey, 'options.signature.key');
+ const digest = getRequired(signature, 'digest', 'options.signature.digest');
+ if (digest !== null) validateString(digest, 'options.signature.digest');
+ const saltLength = getSaltLength(signingKey, 'options.signature.key');
+ const padding = getPadding(signingKey, 'options.signature.key');
+ let extensions;
+ if (csr !== undefined) {
+ const policy = getRequired(
+ options, 'selectExtensions', 'options.selectExtensions');
+ validateFunction(policy, 'options.selectExtensions');
+ const { 0: preparedCsr, 1: requested } =
+ parseX509CertificateRequest(csr);
+ csr = preparedCsr;
+ const requestedOptions = [];
+ for (let i = 0; i < requested.length; i++) {
+ const extension = requested[i];
+ ArrayPrototypePush(requestedOptions, {
+ type: 'raw',
+ oid: extension.oid,
+ critical: extension.critical,
+ value: extension.value,
+ });
+ }
+ const selected = FunctionPrototypeCall(policy, undefined, requestedOptions);
+ validateArray(selected, 'options.selectExtensions return value');
+ extensions = normalizeExtensions(
+ selected, 'options.selectExtensions return value');
+ } else {
+ extensions = normalizeExtensions(options.extensions);
+ }
+ return new X509GenerateJob(
+ mode,
+ csr,
+ subject,
+ publicKeyData,
+ publicKeyFormat,
+ publicKeyType,
+ publicKeyPassphrase,
+ publicKeyNamedCurve,
+ issuer,
+ serial,
+ notBefore.value,
+ notAfter.value,
+ extensions,
+ signingKeyData,
+ signingKeyFormat,
+ signingKeyType,
+ signingKeyPassphrase,
+ signingKeyNamedCurve,
+ digest,
+ saltLength,
+ padding,
+ );
+}
+
+function generateX509Certificate(options, callback) {
+ validateFunction(callback, 'callback');
+ const job = normalizeOptions(options, kCryptoJobAsync);
+ job.ondone = (error, der) => {
+ if (error !== undefined)
+ return FunctionPrototypeCall(callback, job, error);
+ let certificate;
+ try {
+ certificate = new X509Certificate(Buffer.from(der));
+ } catch (err) {
+ return FunctionPrototypeCall(callback, job, err);
+ }
+ return FunctionPrototypeCall(callback, job, null, certificate);
+ };
+ job.run();
+}
+
+function generateX509CertificateSync(options) {
+ const job = normalizeOptions(options, kCryptoJobSync);
+ const { 0: error, 1: der } = job.run();
+ if (error !== undefined) throw error;
+ return new X509Certificate(Buffer.from(der));
+}
+
+module.exports = {
+ generateX509Certificate,
+ generateX509CertificateSync,
+};
diff --git a/node.gyp b/node.gyp
index 714609b03e6..9c0b270ea5f 100644
--- a/node.gyp
+++ b/node.gyp
@@ -429,6 +429,7 @@
'src/crypto/crypto_scrypt.cc',
'src/crypto/crypto_tls.cc',
'src/crypto/crypto_x509.cc',
+ 'src/crypto/crypto_x509_gen.cc',
'src/crypto/crypto_argon2.h',
'src/crypto/crypto_bio.h',
'src/crypto/crypto_dh.h',
@@ -458,6 +459,7 @@
'src/crypto/crypto_random.h',
'src/crypto/crypto_timing.h',
'src/crypto/crypto_x509.h',
+ 'src/crypto/crypto_x509_gen.h',
'src/node_crypto.cc',
'src/node_crypto.h',
],
diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h
index d299bc7f55b..060f77fc362 100644
--- a/src/crypto/crypto_util.h
+++ b/src/crypto/crypto_util.h
@@ -57,6 +57,7 @@ constexpr size_t kSizeOf_EVP_PKEY_CTX = 80;
constexpr size_t kSizeOf_HMAC_CTX = 32;
constexpr size_t kSizeOf_SSL_CTX = 240;
constexpr size_t kSizeOf_X509 = 128;
+constexpr size_t kSizeOf_X509_REQ = 128;
template <typename T>
constexpr T NumBitsToBytes(T bits) {
@@ -118,7 +119,10 @@ void Decode(const v8::FunctionCallbackInfo<v8::Value>& args,
V(KMAC_FAILED, "KMAC derivation failed") \
V(OK, "Ok") \
V(PBKDF2_FAILED, "PBKDF2 derivation failed") \
- V(SCRYPT_FAILED, "scrypt derivation failed")
+ V(SCRYPT_FAILED, "scrypt derivation failed") \
+ V(X509_GENERATION_FAILED, "X.509 certificate generation failed") \
+ V(X509_REQUEST_VERIFICATION_FAILED, \
+ "Certificate request signature verification failed")
enum class NodeCryptoError {
#define V(CODE, DESCRIPTION) CODE,
diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc
index 37a190a2166..2d5a4e4828b 100644
--- a/src/crypto/crypto_x509.cc
+++ b/src/crypto/crypto_x509.cc
@@ -955,7 +955,8 @@ void CheckPrivateKey(const FunctionCallbackInfo<Value>& args) {
ASSIGN_OR_RETURN_UNWRAP(&key, args[0]);
CHECK_EQ(key->Data().GetKeyType(), kKeyTypePrivate);
args.GetReturnValue().Set(
- cert->view().checkPrivateKey(key->Data().GetAsymmetricKey()));
+ cert->view().checkPrivateKey(key->Data().GetAsymmetricKey()) ==
+ X509View::CheckKeyResult::MATCH);
}
void CheckPublicKey(const FunctionCallbackInfo<Value>& args) {
@@ -1354,6 +1355,12 @@ MaybeLocal<Object> X509ToObject(Environment* env, const X509View& cert) {
}
} // namespace
+MaybeLocal<Value> CreateRawX509Extensions(
+ Environment* env,
+ const std::vector<ncrypto::X509RawExtension>& extensions) {
+ return CreateRawExtensions(env, extensions);
+}
+
Local<FunctionTemplate> X509Certificate::GetConstructorTemplate(
Environment* env) {
Local<FunctionTemplate> tmpl = env->x509_constructor_template();
diff --git a/src/crypto/crypto_x509.h b/src/crypto/crypto_x509.h
index 73f3799fed8..82708fdd958 100644
--- a/src/crypto/crypto_x509.h
+++ b/src/crypto/crypto_x509.h
@@ -18,6 +18,9 @@
namespace node {
namespace crypto {
+v8::MaybeLocal<v8::Value> CreateRawX509Extensions(
+ Environment* env, const std::vector<ncrypto::X509RawExtension>& extensions);
+
// ManagedX509 owns one isolate-local X509. Adopt makes the ownership transfer
// explicit at each call site.
class ManagedX509 final : public MemoryRetainer {
diff --git a/src/crypto/crypto_x509_gen.cc b/src/crypto/crypto_x509_gen.cc
new file mode 100644
index 00000000000..b797b218360
--- /dev/null
+++ b/src/crypto/crypto_x509_gen.cc
@@ -0,0 +1,1183 @@
+#include "crypto/crypto_x509_gen.h"
+
+#include "base_object-inl.h"
+#include "crypto/crypto_x509.h"
+#include "env-inl.h"
+#include "memory_tracker-inl.h"
+#include "ncrypto.h"
+#include "node_buffer.h"
+#include "node_errors.h"
+#include "threadpoolwork-inl.h"
+#include "uv.h"
+#include "v8.h"
+
+#include <openssl/asn1.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+
+#include <array>
+#include <limits>
+#include <string>
+#include <string_view>
+#include <unordered_set>
+#include <utility>
+
+namespace node {
+
+using ncrypto::BignumPointer;
+using ncrypto::BIOPointer;
+using ncrypto::DataPointer;
+using ncrypto::EVPKeyCtxPointer;
+using ncrypto::EVPKeyPointer;
+using ncrypto::EVPMDCtxPointer;
+using ncrypto::MarkPopErrorOnReturn;
+using ncrypto::X509Pointer;
+using v8::Array;
+using v8::Boolean;
+using v8::Context;
+using v8::FunctionCallbackInfo;
+using v8::JustVoid;
+using v8::Local;
+using v8::Maybe;
+using v8::MaybeLocal;
+using v8::Nothing;
+using v8::Object;
+using v8::Uint32;
+using v8::Value;
+
+namespace crypto {
+namespace {
+
+using ASN1BitStringPointer =
+ ncrypto::DeleteFnPtr<ASN1_BIT_STRING, ASN1_BIT_STRING_free>;
+using ASN1IntegerPointer =
+ ncrypto::DeleteFnPtr<ASN1_INTEGER, ASN1_INTEGER_free>;
+using ASN1ObjectPointer = ncrypto::DeleteFnPtr<ASN1_OBJECT, ASN1_OBJECT_free>;
+using ASN1OctetStringPointer =
+ ncrypto::DeleteFnPtr<ASN1_OCTET_STRING, ASN1_OCTET_STRING_free>;
+using AuthorityKeyIdPointer =
+ ncrypto::DeleteFnPtr<AUTHORITY_KEYID, AUTHORITY_KEYID_free>;
+using BasicConstraintsPointer =
+ ncrypto::DeleteFnPtr<BASIC_CONSTRAINTS, BASIC_CONSTRAINTS_free>;
+using ExtendedKeyUsagePointer =
+ ncrypto::DeleteFnPtr<EXTENDED_KEY_USAGE, EXTENDED_KEY_USAGE_free>;
+using GeneralNamePointer =
+ ncrypto::DeleteFnPtr<GENERAL_NAME, GENERAL_NAME_free>;
+using GeneralNamesPointer =
+ ncrypto::DeleteFnPtr<GENERAL_NAMES, GENERAL_NAMES_free>;
+using X509ExtensionPointer =
+ ncrypto::DeleteFnPtr<X509_EXTENSION, X509_EXTENSION_free>;
+using X509NamePointer = ncrypto::DeleteFnPtr<X509_NAME, X509_NAME_free>;
+
+struct X509ExtensionsDeleter final {
+ void operator()(STACK_OF(X509_EXTENSION) * extensions) const {
+ sk_X509_EXTENSION_pop_free(extensions, X509_EXTENSION_free);
+ }
+};
+
+using X509ExtensionsPointer =
+ std::unique_ptr<STACK_OF(X509_EXTENSION), X509ExtensionsDeleter>;
+
+constexpr std::string_view kSubjectAltNameOid = "2.5.29.17";
+constexpr std::string_view kSubjectKeyIdentifierOid = "2.5.29.14";
+constexpr std::string_view kAuthorityKeyIdentifierOid = "2.5.29.35";
+constexpr std::string_view kBasicConstraintsOid = "2.5.29.19";
+constexpr std::string_view kKeyUsageOid = "2.5.29.15";
+constexpr std::string_view kExtendedKeyUsageOid = "2.5.29.37";
+constexpr size_t kRfc7093KeyIdentifierLength = 20;
+constexpr size_t kMaximumSerialLength = 20;
+
+enum class GeneralNameKind : uint32_t {
+ kDns = 0,
+ kIp = 1,
+ kRfc822 = 2,
+ kUri = 4,
+};
+
+enum class ExtensionKind : uint32_t {
+ kRaw = 0,
+ kBasicConstraints = 1,
+ kKeyUsage = 2,
+ kExtendedKeyUsage = 3,
+ kSubjectAltName = 4,
+ kSubjectKeyIdentifier = 5,
+ kAuthorityKeyIdentifier = 6,
+};
+
+constexpr uint32_t kKeyUsageKeyCertSign = 1 << 5;
+
+struct ExtensionProfile final {
+ bool basic_constraints_ca = false;
+ bool basic_constraints_path_length = false;
+ bool key_usage_present = false;
+ uint32_t key_usage = 0;
+ bool subject_alt_name_critical = false;
+};
+
+X509Pointer NewCertificate() {
+#if NCRYPTO_USE_OPENSSL_PROVIDER
+ return X509Pointer(X509_new_ex(nullptr, nullptr));
+#elif NCRYPTO_USE_BORINGSSL
+ return X509Pointer(X509_new());
+#else
+#error "Unsupported ncrypto backend"
+#endif
+}
+
+ByteSource EncodeCertificate(X509* certificate) {
+ const int length = i2d_X509(certificate, nullptr);
+ if (length <= 0) [[unlikely]]
+ return {};
+
+ auto der = DataPointer::Alloc(static_cast<size_t>(length));
+ if (!der) [[unlikely]]
+ return {};
+ unsigned char* cursor = der.get<unsigned char>();
+ if (i2d_X509(certificate, &cursor) != length ||
+ cursor != der.get<unsigned char>() + length) [[unlikely]] {
+ return {};
+ }
+ return ByteSource::Allocated(der.release());
+}
+
+X509RequestPointer ParseDerRequest(ncrypto::Buffer<const unsigned char> input,
+ DataPointer* encoded = nullptr) {
+ if (input.data == nullptr || input.len == 0 ||
+ input.len > static_cast<size_t>(LONG_MAX)) {
+ return {};
+ }
+ const unsigned char* cursor = input.data;
+ X509RequestPointer request(d2i_X509_REQ(
+ nullptr, &cursor, static_cast<long>(input.len))); // NOLINT(runtime/int)
+ if (!request) return {};
+ if (encoded != nullptr) {
+ *encoded = DataPointer::Copy({
+ .data = input.data,
+ .len = static_cast<size_t>(cursor - input.data),
+ });
+ if (!*encoded) return {};
+ }
+ return request;
+}
+
+X509RequestPointer ParseRequest(ncrypto::Buffer<const unsigned char> input,
+ DataPointer* encoded) {
+ if (encoded == nullptr) return {};
+ if (input.data == nullptr || input.len == 0 || input.len > INT_MAX) return {};
+
+ BIOPointer bio = BIOPointer::New(input.data, input.len);
+ unsigned char* der = nullptr;
+ long der_length = 0; // NOLINT(runtime/int)
+ if (bio &&
+ PEM_bytes_read_bio(&der,
+ &der_length,
+ nullptr,
+ PEM_STRING_X509_REQ,
+ bio.get(),
+ NoPasswordCallback,
+ nullptr) == 1 &&
+ der != nullptr && der_length > 0) {
+ DataPointer decoded(der, static_cast<size_t>(der_length));
+ return ParseDerRequest(
+ {
+ .data = decoded.get<const unsigned char>(),
+ .len = decoded.size(),
+ },
+ encoded);
+ }
+ OPENSSL_free(der);
+ return ParseDerRequest(input, encoded);
+}
+
+bool IsValidRequest(X509_REQ* request) {
+ if (request == nullptr || X509_REQ_get_version(request) != 0) return false;
+ const X509_NAME* subject = X509_REQ_get_subject_name(request);
+ return subject != nullptr && X509_NAME_entry_count(subject) >= 0 &&
+ X509_REQ_get0_pubkey(request) != nullptr;
+}
+
+std::optional<std::vector<ncrypto::X509RawExtension>>
+InspectRequestedExtensions(X509_REQ* request) {
+ const int attribute_count = X509_REQ_get_attr_count(request);
+ if (attribute_count < 0) return std::nullopt;
+ bool has_extension_request = false;
+ for (int index = 0; index < attribute_count; index++) {
+ X509_ATTRIBUTE* attribute = X509_REQ_get_attr(request, index);
+ if (attribute == nullptr) return std::nullopt;
+ const ASN1_OBJECT* object = X509_ATTRIBUTE_get0_object(attribute);
+ if (object == nullptr) return std::nullopt;
+ if (X509_REQ_extension_nid(OBJ_obj2nid(object)) == 1) {
+ has_extension_request = true;
+ break;
+ }
+ }
+ if (!has_extension_request) {
+ return std::vector<ncrypto::X509RawExtension>();
+ }
+ X509ExtensionsPointer extensions(X509_REQ_get_extensions(request));
+ if (!extensions) return std::nullopt;
+ return ncrypto::inspectX509Extensions(extensions.get());
+}
+
+bool GetArrayEntry(Local<Context> context,
+ Local<Array> array,
+ uint32_t index,
+ Local<Value>* value) {
+ return array->Get(context, index).ToLocal(value);
+}
+
+ASN1ObjectPointer ParseOid(Environment* env,
+ Local<Value> value,
+ std::string* canonical = nullptr) {
+ if (!value->IsString()) return {};
+ Utf8Value oid(env->isolate(), value);
+ if (*oid == nullptr || oid.length() == 0) return {};
+ const std::string input = oid.ToString();
+ ASN1ObjectPointer object(OBJ_txt2obj(*oid, 1));
+ if (!object) return {};
+
+ const int length = OBJ_obj2txt(nullptr, 0, object.get(), 1);
+ if (length <= 0 || length == std::numeric_limits<int>::max()) return {};
+ std::string text(static_cast<size_t>(length) + 1, '\0');
+ if (OBJ_obj2txt(text.data(), length + 1, object.get(), 1) != length) {
+ return {};
+ }
+ text.resize(static_cast<size_t>(length));
+ if (text != input) return {};
+ if (canonical != nullptr) *canonical = std::move(text);
+ return object;
+}
+
+std::string ReadString(Environment* env, Local<Value> value) {
+ if (!value->IsString()) return {};
+ return Utf8Value(env->isolate(), value).ToString();
+}
+
+bool IsAsciiWithoutNul(std::string_view input) {
+ for (unsigned char byte : input) {
+ if (byte == 0 || byte > 0x7f) return false;
+ }
+ return true;
+}
+
+X509NamePointer BuildName(Environment* env, Local<Array> spec) {
+ Local<Context> context = env->context();
+ X509NamePointer name(X509_NAME_new());
+ if (!name) [[unlikely]]
+ return {};
+
+ for (uint32_t rdn_index = 0; rdn_index < spec->Length(); rdn_index++) {
+ Local<Value> rdn_value;
+ if (!GetArrayEntry(context, spec, rdn_index, &rdn_value) ||
+ !rdn_value->IsArray()) {
+ return {};
+ }
+ Local<Array> rdn = rdn_value.As<Array>();
+ if (rdn->Length() == 0) return {};
+
+ for (uint32_t index = 0; index < rdn->Length(); index++) {
+ Local<Value> attribute_value;
+ if (!GetArrayEntry(context, rdn, index, &attribute_value) ||
+ !attribute_value->IsArray()) {
+ return {};
+ }
+ Local<Array> attribute = attribute_value.As<Array>();
+ if (attribute->Length() != 2) return {};
+
+ Local<Value> oid_value;
+ Local<Value> content_value;
+ if (!GetArrayEntry(context, attribute, 0, &oid_value) ||
+ !GetArrayEntry(context, attribute, 1, &content_value) ||
+ !content_value->IsString()) {
+ return {};
+ }
+ ASN1ObjectPointer oid = ParseOid(env, oid_value);
+ if (!oid) return {};
+
+ const std::string text = ReadString(env, content_value);
+ if (text.size() > static_cast<size_t>(std::numeric_limits<int>::max())) {
+ return {};
+ }
+ const auto* bytes = reinterpret_cast<const unsigned char*>(text.data());
+ if (X509_NAME_add_entry_by_OBJ(name.get(),
+ oid.get(),
+ MBSTRING_UTF8,
+ bytes,
+ static_cast<int>(text.size()),
+ -1,
+ index == 0 ? 0 : -1) != 1) {
+ return {};
+ }
+ }
+ }
+ return name;
+}
+
+X509NamePointer DecodeName(Local<Value> value) {
+ ArrayBufferOrViewContents<unsigned char> input(value);
+ if (!input.CheckSizeInt32() || input.size() == 0) return {};
+
+ const unsigned char* cursor = input.data();
+ X509NamePointer name(
+ d2i_X509_NAME(nullptr,
+ &cursor,
+ static_cast<long>(input.size()))); // NOLINT(runtime/int)
+ if (!name || cursor != input.data() + input.size()) return {};
+ return name;
+}
+
+bool SetString(ASN1_STRING* target, std::string_view value) {
+ return value.size() <= static_cast<size_t>(std::numeric_limits<int>::max()) &&
+ ASN1_STRING_set(
+ target, value.data(), static_cast<int>(value.size())) == 1;
+}
+
+GeneralNamePointer BuildGeneralName(Environment* env, Local<Array> spec) {
+ Local<Context> context = env->context();
+ if (spec->Length() != 2) return {};
+
+ Local<Value> kind_value;
+ Local<Value> value;
+ if (!GetArrayEntry(context, spec, 0, &kind_value) ||
+ !GetArrayEntry(context, spec, 1, &value) || !kind_value->IsUint32() ||
+ !value->IsString()) {
+ return {};
+ }
+
+ const auto kind =
+ static_cast<GeneralNameKind>(kind_value.As<Uint32>()->Value());
+ const std::string text = ReadString(env, value);
+ GeneralNamePointer name(GENERAL_NAME_new());
+ if (!name) return {};
+
+ switch (kind) {
+ case GeneralNameKind::kDns:
+ case GeneralNameKind::kRfc822:
+ case GeneralNameKind::kUri: {
+ if (text.empty() || !IsAsciiWithoutNul(text)) return {};
+ ncrypto::DeleteFnPtr<ASN1_IA5STRING, ASN1_IA5STRING_free> string(
+ ASN1_IA5STRING_new());
+ if (!string || !SetString(string.get(), text)) return {};
+ const int type = kind == GeneralNameKind::kDns ? GEN_DNS
+ : kind == GeneralNameKind::kRfc822 ? GEN_EMAIL
+ : GEN_URI;
+ GENERAL_NAME_set0_value(name.get(), type, string.release());
+ break;
+ }
+ case GeneralNameKind::kIp: {
+ if (text.find('\0') != std::string::npos ||
+ text.find('%') != std::string::npos) {
+ return {};
+ }
+ std::array<unsigned char, 16> address{};
+ size_t length;
+ if (uv_inet_pton(AF_INET, text.c_str(), address.data()) == 0) {
+ length = 4;
+ } else if (uv_inet_pton(AF_INET6, text.c_str(), address.data()) == 0) {
+ length = address.size();
+ } else {
+ return {};
+ }
+ ASN1OctetStringPointer string(ASN1_OCTET_STRING_new());
+ if (!string ||
+ ASN1_OCTET_STRING_set(
+ string.get(), address.data(), static_cast<int>(length)) != 1) {
+ return {};
+ }
+ GENERAL_NAME_set0_value(name.get(), GEN_IPADD, string.release());
+ break;
+ }
+ default:
+ return {};
+ }
+ return name;
+}
+
+GeneralNamesPointer BuildGeneralNames(Environment* env, Local<Array> specs) {
+ Local<Context> context = env->context();
+ if (specs->Length() == 0) return {};
+ GeneralNamesPointer names(GENERAL_NAMES_new());
+ if (!names) return {};
+
+ for (uint32_t index = 0; index < specs->Length(); index++) {
+ Local<Value> value;
+ if (!GetArrayEntry(context, specs, index, &value) || !value->IsArray()) {
+ return {};
+ }
+ GeneralNamePointer name = BuildGeneralName(env, value.As<Array>());
+ if (!name || sk_GENERAL_NAME_push(names.get(), name.get()) <= 0) return {};
+ name.release();
+ }
+ return names;
+}
+
+bool IsValidSerial(const unsigned char* data, size_t size) {
+ if (size == 0 || size > kMaximumSerialLength || data[0] == 0) return false;
+ return size != kMaximumSerialLength || (data[0] & 0x80) == 0;
+}
+
+bool ReadKeyIdentifierSpec(Environment* env,
+ Local<Array> spec,
+ bool* automatic,
+ ASN1OctetStringPointer* identifier) {
+ Local<Context> context = env->context();
+ Local<Value> critical_value;
+ Local<Value> identifier_value;
+ if (spec->Length() != 3 ||
+ !GetArrayEntry(context, spec, 1, &critical_value) ||
+ !GetArrayEntry(context, spec, 2, &identifier_value) ||
+ !critical_value->IsBoolean()) {
+ return false;
+ }
+
+ *automatic = identifier_value->IsUndefined();
+ identifier->reset();
+ if (*automatic) return true;
+ if (!IsAnyBufferSource(identifier_value)) return false;
+
+ ArrayBufferOrViewContents<unsigned char> contents(identifier_value);
+ if (!contents.CheckSizeInt32()) return false;
+ identifier->reset(ASN1_OCTET_STRING_new());
+ return *identifier &&
+ ASN1_OCTET_STRING_set(identifier->get(),
+ contents.size() == 0 ? nullptr : contents.data(),
+ static_cast<int>(contents.size())) == 1;
+}
+
+// RFC 7093, Section 2, method 1.
+ASN1OctetStringPointer ComputeKeyIdentifier(const X509* certificate) {
+ std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
+ unsigned int digest_length = 0;
+ if (X509_pubkey_digest(
+ certificate, EVP_sha256(), digest.data(), &digest_length) != 1 ||
+ digest_length < kRfc7093KeyIdentifierLength) {
+ return {};
+ }
+ ASN1OctetStringPointer identifier(ASN1_OCTET_STRING_new());
+ if (!identifier || ASN1_OCTET_STRING_set(
+ identifier.get(),
+ digest.data(),
+ static_cast<int>(kRfc7093KeyIdentifierLength)) != 1) {
+ return {};
+ }
+ return identifier;
+}
+
+bool AddExtension(X509* certificate, X509ExtensionPointer extension) {
+ return extension && X509_add_ext(certificate, extension.get(), -1) == 1;
+}
+
+bool BuildAndAddExtension(
+ Environment* env,
+ X509* certificate,
+ const X509* signing_certificate,
+ const ASN1_OCTET_STRING* self_signed_subject_key_identifier,
+ Local<Array> spec,
+ std::string* oid_text,
+ ExtensionProfile* profile) {
+ Local<Context> context = env->context();
+ if (spec->Length() < 2) return false;
+
+ Local<Value> kind_value;
+ if (!GetArrayEntry(context, spec, 0, &kind_value) ||
+ !kind_value->IsUint32()) {
+ return false;
+ }
+ const auto kind =
+ static_cast<ExtensionKind>(kind_value.As<Uint32>()->Value());
+
+ Local<Value> critical_value;
+ if (kind == ExtensionKind::kRaw) {
+ Local<Value> oid_value;
+ Local<Value> value;
+ if (spec->Length() != 4 || !GetArrayEntry(context, spec, 1, &oid_value) ||
+ !GetArrayEntry(context, spec, 2, &critical_value) ||
+ !GetArrayEntry(context, spec, 3, &value) || !oid_value->IsString() ||
+ !critical_value->IsBoolean()) {
+ return false;
+ }
+ ASN1ObjectPointer oid = ParseOid(env, oid_value, oid_text);
+ if (!oid || !IsAnyBufferSource(value)) {
+ return false;
+ }
+ ArrayBufferOrViewContents<unsigned char> contents(value);
+ if (!contents.CheckSizeInt32()) return false;
+ ASN1OctetStringPointer octets(ASN1_OCTET_STRING_new());
+ if (!octets ||
+ ASN1_OCTET_STRING_set(octets.get(),
+ contents.data(),
+ static_cast<int>(contents.size())) != 1) {
+ return false;
+ }
+ X509ExtensionPointer extension(X509_EXTENSION_create_by_OBJ(
+ nullptr,
+ oid.get(),
+ critical_value.As<Boolean>()->Value() ? 1 : 0,
+ octets.get()));
+ return AddExtension(certificate, std::move(extension));
+ }
+
+ if (!GetArrayEntry(context, spec, 1, &critical_value) ||
+ !critical_value->IsBoolean()) {
+ return false;
+ }
+ const bool critical = critical_value.As<Boolean>()->Value();
+
+ switch (kind) {
+ case ExtensionKind::kRaw:
+ UNREACHABLE();
+ case ExtensionKind::kBasicConstraints: {
+ Local<Value> ca_value;
+ Local<Value> path_length_value;
+ if (spec->Length() != 4 || !GetArrayEntry(context, spec, 2, &ca_value) ||
+ !GetArrayEntry(context, spec, 3, &path_length_value) ||
+ !ca_value->IsBoolean()) {
+ return false;
+ }
+ BasicConstraintsPointer constraints(BASIC_CONSTRAINTS_new());
+ if (!constraints) return false;
+ constraints->ca = ca_value.As<Boolean>()->Value() ? 0xff : 0;
+ profile->basic_constraints_ca = constraints->ca != 0;
+ if (!path_length_value->IsUndefined()) {
+ int64_t path_length;
+ if (!path_length_value->IntegerValue(context).To(&path_length) ||
+ path_length < 0) {
+ return false;
+ }
+ constraints->pathlen = ASN1_INTEGER_new();
+ if (constraints->pathlen == nullptr ||
+ ASN1_INTEGER_set_int64(constraints->pathlen, path_length) != 1) {
+ return false;
+ }
+ profile->basic_constraints_path_length = true;
+ }
+ oid_text->assign(kBasicConstraintsOid);
+ return AddExtension(
+ certificate,
+ X509ExtensionPointer(X509V3_EXT_i2d(
+ NID_basic_constraints, critical ? 1 : 0, constraints.get())));
+ }
+ case ExtensionKind::kKeyUsage: {
+ Local<Value> mask_value;
+ if (spec->Length() != 3 ||
+ !GetArrayEntry(context, spec, 2, &mask_value) ||
+ !mask_value->IsUint32()) {
+ return false;
+ }
+ const uint32_t mask = mask_value.As<Uint32>()->Value();
+ if (mask == 0 || (mask & ~0x1ffU) != 0) return false;
+ ASN1BitStringPointer usage(ASN1_BIT_STRING_new());
+ if (!usage) return false;
+ for (int bit = 0; bit <= 8; bit++) {
+ if ((mask & (1U << bit)) != 0 &&
+ ASN1_BIT_STRING_set_bit(usage.get(), bit, 1) != 1) {
+ return false;
+ }
+ }
+ profile->key_usage_present = true;
+ profile->key_usage = mask;
+ oid_text->assign(kKeyUsageOid);
+ return AddExtension(certificate,
+ X509ExtensionPointer(X509V3_EXT_i2d(
+ NID_key_usage, critical ? 1 : 0, usage.get())));
+ }
+ case ExtensionKind::kExtendedKeyUsage: {
+ Local<Value> usages_value;
+ if (spec->Length() != 3 ||
+ !GetArrayEntry(context, spec, 2, &usages_value) ||
+ !usages_value->IsArray()) {
+ return false;
+ }
+ Local<Array> usages = usages_value.As<Array>();
+ if (usages->Length() == 0) return false;
+ ExtendedKeyUsagePointer usage(EXTENDED_KEY_USAGE_new());
+ if (!usage) return false;
+ for (uint32_t index = 0; index < usages->Length(); index++) {
+ Local<Value> oid_value;
+ if (!GetArrayEntry(context, usages, index, &oid_value)) return false;
+ ASN1ObjectPointer oid = ParseOid(env, oid_value);
+ if (!oid || sk_ASN1_OBJECT_push(usage.get(), oid.get()) <= 0) {
+ return false;
+ }
+ oid.release();
+ }
+ oid_text->assign(kExtendedKeyUsageOid);
+ return AddExtension(
+ certificate,
+ X509ExtensionPointer(X509V3_EXT_i2d(
+ NID_ext_key_usage, critical ? 1 : 0, usage.get())));
+ }
+ case ExtensionKind::kSubjectAltName: {
+ Local<Value> names_value;
+ if (spec->Length() != 3 ||
+ !GetArrayEntry(context, spec, 2, &names_value) ||
+ !names_value->IsArray()) {
+ return false;
+ }
+ Local<Array> name_specs = names_value.As<Array>();
+ GeneralNamesPointer names = BuildGeneralNames(env, name_specs);
+ if (!names) return false;
+ profile->subject_alt_name_critical = critical;
+ oid_text->assign(kSubjectAltNameOid);
+ return AddExtension(
+ certificate,
+ X509ExtensionPointer(X509V3_EXT_i2d(
+ NID_subject_alt_name, critical ? 1 : 0, names.get())));
+ }
+ case ExtensionKind::kSubjectKeyIdentifier:
+ case ExtensionKind::kAuthorityKeyIdentifier: {
+ bool automatic;
+ ASN1OctetStringPointer identifier;
+ if (!ReadKeyIdentifierSpec(env, spec, &automatic, &identifier)) {
+ return false;
+ }
+ if (automatic) {
+ if (kind == ExtensionKind::kAuthorityKeyIdentifier) {
+ if (signing_certificate == certificate &&
+ self_signed_subject_key_identifier != nullptr) {
+ identifier.reset(
+ ASN1_OCTET_STRING_dup(self_signed_subject_key_identifier));
+ if (!identifier) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(
+ env, "Failed to copy an X.509 key identifier");
+ return false;
+ }
+ } else if (signing_certificate != certificate) {
+ int issuer_identifier_status = -1;
+ identifier.reset(static_cast<ASN1_OCTET_STRING*>(
+ X509_get_ext_d2i(signing_certificate,
+ NID_subject_key_identifier,
+ &issuer_identifier_status,
+ nullptr)));
+ if (!identifier && issuer_identifier_status != -1) {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env, "The issuer has an invalid Subject Key Identifier");
+ return false;
+ }
+ }
+ }
+ if (!identifier) {
+ const X509* source = kind == ExtensionKind::kSubjectKeyIdentifier
+ ? certificate
+ : signing_certificate;
+ identifier = ComputeKeyIdentifier(source);
+ }
+ if (!identifier) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(
+ env, "Failed to derive an X.509 key identifier");
+ return false;
+ }
+ }
+
+ if (kind == ExtensionKind::kSubjectKeyIdentifier) {
+ oid_text->assign(kSubjectKeyIdentifierOid);
+ return AddExtension(
+ certificate,
+ X509ExtensionPointer(X509V3_EXT_i2d(NID_subject_key_identifier,
+ critical ? 1 : 0,
+ identifier.get())));
+ }
+
+ AuthorityKeyIdPointer value(AUTHORITY_KEYID_new());
+ if (!value) return false;
+ value->keyid = identifier.release();
+ oid_text->assign(kAuthorityKeyIdentifierOid);
+ return AddExtension(
+ certificate,
+ X509ExtensionPointer(X509V3_EXT_i2d(
+ NID_authority_key_identifier, critical ? 1 : 0, value.get())));
+ }
+ default:
+ return false;
+ }
+}
+
+bool ValidateExtensionProfile(bool subject_empty,
+ const ExtensionProfile& profile) {
+ if (subject_empty && !profile.subject_alt_name_critical) {
+ return false;
+ }
+
+ const bool key_cert_sign = profile.key_usage_present &&
+ (profile.key_usage & kKeyUsageKeyCertSign) != 0;
+ if (key_cert_sign && !profile.basic_constraints_ca) return false;
+ if (profile.basic_constraints_path_length &&
+ (!profile.basic_constraints_ca || !key_cert_sign)) {
+ return false;
+ }
+ return true;
+}
+
+void ParseX509CertificateRequest(const FunctionCallbackInfo<Value>& args) {
+ MarkPopErrorOnReturn mark_pop_error_on_return;
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ CHECK(IsAnyBufferSource(args[0]));
+ ArrayBufferOrViewContents<unsigned char> input(args[0]);
+ if (!input.CheckSizeInt32()) [[unlikely]] {
+ THROW_ERR_OUT_OF_RANGE(env, "X.509 certificate request is too large");
+ return;
+ }
+
+ DataPointer encoded;
+ X509RequestPointer request(
+ ParseRequest({.data = input.data(), .len = input.size()}, &encoded));
+ if (!IsValidRequest(request.get())) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env, "The \"options.csr\" argument is not a valid X.509 request");
+ return;
+ }
+
+ const auto inspected = InspectRequestedExtensions(request.get());
+ if (!inspected.has_value() || !encoded) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env, "The \"options.csr\" argument is not a valid X.509 request");
+ return;
+ }
+
+ Local<Object> der;
+ Local<Value> requested_extensions;
+ Local<Array> result = Array::New(env->isolate(), 2);
+ const char* data = encoded.size() == 0 ? "" : encoded.get<const char>();
+ if (!Buffer::Copy(env, data, encoded.size()).ToLocal(&der) ||
+ !CreateRawX509Extensions(env, inspected.value())
+ .ToLocal(&requested_extensions) ||
+ result->CreateDataProperty(context, 0, der).IsNothing() ||
+ result->CreateDataProperty(context, 1, requested_extensions)
+ .IsNothing()) {
+ return;
+ }
+ args.GetReturnValue().Set(result);
+}
+
+bool GenerationFailed(CryptoErrorStore* errors) {
+ errors->Capture();
+ errors->Insert(NodeCryptoError::X509_GENERATION_FAILED);
+ errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED");
+ return false;
+}
+
+bool RequestVerificationFailed(CryptoErrorStore* errors) {
+ errors->Insert(NodeCryptoError::X509_REQUEST_VERIFICATION_FAILED);
+ errors->SetNodeErrorCode("ERR_CRYPTO_OPERATION_FAILED");
+ return false;
+}
+
+} // namespace
+
+X509GenerateConfig::X509GenerateConfig(X509GenerateConfig&& other) noexcept
+ : certificate(std::move(other.certificate)),
+ request(std::move(other.request)),
+ request_public_key(std::move(other.request_public_key)),
+ signing_key(std::move(other.signing_key)),
+ digest(other.digest),
+ salt_length(other.salt_length),
+ padding(other.padding) {}
+
+X509GenerateConfig& X509GenerateConfig::operator=(
+ X509GenerateConfig&& other) noexcept {
+ if (&other == this) return *this;
+ this->~X509GenerateConfig();
+ return *new (this) X509GenerateConfig(std::move(other));
+}
+
+void X509GenerateConfig::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackFieldWithSize("certificate", certificate ? kSizeOf_X509 : 0);
+ tracker->TrackFieldWithSize("request", request ? kSizeOf_X509_REQ : 0);
+ tracker->TrackFieldWithSize("request_public_key",
+ request_public_key ? kSizeOf_EVP_PKEY : 0);
+ tracker->TrackField("signing_key", signing_key);
+}
+
+Maybe<void> X509GenerateTraits::AdditionalConfig(
+ CryptoJobMode mode,
+ const FunctionCallbackInfo<Value>& args,
+ unsigned int offset,
+ X509GenerateConfig* params) {
+ MarkPopErrorOnReturn mark_pop_error_on_return;
+ Environment* env = Environment::GetCurrent(args);
+ Local<Context> context = env->context();
+ static_cast<void>(mode);
+
+ CHECK(args[offset]->IsUndefined() || IsAnyBufferSource(args[offset]));
+ CHECK(args[offset + 1]->IsUndefined() || args[offset + 1]->IsArray() ||
+ IsAnyBufferSource(args[offset + 1]));
+ const bool from_request = IsAnyBufferSource(args[offset]);
+ unsigned int cursor = offset + 2;
+ KeyObjectData public_key;
+ X509RequestPointer request;
+ EVPKeyPointer owned_request_public_key;
+ OSSL3_CONST X509_NAME* request_subject = nullptr;
+ EVP_PKEY* request_public_key = nullptr;
+ if (from_request) {
+ CHECK(args[offset + 1]->IsUndefined());
+ ArrayBufferOrViewContents<unsigned char> input(args[offset]);
+ request = ParseDerRequest({.data = input.data(), .len = input.size()});
+ if (!IsValidRequest(request.get())) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env, "The \"options.csr\" argument is not a valid X.509 request");
+ return Nothing<void>();
+ }
+ request_subject = X509_REQ_get_subject_name(request.get());
+ request_public_key = X509_REQ_get0_pubkey(request.get());
+ if (request_public_key == nullptr ||
+ EVP_PKEY_up_ref(request_public_key) != 1) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env, "The \"options.csr\" argument is not a valid X.509 request");
+ return Nothing<void>();
+ }
+ owned_request_public_key.reset(request_public_key);
+ for (unsigned int index = 0; index < 5; index++) {
+ CHECK(args[cursor + index]->IsUndefined());
+ }
+ cursor += 5;
+ } else {
+ CHECK(args[offset + 1]->IsArray() || IsAnyBufferSource(args[offset + 1]));
+ public_key = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &cursor);
+ if (!public_key) return Nothing<void>();
+ if (public_key.GetKeyType() == kKeyTypeSecret) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid certificate public key");
+ return Nothing<void>();
+ }
+ }
+
+ const unsigned int issuer_offset = cursor++;
+ const unsigned int serial_offset = cursor++;
+ const unsigned int not_before_offset = cursor++;
+ const unsigned int not_after_offset = cursor++;
+ const unsigned int extensions_offset = cursor++;
+ CHECK(args[issuer_offset]->IsUndefined() ||
+ X509Certificate::HasInstance(env, args[issuer_offset]));
+ CHECK(IsAnyBufferSource(args[serial_offset]));
+ CHECK(args[not_before_offset]->IsString());
+ CHECK(args[not_after_offset]->IsString());
+ CHECK(args[extensions_offset]->IsArray());
+
+ KeyObjectData signing_key =
+ KeyObjectData::GetPrivateKeyFromJs(args, &cursor, true);
+ if (!signing_key) return Nothing<void>();
+ if (signing_key.GetKeyType() != kKeyTypePrivate) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env, "The \"options.signature.key\" argument must be a private key");
+ return Nothing<void>();
+ }
+
+ const unsigned int digest_offset = cursor++;
+ const unsigned int salt_length_offset = cursor++;
+ const unsigned int padding_offset = cursor;
+ CHECK(args[digest_offset]->IsString() || args[digest_offset]->IsNull());
+ CHECK(args[salt_length_offset]->IsUndefined() ||
+ args[salt_length_offset]->IsInt32());
+ CHECK(args[padding_offset]->IsUndefined() || args[padding_offset]->IsInt32());
+ if (args[digest_offset]->IsString()) {
+ Utf8Value digest(env->isolate(), args[digest_offset]);
+ params->digest = ncrypto::Digest::FromName(*digest);
+ if (!params->digest) [[unlikely]] {
+ THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", digest);
+ return Nothing<void>();
+ }
+ } else {
+ const auto* algorithm = signing_key.GetAsymmetricKey().getAlgorithm();
+ if (algorithm == nullptr || !algorithm->isOneShot()) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env,
+ "The \"options.signature.digest\" option may be null only for "
+ "an intrinsic signature algorithm");
+ return Nothing<void>();
+ }
+ }
+ if (args[salt_length_offset]->IsInt32()) {
+ params->salt_length = args[salt_length_offset].As<v8::Int32>()->Value();
+ }
+ if (args[padding_offset]->IsInt32()) {
+ params->padding = args[padding_offset].As<v8::Int32>()->Value();
+ }
+
+#if !NCRYPTO_HAS_X509_RSA_PSS_FLEXIBLE_PARAMETERS
+ const EVPKeyPointer& native_signing_key = signing_key.GetAsymmetricKey();
+ if (native_signing_key.isRsaVariant()) {
+ const int padding =
+ params->padding.value_or(native_signing_key.getDefaultSignPadding());
+ if (padding == RSA_PKCS1_PSS_PADDING) {
+ const int digest_nid =
+ params->digest ? EVP_MD_type(params->digest.get()) : NID_undef;
+ const int digest_size =
+ params->digest ? EVP_MD_size(params->digest.get()) : -1;
+ const bool supported_digest = digest_nid == NID_sha256 ||
+ digest_nid == NID_sha384 ||
+ digest_nid == NID_sha512;
+ const bool supported_salt =
+ !params->salt_length.has_value() ||
+ params->salt_length == RSA_PSS_SALTLEN_DIGEST ||
+ params->salt_length == digest_size;
+ if (!supported_digest || !supported_salt) {
+ THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
+ env,
+ "This backend does not support the requested X.509 RSA-PSS "
+ "parameters");
+ return Nothing<void>();
+ }
+ }
+ }
+#endif
+
+ X509NamePointer owned_subject;
+ OSSL3_CONST X509_NAME* subject = request_subject;
+ EVP_PKEY* certificate_public_key = request_public_key;
+ if (!from_request) {
+ Local<Value> subject_value = args[offset + 1];
+ owned_subject = subject_value->IsArray()
+ ? BuildName(env, subject_value.As<Array>())
+ : DecodeName(subject_value);
+ if (!owned_subject) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid subject distinguished name");
+ return Nothing<void>();
+ }
+ subject = owned_subject.get();
+ certificate_public_key = public_key.GetAsymmetricKey().get();
+ }
+ const bool subject_empty = X509_NAME_entry_count(subject) == 0;
+ if (subject_empty && args[issuer_offset]->IsUndefined()) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env, "A self-signed X.509 certificate must have a non-empty subject");
+ return Nothing<void>();
+ }
+
+ ArrayBufferOrViewContents<unsigned char> serial(args[serial_offset]);
+ if (!serial.CheckSizeInt32()) [[unlikely]] {
+ THROW_ERR_OUT_OF_RANGE(env, "X.509 generation input is too large");
+ return Nothing<void>();
+ }
+ if (!IsValidSerial(serial.data(), serial.size())) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid certificate serial number");
+ return Nothing<void>();
+ }
+
+ X509Pointer certificate = NewCertificate();
+ if (!certificate) [[unlikely]] {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(
+ env, "Failed to allocate an X.509 certificate");
+ return Nothing<void>();
+ }
+ if (X509_set_version(certificate.get(), 2) != 1 ||
+ X509_set_subject_name(certificate.get(), subject) != 1 ||
+ X509_set_pubkey(certificate.get(), certificate_public_key) != 1)
+ [[unlikely]] {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(
+ env, "Failed to construct an X.509 certificate");
+ return Nothing<void>();
+ }
+
+ const X509* signing_certificate = certificate.get();
+ if (args[issuer_offset]->IsUndefined()) {
+ if (X509_set_issuer_name(certificate.get(), subject) != 1) [[unlikely]] {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+ "Failed to set the X.509 issuer name");
+ return Nothing<void>();
+ }
+ } else {
+ X509Certificate* issuer;
+ ASSIGN_OR_RETURN_UNWRAP(
+ &issuer, args[issuer_offset].As<Object>(), Nothing<void>());
+ auto* issuer_name = X509_get_subject_name(issuer->get());
+ if (issuer_name == nullptr || X509_NAME_entry_count(issuer_name) <= 0) {
+ THROW_ERR_INVALID_ARG_VALUE(
+ env,
+ "The \"options.issuer\" certificate must have a non-empty subject");
+ return Nothing<void>();
+ }
+ if (X509_set_issuer_name(certificate.get(), issuer_name) != 1)
+ [[unlikely]] {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+ "Failed to set the X.509 issuer name");
+ return Nothing<void>();
+ }
+ signing_certificate = issuer->get();
+ }
+
+ switch (ncrypto::X509View(signing_certificate)
+ .checkPrivateKey(signing_key.GetAsymmetricKey())) {
+ case ncrypto::X509View::CheckKeyResult::MATCH:
+ break;
+ case ncrypto::X509View::CheckKeyResult::NO_MATCH:
+ THROW_ERR_INVALID_ARG_VALUE(
+ env,
+ "The \"options.signature.key\" argument does not match the "
+ "certificate public key");
+ return Nothing<void>();
+ case ncrypto::X509View::CheckKeyResult::UNSUPPORTED_OPERATION:
+ break;
+ case ncrypto::X509View::CheckKeyResult::OPERATION_FAILED:
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to compare public keys");
+ return Nothing<void>();
+ }
+
+ BignumPointer serial_value(serial.data(), serial.size());
+ ASN1IntegerPointer serial_number(
+ serial_value ? BN_to_ASN1_INTEGER(serial_value.get(), nullptr) : nullptr);
+ if (!serial_number ||
+ X509_set_serialNumber(certificate.get(), serial_number.get()) != 1)
+ [[unlikely]] {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+ "Failed to set the X.509 serial number");
+ return Nothing<void>();
+ }
+
+ Utf8Value not_before(env->isolate(), args[not_before_offset]);
+ Utf8Value not_after(env->isolate(), args[not_after_offset]);
+ if (*not_before == nullptr || *not_after == nullptr ||
+ ASN1_TIME_set_string_X509(X509_getm_notBefore(certificate.get()),
+ *not_before) != 1 ||
+ ASN1_TIME_set_string_X509(X509_getm_notAfter(certificate.get()),
+ *not_after) != 1) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid certificate validity time");
+ return Nothing<void>();
+ }
+
+ Local<Array> extensions = args[extensions_offset].As<Array>();
+ ASN1OctetStringPointer self_signed_subject_key_identifier;
+ if (signing_certificate == certificate.get()) {
+ for (uint32_t index = 0; index < extensions->Length(); index++) {
+ Local<Value> entry_value;
+ Local<Value> kind_value;
+ if (!GetArrayEntry(context, extensions, index, &entry_value) ||
+ !entry_value->IsArray() ||
+ !GetArrayEntry(context, entry_value.As<Array>(), 0, &kind_value) ||
+ !kind_value->IsUint32()) {
+ if (env->isolate()->HasPendingException()) return Nothing<void>();
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid X.509 extension");
+ return Nothing<void>();
+ }
+ if (static_cast<ExtensionKind>(kind_value.As<Uint32>()->Value()) !=
+ ExtensionKind::kSubjectKeyIdentifier) {
+ continue;
+ }
+ bool automatic;
+ ASN1OctetStringPointer identifier;
+ if (!ReadKeyIdentifierSpec(
+ env, entry_value.As<Array>(), &automatic, &identifier)) {
+ if (env->isolate()->HasPendingException()) return Nothing<void>();
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid X.509 extension");
+ return Nothing<void>();
+ }
+ if (!automatic) {
+ self_signed_subject_key_identifier = std::move(identifier);
+ }
+ break;
+ }
+ }
+
+ std::unordered_set<std::string> extension_oids;
+ ExtensionProfile profile;
+ for (uint32_t index = 0; index < extensions->Length(); index++) {
+ Local<Value> entry_value;
+ if (!extensions->Get(context, index).ToLocal(&entry_value)) {
+ return Nothing<void>();
+ }
+ CHECK(entry_value->IsArray());
+ std::string oid;
+ const int previous_extension_count = X509_get_ext_count(certificate.get());
+ if (previous_extension_count < 0 ||
+ !BuildAndAddExtension(env,
+ certificate.get(),
+ signing_certificate,
+ self_signed_subject_key_identifier.get(),
+ entry_value.As<Array>(),
+ &oid,
+ &profile)) [[unlikely]] {
+ if (env->isolate()->HasPendingException()) return Nothing<void>();
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid X.509 extension");
+ return Nothing<void>();
+ }
+ if (X509_get_ext_count(certificate.get()) != previous_extension_count + 1)
+ [[unlikely]] {
+ THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
+ env, "The crypto backend does not support this X.509 extension");
+ return Nothing<void>();
+ }
+ if (!extension_oids.emplace(std::move(oid)).second) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Duplicate X.509 extension OID");
+ return Nothing<void>();
+ }
+ }
+ if (!ValidateExtensionProfile(subject_empty, profile)) [[unlikely]] {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid X.509 extension profile");
+ return Nothing<void>();
+ }
+
+ params->certificate = std::move(certificate);
+ params->request = std::move(request);
+ params->request_public_key = std::move(owned_request_public_key);
+ params->signing_key = std::move(signing_key);
+ return JustVoid();
+}
+
+bool X509GenerateTraits::DeriveBits(Environment* env,
+ const X509GenerateConfig& params,
+ ByteSource* out,
+ CryptoJobMode mode,
+ CryptoErrorStore* errors) {
+ static_cast<void>(env);
+ static_cast<void>(mode);
+
+ if (params.request) {
+ int verification_result = 0;
+ {
+ MarkPopErrorOnReturn mark_pop_error_on_return;
+ if (params.request_public_key) {
+ verification_result = X509_REQ_verify(params.request.get(),
+ params.request_public_key.get());
+ }
+ }
+ if (verification_result != 1) [[unlikely]] {
+ return RequestVerificationFailed(errors);
+ }
+ }
+
+ X509* certificate = params.certificate.get();
+ const EVPKeyPointer& signing_key = params.signing_key.GetAsymmetricKey();
+ EVPMDCtxPointer context = EVPMDCtxPointer::New();
+ if (!context) [[unlikely]]
+ return GenerationFailed(errors);
+ auto signing_context = context.signInit(signing_key, params.digest);
+ if (!signing_context.has_value()) [[unlikely]] {
+ return GenerationFailed(errors);
+ }
+ if (signing_key.isRsaVariant()) {
+ const int padding =
+ params.padding.value_or(signing_key.getDefaultSignPadding());
+ if (!EVPKeyCtxPointer::setRsaPadding(
+ *signing_context, padding, params.salt_length)) [[unlikely]] {
+ return GenerationFailed(errors);
+ }
+ }
+ if (X509_sign_ctx(certificate, context.get()) <= 0) [[unlikely]] {
+ return GenerationFailed(errors);
+ }
+
+ *out = EncodeCertificate(certificate);
+ return *out ? true : GenerationFailed(errors);
+}
+
+MaybeLocal<Value> X509GenerateTraits::EncodeOutput(
+ Environment* env, const X509GenerateConfig& params, ByteSource* out) {
+ static_cast<void>(params);
+ return out->ToArrayBuffer(env);
+}
+
+void X509Generation::Initialize(Environment* env, Local<Object> target) {
+ SetMethod(env->context(),
+ target,
+ "parseX509CertificateRequest",
+ ParseX509CertificateRequest);
+}
+
+void X509Generation::RegisterExternalReferences(
+ ExternalReferenceRegistry* registry) {
+ registry->Register(ParseX509CertificateRequest);
+}
+
+} // namespace crypto
+} // namespace node
diff --git a/src/crypto/crypto_x509_gen.h b/src/crypto/crypto_x509_gen.h
new file mode 100644
index 00000000000..268b09c4777
--- /dev/null
+++ b/src/crypto/crypto_x509_gen.h
@@ -0,0 +1,68 @@
+#ifndef SRC_CRYPTO_CRYPTO_X509_GEN_H_
+#define SRC_CRYPTO_CRYPTO_X509_GEN_H_
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+
+#include "async_wrap.h"
+#include "crypto/crypto_keys.h"
+#include "crypto/crypto_util.h"
+#include "memory_tracker.h"
+#include "v8.h"
+
+#include <optional>
+
+namespace node::crypto {
+
+using X509RequestPointer = ncrypto::DeleteFnPtr<X509_REQ, X509_REQ_free>;
+
+struct X509GenerateConfig final : public MemoryRetainer {
+ ncrypto::X509Pointer certificate;
+ X509RequestPointer request;
+ ncrypto::EVPKeyPointer request_public_key;
+ KeyObjectData signing_key;
+ ncrypto::Digest digest;
+ std::optional<int> salt_length;
+ std::optional<int> padding;
+
+ X509GenerateConfig() = default;
+ explicit X509GenerateConfig(X509GenerateConfig&& other) noexcept;
+ X509GenerateConfig& operator=(X509GenerateConfig&& other) noexcept;
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_MEMORY_INFO_NAME(X509GenerateConfig)
+ SET_SELF_SIZE(X509GenerateConfig)
+};
+
+struct X509GenerateTraits final {
+ using AdditionalParameters = X509GenerateConfig;
+ static constexpr const char* JobName = "X509GenerateJob";
+ static constexpr AsyncWrap::ProviderType Provider =
+ AsyncWrap::PROVIDER_SIGNREQUEST;
+
+ static v8::Maybe<void> AdditionalConfig(
+ CryptoJobMode mode,
+ const v8::FunctionCallbackInfo<v8::Value>& args,
+ unsigned int offset,
+ X509GenerateConfig* params);
+
+ static bool DeriveBits(Environment* env,
+ const X509GenerateConfig& params,
+ ByteSource* out,
+ CryptoJobMode mode,
+ CryptoErrorStore* errors);
+
+ static v8::MaybeLocal<v8::Value> EncodeOutput(
+ Environment* env, const X509GenerateConfig& params, ByteSource* out);
+};
+
+using X509GenerateJob = DeriveBitsJob<X509GenerateTraits>;
+
+struct X509Generation final {
+ static void Initialize(Environment* env, v8::Local<v8::Object> target);
+ static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
+};
+
+} // namespace node::crypto
+
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#endif // SRC_CRYPTO_CRYPTO_X509_GEN_H_
diff --git a/src/node_crypto.cc b/src/node_crypto.cc
index 9b096ac8437..f043d18ab3f 100644
--- a/src/node_crypto.cc
+++ b/src/node_crypto.cc
@@ -61,7 +61,9 @@ namespace crypto {
V(Timing) \
V(Util) \
V(Verify) \
- V(X509Certificate)
+ V(X509Certificate) \
+ V(X509Generation) \
+ V(X509GenerateJob)
#if OPENSSL_WITH_ARGON2
#define ARGON2_NAMESPACE_LIST(V) V(Argon2)
diff --git a/src/node_crypto.h b/src/node_crypto.h
index 92d35b844f9..4518336fde2 100644
--- a/src/node_crypto.h
+++ b/src/node_crypto.h
@@ -61,6 +61,7 @@
#include "crypto/crypto_turboshake.h"
#include "crypto/crypto_util.h"
#include "crypto/crypto_x509.h"
+#include "crypto/crypto_x509_gen.h"
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/test/parallel/test-crypto-key-store-pkcs11.js b/test/parallel/test-crypto-key-store-pkcs11.js
index 56855b0d63e..af843277291 100644
--- a/test/parallel/test-crypto-key-store-pkcs11.js
+++ b/test/parallel/test-crypto-key-store-pkcs11.js
@@ -30,8 +30,11 @@ const {
createVerify,
diffieHellman,
generateKeyPairSync,
+ generateX509Certificate,
+ generateX509CertificateSync,
sign,
verify,
+ X509Certificate,
} = require('crypto');
const tmpdir = require('../common/tmpdir');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
@@ -446,6 +449,48 @@ function assertInlineSignWithStoreUrl(privateKey) {
assert.strictEqual(verify('sha256', kData, publicKey, signature), true);
}
+async function assertX509Generation(privateKey, publicKey) {
+ const notBefore = new Date('2025-01-01T00:00:00Z');
+ const notAfter = new Date('2035-01-01T00:00:00Z');
+ const issuer = generateX509CertificateSync({
+ subject: [{ name: 'CN', value: 'Node.js PKCS#11 test CA' }],
+ publicKey,
+ serialNumber: 1n,
+ notBefore,
+ notAfter,
+ signature: { key: privateKey, digest: 'sha256' },
+ });
+ assert(issuer instanceof X509Certificate);
+ assert.strictEqual(issuer.verify(publicKey), true);
+
+ const { publicKey: leafPublicKey } = generateKeyPairSync(
+ 'ec', { namedCurve: 'prime256v1' });
+ const leaf = await new Promise((resolve, reject) => {
+ generateX509Certificate({
+ subject: [{ name: 'CN', value: 'Node.js PKCS#11 test leaf' }],
+ publicKey: leafPublicKey,
+ issuer,
+ serialNumber: 2n,
+ notBefore,
+ notAfter,
+ signature: {
+ key: {
+ key: privateKeyUrl('node-rsa'),
+ passphrase: kPin,
+ properties: kProperties,
+ },
+ digest: 'sha256',
+ },
+ }, common.mustCall((err, certificate) => {
+ if (err) reject(err);
+ else resolve(certificate);
+ }));
+ });
+ assert(leaf instanceof X509Certificate);
+ assert.strictEqual(leaf.verify(publicKey), true);
+ assert.strictEqual(leaf.checkIssued(issuer), true);
+}
+
async function testRsa() {
const privateKey = loadPrivateKey('node-rsa');
assertKeyDetails(privateKey, 'private', 'rsa');
@@ -460,6 +505,7 @@ async function testRsa() {
await assertAsyncSignVerify('sha256', kData, privateKey);
assertKeyObjectEquality(privateKey, 'node-rsa');
assertInlineSignWithStoreUrl(privateKey);
+ await assertX509Generation(privateKey, publicKey);
assertPublicExports(publicKey);
assertPrivateExportsRejected(privateKey, 'rsa');
await assertPrivateCryptoKeyExportsRejected(
diff --git a/test/parallel/test-crypto-x509-generate-csr.js b/test/parallel/test-crypto-x509-generate-csr.js
new file mode 100644
index 00000000000..d45baf11457
--- /dev/null
+++ b/test/parallel/test-crypto-x509-generate-csr.js
@@ -0,0 +1,444 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const { hasFIPS } = require('../common/crypto');
+const {
+ createPrivateKey,
+ generateKeyPairSync,
+ generateX509Certificate,
+ generateX509CertificateSync,
+ X509Certificate,
+} = require('crypto');
+const fixtures = require('../common/fixtures');
+
+const csr = `-----BEGIN CERTIFICATE REQUEST-----
+MIIBOzCB4wIBADAuMRAwDgYDVQQKDAdOb2RlLmpzMRowGAYDVQQDDBFyZXF1ZXN0
+ZWQuZXhhbXBsZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEppwUcxZQc2qbTx
+ko8VEYAtKhMPbzzkhMlC3iqv+DJG9gXRJ3Kt8vz3/HuBV6gGDjuaTObou//p7sBj
+OdvcTXCgUzBRBgkqhkiG9w0BCQ4xRDBCMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/
+BAQDAgOIMCIGA1UdEQQbMBmCEXJlcXVlc3RlZC5leGFtcGxlhwTAAAIBMAoGCCqG
+SM49BAMCA0cAMEQCIBYr+BErErRvcUIOPoAORKKS2x4mPpLPKlWNU1MxItrvAiAg
+0eXrZcLTeGhOpUB3hkIdfASAgqOp+sHKWLj5nBr/5A==
+-----END CERTIFICATE REQUEST-----`;
+const csrDer = Buffer.from(
+ csr.split('\n').slice(1, -1).join(''),
+ 'base64',
+);
+const ed25519Csr = `-----BEGIN CERTIFICATE REQUEST-----
+MIGhMFUCAQAwIjEgMB4GA1UEAwwXZWQyNTUxOS1yZXF1ZXN0LmV4YW1wbGUwKjAF
+BgMrZXADIQArXAii6qe6IDThvdasxr7Eogoil8d601lG/eH18F/vr6AAMAUGAytl
+cANBACDLbdXbIje6mUPTvgtLtdUlWBV39VwDlq4jwoYm09XgriyJKSh4vt7RUuhI
+7gjQGMN+Thmaas/5esSZYZ5ZmQQ=
+-----END CERTIFICATE REQUEST-----`;
+const emptySubjectCsr = `-----BEGIN CERTIFICATE REQUEST-----
+MIHwMIGYAgEAMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARKacFHMWUHNqm0
+8ZKPFRGALSoTD2885ITJQt4qr/gyRvYF0SdyrfL89/x7gVeoBg47mkzm6Lv/6e7A
+Yznb3E1woDYwNAYJKoZIhvcNAQkOMScwJTAjBgNVHREBAf8EGTAXghVlbXB0eS1z
+dWJqZWN0LmV4YW1wbGUwCgYIKoZIzj0EAwIDRwAwRAIgCrO1gUwniOWwi99FaB8X
+lFGt950G2N/Yaa2mv5uV3qsCIB4S7xXaiS4GcwwAgrsFrmXOb+Wq3jx+HNoS0O21
+kK/d
+-----END CERTIFICATE REQUEST-----`;
+const privateKeyPem = fixtures.readKey('ec-key.pem');
+const privateKey = createPrivateKey(privateKeyPem);
+const notBefore = new Date('2025-01-02T03:04:05Z');
+const notAfter = new Date('2030-01-02T03:04:05Z');
+
+const requestedExtensions = [
+ {
+ type: 'raw',
+ oid: '2.5.29.19',
+ critical: true,
+ value: Buffer.from('3000', 'hex'),
+ },
+ {
+ type: 'raw',
+ oid: '2.5.29.15',
+ critical: true,
+ value: Buffer.from('03020388', 'hex'),
+ },
+ {
+ type: 'raw',
+ oid: '2.5.29.17',
+ critical: false,
+ value: Buffer.from(
+ '301982117265717565737465642e6578616d706c658704c0000201',
+ 'hex',
+ ),
+ },
+];
+
+function options(
+ csrInput = csr,
+ selectExtensions = (requested) => requested,
+) {
+ return {
+ csr: csrInput,
+ serialNumber: 1n,
+ notBefore,
+ notAfter,
+ signature: {
+ key: privateKeyPem,
+ digest: 'sha256',
+ },
+ selectExtensions,
+ };
+}
+
+function assertRequestCertificate(certificate) {
+ assert(certificate instanceof X509Certificate);
+ assert.strictEqual(certificate.subject, 'O=Node.js\nCN=requested.example');
+ assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
+}
+
+const padded = Buffer.concat([Buffer.alloc(7), csrDer, Buffer.alloc(5)]);
+const offsetView = new Uint8Array(
+ padded.buffer,
+ padded.byteOffset + 7,
+ csrDer.length,
+);
+const sharedCsr = new SharedArrayBuffer(csrDer.length);
+new Uint8Array(sharedCsr).set(csrDer);
+const arrayBufferCsr = csrDer.buffer.slice(
+ csrDer.byteOffset,
+ csrDer.byteOffset + csrDer.byteLength,
+);
+for (const input of [
+ csr,
+ Buffer.from(csr),
+ csrDer,
+ offsetView,
+ arrayBufferCsr,
+ new DataView(arrayBufferCsr),
+ sharedCsr,
+ `ignored by the backend parser\n${csr}\r\n`,
+]) {
+ const certificate = generateX509CertificateSync(options(input, common.mustCall((requested) => {
+ assert.deepStrictEqual(requested, requestedExtensions);
+ return requested;
+ })));
+ assertRequestCertificate(certificate);
+ assert.deepStrictEqual(
+ certificate.extensions,
+ requestedExtensions.map(({ type, ...extension }) => extension),
+ );
+}
+
+{
+ const certificate = generateX509CertificateSync(options(csr, () => [
+ { type: 'basicConstraints', ca: false },
+ {
+ type: 'subjectAltName',
+ names: [{ type: 'dns', value: 'policy.example' }],
+ },
+ ]));
+ assertRequestCertificate(certificate);
+ assert.strictEqual(certificate.checkHost('policy.example'), 'policy.example');
+ assert.strictEqual(certificate.checkHost('requested.example'), undefined);
+}
+
+{
+ const certificate = generateX509CertificateSync(options(csr, () => [
+ { type: 'subjectKeyIdentifier', value: 'auto' },
+ { type: 'authorityKeyIdentifier', value: 'auto' },
+ ]));
+ assertRequestCertificate(certificate);
+ assert.deepStrictEqual(
+ certificate.extensions.map(({ oid }) => oid),
+ ['2.5.29.14', '2.5.29.35'],
+ );
+ assert.deepStrictEqual(
+ certificate.extensions[0].value.subarray(2),
+ certificate.extensions[1].value.subarray(4),
+ );
+}
+
+{
+ const requestWithoutExtensions = fixtures.readKey('ec-csr.pem', 'utf8');
+ let requested;
+ const certificate = generateX509CertificateSync(options(
+ requestWithoutExtensions,
+ (value) => {
+ requested = value;
+ return [];
+ },
+ ));
+ assert.deepStrictEqual(requested, []);
+ assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
+}
+
+{
+ const rsaCsr = fixtures.readKey('agent1-csr.pem', 'utf8');
+ const rsaKey = fixtures.readKey('agent1-key.pem');
+ const certificate = generateX509CertificateSync({
+ ...options(rsaCsr, () => []),
+ signature: { key: rsaKey, digest: 'sha256' },
+ });
+ assert.strictEqual(certificate.checkPrivateKey(createPrivateKey(rsaKey)), true);
+}
+
+if (!hasFIPS()) {
+ const ed25519Key = fixtures.readKey('ed25519_private.pem');
+ const certificate = generateX509CertificateSync({
+ ...options(ed25519Csr, () => []),
+ signature: { key: ed25519Key, digest: null },
+ });
+ assert.strictEqual(certificate.subject, 'CN=ed25519-request.example');
+ assert.strictEqual(
+ certificate.checkPrivateKey(createPrivateKey(ed25519Key)),
+ true,
+ );
+}
+
+{
+ const issuerKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+ const issuer = generateX509CertificateSync({
+ subject: [{ name: 'CN', value: 'CSR issuer' }],
+ publicKey: issuerKeys.publicKey,
+ serialNumber: 2n,
+ notBefore,
+ notAfter,
+ signature: { key: issuerKeys.privateKey, digest: 'sha256' },
+ });
+ const certificate = generateX509CertificateSync({
+ ...options(csr, () => []),
+ issuer,
+ signature: { key: issuerKeys.privateKey, digest: 'sha256' },
+ });
+ assert.strictEqual(certificate.verify(issuerKeys.publicKey), true);
+ assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
+ assert.strictEqual(certificate.checkIssued(issuer), true);
+
+ assert.throws(() => generateX509CertificateSync({
+ ...options(emptySubjectCsr),
+ issuer,
+ signature: { key: issuerKeys.privateKey, digest: 'sha256' },
+ }), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: /extension profile/,
+ });
+ const emptySubjectCertificate = generateX509CertificateSync({
+ ...options(emptySubjectCsr, () => [{
+ type: 'subjectAltName',
+ critical: true,
+ names: [{ type: 'dns', value: 'empty-subject.example' }],
+ }]),
+ issuer,
+ signature: { key: issuerKeys.privateKey, digest: 'sha256' },
+ });
+ assert.strictEqual(emptySubjectCertificate.subject, '');
+ assert.strictEqual(
+ emptySubjectCertificate.checkHost('empty-subject.example'),
+ 'empty-subject.example',
+ );
+}
+
+for (const conflict of [
+ { subject: [{ name: 'CN', value: 'ignored' }] },
+ { publicKey: privateKey },
+ { extensions: [] },
+]) {
+ assert.throws(
+ () => generateX509CertificateSync({ ...options(), ...conflict }),
+ { code: 'ERR_INCOMPATIBLE_OPTION_PAIR' },
+ );
+}
+
+assert.throws(() => generateX509CertificateSync({
+ selectExtensions: common.mustNotCall(),
+}), {
+ code: 'ERR_MISSING_OPTION',
+ message: /options\.csr/,
+});
+
+{
+ const missing = options();
+ delete missing.selectExtensions;
+ assert.throws(() => generateX509CertificateSync(missing), {
+ code: 'ERR_MISSING_OPTION',
+ message: /options\.selectExtensions/,
+ });
+}
+
+for (const selectExtensions of [
+ [],
+ undefined,
+ () => undefined,
+ () => Promise.resolve([]),
+]) {
+ const value = options(csr, selectExtensions);
+ if (selectExtensions === undefined) delete value.selectExtensions;
+ assert.throws(() => generateX509CertificateSync(value), {
+ code: selectExtensions === undefined ? 'ERR_MISSING_OPTION' :
+ 'ERR_INVALID_ARG_TYPE',
+ message: /options\.selectExtensions/,
+ });
+}
+
+assert.throws(
+ () => generateX509CertificateSync(options(csr, () => [{}])),
+ {
+ code: 'ERR_MISSING_OPTION',
+ message: /options\.selectExtensions return value\[0\]\.type/,
+ },
+);
+
+{
+ const expected = new Error('policy rejected the request');
+ assert.throws(() => generateX509Certificate(
+ options(csr, () => { throw expected; }),
+ common.mustNotCall(),
+ ), (error) => error === expected);
+}
+
+const invalidExtensionValue = Buffer.from(csrDer);
+const extensionRequest = invalidExtensionValue.indexOf(
+ Buffer.from('31443042', 'hex'),
+);
+assert.notStrictEqual(extensionRequest, -1);
+invalidExtensionValue[extensionRequest + 2] = 0x04;
+
+const invalidVersion = Buffer.from(csrDer);
+invalidVersion[9] = 1;
+
+const duplicateRequestedOid = Buffer.from(csrDer);
+const subjectAltNameOid = duplicateRequestedOid.indexOf(
+ Buffer.from('0603551d11', 'hex'),
+);
+assert.notStrictEqual(subjectAltNameOid, -1);
+duplicateRequestedOid[subjectAltNameOid + 4] = 0x13;
+
+const duplicateExtensionRequests = Buffer.from(
+ 'MIIBkDCCATcCAQAwLjEQMA4GA1UECgwHTm9kZS5qczEaMBgGA1UEAwwRcmVx' +
+ 'dWVzdGVkLmV4YW1wbGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARKacFH' +
+ 'MWUHNqm08ZKPFRGALSoTD2885ITJQt4qr/gyRvYF0SdyrfL89/x7gVeoBg47' +
+ 'mkzm6Lv/6e7AYznb3E1woIGmMFEGCSqGSIb3DQEJDjFEMEIwDAYDVR0TAQH/' +
+ 'BAIwADAOBgNVHQ8BAf8EBAMCA4gwIgYDVR0RBBswGYIRcmVxdWVzdGVkLmV4' +
+ 'YW1wbGWHBMAAAgEwUQYJKoZIhvcNAQkOMUQwQjAMBgNVHRMBAf8EAjAAMA4G' +
+ 'A1UdDwEB/wQEAwIDiDAiBgNVHREEGzAZghFyZXF1ZXN0ZWQuZXhhbXBsZYcE' +
+ 'wAACATAKBggqhkjOPQQDAgNHADBEAiAWK/gRKxK0b3FCDj6ADkSiktseJj6S' +
+ 'zypVjVNTMSLa7wIgINHl62XC03hoTqVAd4ZCHXwEgIKjqfrByli4+Zwa/+Q=',
+ 'base64',
+);
+assert.strictEqual(duplicateExtensionRequests.length, 404);
+
+const pemWithDecodedSuffix =
+ `-----BEGIN CERTIFICATE REQUEST-----\n${Buffer.concat([
+ csrDer,
+ Buffer.from('suffix'),
+ ]).toString('base64')}\n-----END CERTIFICATE REQUEST-----`;
+
+// CSR inputs follow the crypto backend's parsing behavior. As with existing
+// certificate inputs, bytes after the first decoded object are not rejected.
+for (const input of [
+ Buffer.concat([csrDer, Buffer.from('suffix')]),
+ `${csr}\n${csr}`,
+ pemWithDecodedSuffix,
+]) {
+ assertRequestCertificate(generateX509CertificateSync(options(input)));
+}
+
+// When more than one recognized extension-request attribute is present, use
+// the list selected by the crypto backend.
+assert.throws(() => generateX509CertificateSync(options(
+ duplicateExtensionRequests,
+ common.mustCall((requested) => {
+ assert.deepStrictEqual(requested, requestedExtensions);
+ return requested;
+ }),
+)), {
+ code: 'ERR_CRYPTO_OPERATION_FAILED',
+ message: /Certificate request signature verification failed/,
+});
+
+for (const invalid of [
+ Buffer.from([0x30, 0x00]),
+ invalidVersion,
+ invalidExtensionValue,
+ csr.replace('REQUEST-----\n', 'REQUEST-----junk\n'),
+]) {
+ assert.throws(() => generateX509CertificateSync(
+ options(invalid, common.mustNotCall()),
+ ), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: /options\.csr/,
+ });
+}
+assert.throws(() => generateX509Certificate(
+ options(invalidExtensionValue, common.mustNotCall()),
+ common.mustNotCall(),
+), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: /options\.csr/,
+});
+
+// Requested extensions are untrusted policy input. Even conflicting requests
+// reach the policy callback so that it can choose what to include. The modified
+// request no longer has a valid proof of possession, which is checked only
+// after policy has run.
+assert.throws(() => generateX509CertificateSync(
+ options(duplicateRequestedOid, common.mustCall((requested) => {
+ assert.strictEqual(requested[0].oid, requested[2].oid);
+ return [requested[0]];
+ })),
+), {
+ code: 'ERR_CRYPTO_OPERATION_FAILED',
+ message: /Certificate request signature verification failed/,
+});
+
+const invalidProof = Buffer.from(csrDer);
+invalidProof[invalidProof.length - 1] ^= 1;
+let invalidProofPolicyCalls = 0;
+assert.throws(() => generateX509CertificateSync(options(
+ invalidProof,
+ () => {
+ invalidProofPolicyCalls++;
+ return [];
+ },
+)), {
+ code: 'ERR_CRYPTO_OPERATION_FAILED',
+ message: /Certificate request signature verification failed/,
+});
+assert.strictEqual(invalidProofPolicyCalls, 1);
+
+(async () => {
+ let returned = false;
+ let policyInput;
+ const mutableCsr = Buffer.from(csrDer);
+ const generated = await new Promise((resolve, reject) => {
+ generateX509Certificate(options(mutableCsr, common.mustCall((requested) => {
+ assert.strictEqual(returned, false);
+ policyInput = requested;
+ return requested;
+ })), common.mustCall((error, certificate) => {
+ assert.strictEqual(returned, true);
+ if (error) return reject(error);
+ resolve(certificate);
+ }));
+ returned = true;
+ mutableCsr.fill(0);
+ policyInput[0].value.fill(0);
+ });
+ assertRequestCertificate(generated);
+ assert.deepStrictEqual(
+ generated.extensions[0].value,
+ requestedExtensions[0].value,
+ );
+
+ await new Promise((resolve) => {
+ generateX509Certificate(options(invalidProof, () => []), common.mustCall(
+ (error, certificate) => {
+ assert.strictEqual(error.code, 'ERR_CRYPTO_OPERATION_FAILED');
+ assert.match(
+ error.message,
+ /Certificate request signature verification failed/,
+ );
+ assert.strictEqual(certificate, undefined);
+ resolve();
+ },
+ ));
+ });
+})().then(common.mustCall());
diff --git a/test/parallel/test-crypto-x509-generate.js b/test/parallel/test-crypto-x509-generate.js
new file mode 100644
index 00000000000..9f47f86bafe
--- /dev/null
+++ b/test/parallel/test-crypto-x509-generate.js
@@ -0,0 +1,841 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const { hasFIPS, isBoringSSL } = require('../common/crypto');
+const {
+ constants,
+ createPublicKey,
+ generateKeyPairSync,
+ generateX509Certificate,
+ generateX509CertificateSync,
+ X509Certificate,
+} = require('crypto');
+const { promisify } = require('util');
+
+const generationModule = 'NativeModule internal/crypto/x509_generation';
+assert(!process.moduleLoadList.includes(generationModule));
+
+const notBefore = new Date('2025-01-02T03:04:05.999Z');
+const notAfter = new Date('2050-01-02T03:04:05.123Z');
+const subjectKeyIdentifierOid = '2.5.29.14';
+const authorityKeyIdentifierOid = '2.5.29.35';
+const encodedSubject = Buffer.from(
+ '3011310f300d060355040313066167656e7431', 'hex');
+const generateX509CertificateAsync = promisify(generateX509Certificate);
+
+function findExtension(certificate, oid) {
+ const extension = certificate.extensions.find((entry) => entry.oid === oid);
+ assert(extension);
+ return extension;
+}
+
+function getSubjectKeyIdentifier(certificate) {
+ const { value } = findExtension(certificate, subjectKeyIdentifierOid);
+ assert.deepStrictEqual(value.subarray(0, 2), Buffer.from([0x04, 0x14]));
+ assert.strictEqual(value.length, 22);
+ return value.subarray(2);
+}
+
+function getAuthorityKeyIdentifier(certificate) {
+ const { value } = findExtension(certificate, authorityKeyIdentifierOid);
+ assert.deepStrictEqual(
+ value.subarray(0, 4), Buffer.from([0x30, 0x16, 0x80, 0x14]));
+ assert.strictEqual(value.length, 24);
+ return value.subarray(4);
+}
+
+function baseOptions(keys, overrides = {}) {
+ return {
+ subject: [{ name: 'CN', value: 'example.com' }],
+ publicKey: keys.publicKey,
+ serialNumber: 1n,
+ notBefore,
+ notAfter,
+ signature: {
+ key: keys.privateKey,
+ digest: keys.publicKey.asymmetricKeyType === 'ed25519' ?
+ null : 'sha256',
+ },
+ ...overrides,
+ };
+}
+
+function assertSigned(certificate, publicKey, privateKey) {
+ assert(certificate instanceof X509Certificate);
+ assert.strictEqual(certificate.verify(publicKey), true);
+ if (privateKey !== undefined)
+ assert.strictEqual(certificate.checkPrivateKey(privateKey), true);
+}
+
+function assertSynchronousFailure(options, expected) {
+ assert.throws(() => generateX509CertificateSync(options), expected);
+ assert.throws(() => generateX509Certificate(
+ options, common.mustNotCall()), expected);
+}
+
+// This certificate has a valid empty subject with a critical SAN, but it cannot
+// supply the nonempty issuer name required when generating another certificate.
+const emptySubjectIssuer = new X509Certificate(`-----BEGIN CERTIFICATE-----
+MIIBDTCBtaADAgECAgEBMAoGCCqGSM49BAMCMAAwHhcNMjUwMTAxMDAwMDAwWhcN
+MzAwMTAxMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3AsDY6Mw
+cXKGY4R96+CAApyMKVVxUj/vkSU05Fo//J1cC32pINc7r3DSCEgbcbSgl5dau7wm
+62/uYVKbS3Uwm6MgMB4wHAYDVR0RAQH/BBIwEIIOaXNzdWVyLmV4YW1wbGUwCgYI
+KoZIzj0EAwIDRwAwRAIhAM4l65D0ooMAWBw8LdoN/mGSEM7lQhCawDxHRN+Dqzl6
+Ah97+DipkjdevW5E+ACvluxAP30ZJCjMak2QtuQjyC7b
+-----END CERTIFICATE-----`);
+assert.strictEqual(emptySubjectIssuer.subject, '');
+
+const caKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+const ca = generateX509CertificateSync(baseOptions(caKeys, {
+ subject: [
+ [
+ { name: 'O', value: 'Node.js' },
+ { name: 'OU', value: 'Crypto' },
+ ],
+ { name: 'C', value: 'CZ' },
+ { name: '1.2.3.4', value: 'custom' },
+ ],
+ serialNumber: (1n << 159n) - 1n,
+ extensions: [
+ { type: 'basicConstraints', ca: true, pathLength: 1 },
+ { type: 'keyUsage', usages: ['keyCertSign', 'crlSign'], critical: true },
+ { type: 'authorityKeyIdentifier', value: 'auto' },
+ { type: 'subjectKeyIdentifier', value: 'auto' },
+ ],
+}));
+
+assert(process.moduleLoadList.includes(generationModule));
+assertSigned(ca, caKeys.publicKey, caKeys.privateKey);
+assert.strictEqual(ca.ca, true);
+assert.strictEqual(ca.issuerCertificate, undefined);
+assert.strictEqual(ca.validFromDate.toISOString(), '2025-01-02T03:04:05.000Z');
+assert.strictEqual(ca.validToDate.toISOString(), '2050-01-02T03:04:05.000Z');
+assert.strictEqual(
+ ca.subject,
+ 'OU=Crypto + O=Node.js\nC=CZ\n1.2.3.4=custom',
+);
+assert.strictEqual(ca.issuer, ca.subject);
+assert.deepStrictEqual(
+ getAuthorityKeyIdentifier(ca), getSubjectKeyIdentifier(ca));
+
+{
+ const expectedSubject = ca.subjectName.raw;
+ const backing = Buffer.concat([
+ Buffer.from([0xff]),
+ expectedSubject,
+ Buffer.from([0xff]),
+ ]);
+ const subject = new DataView(
+ backing.buffer, backing.byteOffset + 1, expectedSubject.length);
+ const certificate = generateX509CertificateSync(baseOptions(caKeys, {
+ subject,
+ }));
+ assert.deepStrictEqual(certificate.subjectName.raw, expectedSubject);
+ assert.deepStrictEqual(certificate.issuerName.raw, expectedSubject);
+}
+
+// RFC 7093 method 1 hashes only the subjectPublicKey BIT STRING contents and
+// truncates the SHA-256 digest to its leftmost 160 bits.
+{
+ const publicKey = createPublicKey({
+ key: Buffer.from(
+ '3059301306072a8648ce3d020106082a8648ce3d030107034200' +
+ '047f7f35a79794c950060b8029fc8f363a28f11159692d9d34' +
+ 'e6ac948190434735f833b1a66652dc514337aff7f5c9c75d67' +
+ '0c019d95a5d639b72744c64a9128bb',
+ 'hex'),
+ format: 'der',
+ type: 'spki',
+ });
+ const certificate = generateX509CertificateSync(baseOptions(
+ { publicKey, privateKey: caKeys.privateKey },
+ {
+ issuer: ca,
+ signature: { key: caKeys.privateKey, digest: 'sha256' },
+ extensions: [{ type: 'subjectKeyIdentifier', value: 'auto' }],
+ },
+ ));
+ assert.deepStrictEqual(
+ getSubjectKeyIdentifier(certificate),
+ Buffer.from('bf37b3e5808fd46d54b28e846311bcce1cad2e1a', 'hex'),
+ );
+}
+
+const leafKeys = generateKeyPairSync('rsa', { modulusLength: 2048 });
+const rawValue = Buffer.from('deadbeefcafebabe', 'hex');
+const leaf = generateX509CertificateSync(baseOptions(leafKeys, {
+ issuer: ca,
+ serialNumber: 2n,
+ signature: {
+ key: caKeys.privateKey,
+ digest: 'sha256',
+ },
+ extensions: [
+ { type: 'basicConstraints', ca: false },
+ {
+ type: 'keyUsage',
+ usages: ['digitalSignature', 'keyEncipherment'],
+ critical: true,
+ },
+ {
+ type: 'extendedKeyUsage',
+ usages: ['serverAuth', '1.2.3.5'],
+ },
+ {
+ type: 'subjectAltName',
+ names: [
+ { type: 'dns', value: 'example.com' },
+ { type: 'ip', value: '192.0.2.1' },
+ { type: 'ip', value: '2001:db8::1' },
+ { type: 'email', value: 'local@example.com' },
+ { type: 'uri', value: 'https://example.com/path' },
+ ],
+ },
+ { type: 'subjectKeyIdentifier', value: 'auto' },
+ { type: 'authorityKeyIdentifier', value: 'auto' },
+ { type: 'raw', oid: '1.2.3.8', critical: true, value: rawValue },
+ { type: 'raw', oid: '2.5.29.18', value: Buffer.from('3000', 'hex') },
+ ],
+}));
+
+assertSigned(leaf, caKeys.publicKey, leafKeys.privateKey);
+assert.strictEqual(leaf.checkIssued(ca), true);
+assert.strictEqual(leaf.issuerCertificate, undefined);
+const clonedLeaf = structuredClone(leaf);
+assert.deepStrictEqual(clonedLeaf.raw, leaf.raw);
+assert.strictEqual(clonedLeaf.issuerCertificate, undefined);
+assert.strictEqual(leaf.checkHost('example.com'), 'example.com');
+assert.strictEqual(leaf.checkIP('192.0.2.1'), '192.0.2.1');
+assert.strictEqual(leaf.checkIP('2001:db8::1'), '2001:db8::1');
+assert.strictEqual(leaf.checkEmail('local@example.com'), 'local@example.com');
+assert.match(leaf.subjectAltName, /URI:https:\/\/example\.com\/path/);
+assert.deepStrictEqual(
+ leaf.keyUsage, ['1.3.6.1.5.5.7.3.1', '1.2.3.5']);
+assert(leaf.raw.includes(rawValue));
+assert.deepStrictEqual(
+ getAuthorityKeyIdentifier(leaf), getSubjectKeyIdentifier(ca));
+
+// Binary typed values are identifier bytes rather than DER. BufferSource views
+// are copied and wrapped by the crypto backend.
+{
+ const skiBacking = Uint8Array.from([0xff, 1, 2, 3, 4, 5, 0xff]);
+ const akiBacking = Uint8Array.from([0xff, 6, 7, 8, 9, 10, 0xff]);
+ const certificate = generateX509CertificateSync(baseOptions(leafKeys, {
+ extensions: [
+ {
+ type: 'subjectKeyIdentifier',
+ value: new DataView(skiBacking.buffer, 1, 5),
+ },
+ {
+ type: 'authorityKeyIdentifier',
+ value: new Uint8Array(akiBacking.buffer, 1, 5),
+ },
+ ],
+ }));
+ assert.deepStrictEqual(
+ findExtension(certificate, subjectKeyIdentifierOid),
+ {
+ oid: subjectKeyIdentifierOid,
+ critical: false,
+ value: Buffer.from('04050102030405', 'hex'),
+ },
+ );
+ assert.deepStrictEqual(
+ findExtension(certificate, authorityKeyIdentifierOid),
+ {
+ oid: authorityKeyIdentifierOid,
+ critical: false,
+ value: Buffer.from('30078005060708090a', 'hex'),
+ },
+ );
+
+ const copied = generateX509CertificateSync(baseOptions(leafKeys, {
+ extensions: [
+ { type: 'authorityKeyIdentifier', value: 'auto' },
+ {
+ type: 'subjectKeyIdentifier',
+ value: new DataView(skiBacking.buffer, 1, 5),
+ },
+ ],
+ }));
+ assert.deepStrictEqual(
+ findExtension(copied, authorityKeyIdentifierOid).value,
+ Buffer.from('300780050102030405', 'hex'),
+ );
+}
+
+// An automatic AKI copies an explicitly selected issuer SKI instead of
+// replacing the issuer's identifier convention with RFC 7093 method 1.
+{
+ const identifier = Buffer.from('a1a2a3a4a5', 'hex');
+ const issuer = generateX509CertificateSync(baseOptions(caKeys, {
+ subject: [{ name: 'CN', value: 'Explicit identifier issuer' }],
+ extensions: [{ type: 'subjectKeyIdentifier', value: identifier }],
+ }));
+ const certificate = generateX509CertificateSync(baseOptions(leafKeys, {
+ issuer,
+ signature: { key: caKeys.privateKey, digest: 'sha256' },
+ extensions: [{ type: 'authorityKeyIdentifier', value: 'auto' }],
+ }));
+ assert.deepStrictEqual(
+ findExtension(certificate, authorityKeyIdentifierOid).value,
+ Buffer.concat([Buffer.from([0x30, 0x07, 0x80, identifier.length]),
+ identifier]),
+ );
+
+ const issuerWithoutIdentifier = generateX509CertificateSync(baseOptions(
+ caKeys,
+ {
+ subject: [{ name: 'CN', value: 'Issuer without an identifier' }],
+ extensions: [],
+ },
+ ));
+ const derived = generateX509CertificateSync(baseOptions(leafKeys, {
+ issuer: issuerWithoutIdentifier,
+ signature: { key: caKeys.privateKey, digest: 'sha256' },
+ extensions: [{ type: 'authorityKeyIdentifier', value: 'auto' }],
+ }));
+ assert.deepStrictEqual(
+ getAuthorityKeyIdentifier(derived), getSubjectKeyIdentifier(ca));
+
+ const malformedIssuer = generateX509CertificateSync(baseOptions(caKeys, {
+ subject: [{ name: 'CN', value: 'Malformed identifier issuer' }],
+ extensions: [{
+ type: 'raw',
+ oid: subjectKeyIdentifierOid,
+ value: Buffer.from('0500', 'hex'),
+ }],
+ }));
+ assertSynchronousFailure(baseOptions(leafKeys, {
+ issuer: malformedIssuer,
+ signature: { key: caKeys.privateKey, digest: 'sha256' },
+ extensions: [{ type: 'authorityKeyIdentifier', value: 'auto' }],
+ }), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: /issuer has an invalid Subject Key Identifier/,
+ });
+}
+
+// Basic Constraints do not have to be critical merely because cA is true.
+assertSigned(generateX509CertificateSync(baseOptions(caKeys, {
+ extensions: [
+ { type: 'basicConstraints', ca: true },
+ { type: 'keyUsage', usages: ['keyCertSign'] },
+ ],
+})), caKeys.publicKey, caKeys.privateKey);
+
+const emptySubject = generateX509CertificateSync(baseOptions(leafKeys, {
+ subject: [],
+ issuer: ca,
+ signature: {
+ key: caKeys.privateKey,
+ digest: 'sha256',
+ },
+ extensions: [{
+ type: 'subjectAltName',
+ critical: true,
+ names: [{ type: 'dns', value: 'empty-subject.example' }],
+ }],
+}));
+assertSigned(emptySubject, caKeys.publicKey, leafKeys.privateKey);
+assert.strictEqual(emptySubject.subject, '');
+assert.strictEqual(emptySubject.issuer, ca.subject);
+
+const encodedEmptySubject = generateX509CertificateSync(baseOptions(leafKeys, {
+ subject: Buffer.from('3000', 'hex'),
+ issuer: ca,
+ signature: {
+ key: caKeys.privateKey,
+ digest: 'sha256',
+ },
+ extensions: [{
+ type: 'subjectAltName',
+ critical: true,
+ names: [{ type: 'dns', value: 'empty-subject.example' }],
+ }],
+}));
+assert.deepStrictEqual(
+ encodedEmptySubject.subjectName.raw, Buffer.from('3000', 'hex'));
+
+// DNS, email, and URI inputs receive encoding validation, not application
+// syntax or policy validation.
+assertSigned(generateX509CertificateSync(baseOptions(caKeys, {
+ subject: [{ name: 'DC', value: 'not a domain label' }],
+ extensions: [{
+ type: 'subjectAltName',
+ names: [
+ { type: 'dns', value: 'not a dns name' },
+ { type: 'email', value: 'not-an-email-address' },
+ { type: 'uri', value: 'relative uri value' },
+ ],
+ }],
+})), caKeys.publicKey, caKeys.privateKey);
+
+for (const [type, options] of [
+ ['ec', { namedCurve: 'P-256' }],
+ ...hasFIPS() ? [] : [['ed25519', {}]],
+]) {
+ const keys = generateKeyPairSync(type, options);
+ assertSigned(
+ generateX509CertificateSync(baseOptions(keys)),
+ keys.publicKey,
+ keys.privateKey,
+ );
+}
+
+{
+ const keys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+ const passphrase = 'secret';
+ const encryptedPrivateKey = keys.privateKey.export({
+ type: 'pkcs8',
+ format: 'pem',
+ cipher: 'aes-256-cbc',
+ passphrase,
+ });
+ const preparedKeys = [
+ [
+ keys.publicKey.export({ type: 'spki', format: 'pem' }),
+ keys.privateKey.export({ type: 'pkcs8', format: 'pem' }),
+ ],
+ [
+ {
+ key: keys.publicKey.export({ type: 'spki', format: 'der' }),
+ type: 'spki',
+ format: 'der',
+ },
+ {
+ key: keys.privateKey.export({ type: 'pkcs8', format: 'der' }),
+ type: 'pkcs8',
+ format: 'der',
+ },
+ ],
+ [
+ keys.privateKey,
+ { key: encryptedPrivateKey, passphrase },
+ ],
+ [
+ { key: keys.publicKey.export({ format: 'jwk' }), format: 'jwk' },
+ { key: keys.privateKey.export({ format: 'jwk' }), format: 'jwk' },
+ ],
+ ];
+
+ for (const [publicKey, key] of preparedKeys) {
+ const certificate = generateX509CertificateSync(baseOptions(keys, {
+ publicKey,
+ signature: { key, digest: 'sha256' },
+ }));
+ assertSigned(certificate, keys.publicKey, keys.privateKey);
+ }
+
+ assertSynchronousFailure(baseOptions(keys, {
+ publicKey: 'not a key',
+ }), Error);
+ assertSynchronousFailure(baseOptions(keys, {
+ signature: { key: keys.publicKey, digest: 'sha256' },
+ }), { code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE' });
+ assertSynchronousFailure(baseOptions(keys, {
+ signature: {
+ key: { key: encryptedPrivateKey, passphrase: 'wrong' },
+ digest: 'sha256',
+ },
+ }), Error);
+ assertSynchronousFailure(baseOptions(keys, {
+ signature: {
+ key: { key: keys.publicKey.export({ format: 'jwk' }), format: 'jwk' },
+ digest: 'sha256',
+ },
+ }), { code: 'ERR_INVALID_ARG_VALUE' });
+}
+
+assertSigned(generateX509CertificateSync(baseOptions(leafKeys, {
+ extensions: [{
+ type: 'keyUsage',
+ usages: ['encipherOnly', 'decipherOnly'],
+ }],
+})), leafKeys.publicKey, leafKeys.privateKey);
+
+{
+ const certificate = generateX509CertificateSync(baseOptions(leafKeys, {
+ signature: {
+ key: {
+ key: leafKeys.privateKey,
+ padding: constants.RSA_PKCS1_PSS_PADDING,
+ saltLength: 32,
+ },
+ digest: 'sha256',
+ },
+ }));
+ assertSigned(certificate, leafKeys.publicKey, leafKeys.privateKey);
+ assert.strictEqual(certificate.signatureAlgorithmOid, '1.2.840.113549.1.1.10');
+}
+
+if (!isBoringSSL) {
+ const keys = generateKeyPairSync('rsa-pss', {
+ modulusLength: 2048,
+ hashAlgorithm: 'sha256',
+ mgf1HashAlgorithm: 'sha256',
+ saltLength: 64,
+ });
+ const certificate = generateX509CertificateSync(baseOptions(keys));
+ assertSigned(certificate, keys.publicKey, keys.privateKey);
+ assert.strictEqual(certificate.signatureAlgorithmOid, '1.2.840.113549.1.1.10');
+}
+
+assert.strictEqual(generateX509Certificate.length, 2);
+assert.strictEqual(generateX509CertificateSync.length, 1);
+
+const invalidOptions = [
+ ['ERR_OUT_OF_RANGE', baseOptions(leafKeys, { serialNumber: 0n })],
+ ['ERR_OUT_OF_RANGE', baseOptions(leafKeys, { serialNumber: 1n << 159n })],
+ ['ERR_OUT_OF_RANGE', baseOptions(leafKeys, {
+ notBefore: new Date('1949-12-31T23:59:59Z'),
+ })],
+ ['ERR_OUT_OF_RANGE', baseOptions(leafKeys, {
+ notAfter: new Date(253402300800000),
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ notBefore: new Date('2025-01-01T00:00:00.100Z'),
+ notAfter: new Date('2025-01-01T00:00:00.900Z'),
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, { subject: [] })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: Buffer.alloc(0),
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: Buffer.from('3001', 'hex'),
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: Buffer.from('3100', 'hex'),
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: Buffer.concat([encodedSubject, Buffer.from([0])]),
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: Buffer.from('3000', 'hex'),
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: [],
+ issuer: ca,
+ signature: { key: caKeys.privateKey, digest: 'sha256' },
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'dns', value: 'example.com' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: [],
+ issuer: ca,
+ signature: { key: caKeys.privateKey, digest: 'sha256' },
+ extensions: [{
+ type: 'raw',
+ oid: '2.5.29.17',
+ critical: true,
+ value: Buffer.from('300d820b6578616d706c652e636f6d', 'hex'),
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: [{ name: 'C', value: 'CZE' }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ subject: [{ name: '2.5.4.6', value: 'CZE' }],
+ })],
+ ['ERR_MISSING_OPTION', baseOptions(leafKeys, {
+ subject: [{ oid: '1.2.3.4', raw: Buffer.from('0c00', 'hex') }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{ type: 'keyUsage', usages: ['keyCertSign'] }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{ type: 'basicConstraints', ca: true, pathLength: 0 }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [
+ { type: 'basicConstraints', ca: false },
+ {
+ type: 'raw',
+ oid: '2.5.29.19',
+ value: Buffer.from('3000', 'hex'),
+ },
+ ],
+ })],
+ ['ERR_INVALID_ARG_TYPE', baseOptions(leafKeys, {
+ extensions: [{ type: 'raw', oid: '1.2.3.4', value: '0500' }],
+ })],
+ ['ERR_MISSING_OPTION', baseOptions(leafKeys, {
+ extensions: [{ type: 'subjectKeyIdentifier' }],
+ })],
+ ['ERR_INVALID_ARG_TYPE', baseOptions(leafKeys, {
+ extensions: [{ type: 'authorityKeyIdentifier', value: 'sha256' }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [
+ { type: 'subjectKeyIdentifier', value: 'auto' },
+ {
+ type: 'raw',
+ oid: subjectKeyIdentifierOid,
+ value: Buffer.from('040101', 'hex'),
+ },
+ ],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [
+ {
+ type: 'raw',
+ oid: authorityKeyIdentifierOid,
+ value: Buffer.from('3003800101', 'hex'),
+ },
+ { type: 'authorityKeyIdentifier', value: 'auto' },
+ ],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'directoryName', value: [] }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'dns', value: 'b\u00fccher.example' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'dns', value: 'nul\0.example' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'dns', value: '' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'email', value: '' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'uri', value: '' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'ip', value: '192.0.2.999' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ extensions: [{
+ type: 'subjectAltName',
+ names: [{ type: 'ip', value: 'fe80::1%eth0' }],
+ }],
+ })],
+ ['ERR_INVALID_ARG_VALUE', baseOptions(leafKeys, {
+ signature: {
+ key: leafKeys.privateKey,
+ digest: null,
+ },
+ })],
+];
+
+for (const [code, options] of invalidOptions) {
+ assert.throws(() => generateX509CertificateSync(options), { code });
+}
+
+assert.throws(() => generateX509CertificateSync(baseOptions(leafKeys, {
+ subject: [],
+ extensions: [{
+ type: 'subjectAltName',
+ critical: true,
+ names: [{ type: 'dns', value: 'self-signed.example' }],
+ }],
+})), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: /self-signed X\.509 certificate must have a non-empty subject/,
+});
+
+assert.throws(() => generateX509CertificateSync(baseOptions(leafKeys, {
+ issuer: emptySubjectIssuer,
+})), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: /"options\.issuer" certificate must have a non-empty subject/,
+});
+
+assert.throws(() => generateX509Certificate({}, undefined), {
+ code: 'ERR_INVALID_ARG_TYPE',
+});
+
+assert.throws(() => generateX509Certificate(baseOptions(leafKeys, {
+ extensions: [
+ { type: 'basicConstraints', ca: false },
+ { type: 'basicConstraints', ca: false },
+ ],
+}), common.mustNotCall()), {
+ code: 'ERR_INVALID_ARG_VALUE',
+});
+
+for (const [oid, value] of [
+ ['2.5.29.19', Buffer.from('3000', 'hex')],
+ ['2.5.29.14', Buffer.from('0400', 'hex')],
+ ['2.5.29.35', Buffer.from('3001', 'hex')],
+ ['2.5.29.35', Buffer.from('30000500', 'hex')],
+]) {
+ const options = baseOptions(leafKeys, {
+ issuer: ca,
+ signature: {
+ key: caKeys.privateKey,
+ digest: 'sha256',
+ },
+ extensions: [{
+ type: 'raw',
+ oid,
+ value,
+ }],
+ });
+ const backendMayReject = value.length === 2 &&
+ (oid === '2.5.29.14' || oid === '2.5.29.35');
+ let certificate;
+ try {
+ certificate = generateX509CertificateSync(options);
+ } catch (error) {
+ if (!backendMayReject) throw error;
+ assert.strictEqual(error.code, 'ERR_CRYPTO_UNSUPPORTED_OPERATION');
+ assert.throws(() => generateX509Certificate(
+ options, common.mustNotCall()), {
+ code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION',
+ });
+ continue;
+ }
+ assertSigned(certificate, caKeys.publicKey, leafKeys.privateKey);
+ assert.deepStrictEqual(certificate.extensions, [{
+ oid,
+ critical: false,
+ value,
+ }]);
+}
+
+const mismatchedKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+assert.throws(() => generateX509Certificate(
+ baseOptions(leafKeys, {
+ signature: {
+ key: mismatchedKeys.privateKey,
+ digest: 'sha256',
+ },
+ }),
+ common.mustNotCall(),
+), {
+ code: 'ERR_INVALID_ARG_VALUE',
+});
+
+(async () => {
+ const mutableName = { name: 'CN', value: 'before.example.com' };
+ const expectedRaw = Buffer.from('0408feedfacedeadbeef', 'hex');
+ const mutableRaw = Buffer.from(expectedRaw);
+ const expectedIdentifier = Buffer.from('0102030405', 'hex');
+ const mutableIdentifier = Buffer.from(expectedIdentifier);
+ const options = baseOptions(leafKeys, {
+ subject: [mutableName],
+ issuer: ca,
+ signature: {
+ key: caKeys.privateKey,
+ digest: 'sha256',
+ },
+ extensions: [
+ { type: 'raw', oid: '1.2.3.9', value: mutableRaw },
+ { type: 'subjectKeyIdentifier', value: mutableIdentifier },
+ ],
+ });
+ const pending = generateX509CertificateAsync(options);
+ mutableName.value = 'after.example.com';
+ mutableRaw.fill(0);
+ mutableIdentifier.fill(0);
+ options.extensions.length = 0;
+ const generated = await pending;
+ assert.strictEqual(generated.subject, 'CN=before.example.com');
+ assert(generated.raw.includes(expectedRaw));
+ assert.deepStrictEqual(
+ findExtension(generated, subjectKeyIdentifierOid).value,
+ Buffer.concat([Buffer.from([0x04, expectedIdentifier.length]),
+ expectedIdentifier]),
+ );
+ assert.strictEqual(generated.issuerCertificate, undefined);
+
+ const mutableSubject = Buffer.from(encodedSubject);
+ const encodedSubjectPending = generateX509CertificateAsync(baseOptions(
+ leafKeys,
+ { subject: mutableSubject },
+ ));
+ mutableSubject.fill(0);
+ const encodedSubjectCertificate = await encodedSubjectPending;
+ assert.deepStrictEqual(
+ encodedSubjectCertificate.subjectName.raw, encodedSubject);
+
+ const encodedKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+ const publicKey = encodedKeys.publicKey.export({
+ type: 'spki',
+ format: 'der',
+ });
+ const privateKey = encodedKeys.privateKey.export({
+ type: 'pkcs8',
+ format: 'der',
+ });
+ const encodedKeyPending = generateX509CertificateAsync(baseOptions(
+ encodedKeys,
+ {
+ publicKey: { key: publicKey, type: 'spki', format: 'der' },
+ signature: {
+ key: { key: privateKey, type: 'pkcs8', format: 'der' },
+ digest: 'sha256',
+ },
+ },
+ ));
+ publicKey.fill(0);
+ privateKey.fill(0);
+ const encodedKeyCertificate = await encodedKeyPending;
+ assertSigned(
+ encodedKeyCertificate,
+ encodedKeys.publicKey,
+ encodedKeys.privateKey,
+ );
+
+ const callbackResult = await new Promise((resolve, reject) => {
+ generateX509Certificate(baseOptions(leafKeys, {
+ issuer: ca,
+ signature: {
+ key: caKeys.privateKey,
+ digest: 'sha256',
+ },
+ }), (error, certificate) => {
+ if (error) return reject(error);
+ resolve(certificate);
+ });
+ });
+ assertSigned(callbackResult, caKeys.publicKey, leafKeys.privateKey);
+ assert.strictEqual(callbackResult.issuerCertificate, undefined);
+
+ if (!hasFIPS()) {
+ const unsupportedKeys = generateKeyPairSync('x25519');
+ await new Promise((resolve) => {
+ generateX509Certificate(baseOptions(unsupportedKeys), common.mustCall(
+ (error, certificate) => {
+ assert(error instanceof Error);
+ assert.strictEqual(certificate, undefined);
+ resolve();
+ }));
+ });
+ }
+
+ const esm = await import('node:crypto');
+ assert.strictEqual(esm.generateX509Certificate, generateX509Certificate);
+ assert.strictEqual(
+ esm.generateX509CertificateSync, generateX509CertificateSync);
+})().then(common.mustCall());
diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts
index 1ff5a8b1ed4..63f2f76bd5a 100644
--- a/typings/internalBinding/crypto.d.ts
+++ b/typings/internalBinding/crypto.d.ts
@@ -522,6 +522,59 @@ declare namespace InternalCryptoBinding {
): CryptoJobForMode<M, SignJobResult<S>>;
}
+ type X509NameAttributeSpec = [
+ oid: string,
+ value: string,
+ ];
+ type X509NameSpec = X509NameAttributeSpec[][] | BufferSource;
+
+ type X509GeneralNameSpec = [
+ type: 0 | 1 | 2 | 4,
+ value: string,
+ ];
+
+ type X509ExtensionSpec =
+ | [type: 0, oid: string, critical: boolean, value: BufferSource]
+ | [type: 1, critical: boolean, ca: boolean, pathLength: number | undefined]
+ | [type: 2, critical: boolean, usages: number]
+ | [type: 3, critical: boolean, usages: string[]]
+ | [type: 4, critical: boolean, names: X509GeneralNameSpec[]]
+ | [type: 5 | 6, critical: false, identifier: BufferSource | undefined];
+
+ type X509GenerateCertificateArgs =
+ | [
+ csr: undefined,
+ subject: X509NameSpec,
+ ...publicKey: PreparedAsymmetricKeyArgs,
+ ]
+ | [
+ csr: BufferSource,
+ subject: undefined,
+ keyData: undefined,
+ keyFormat: undefined,
+ keyType: undefined,
+ keyPassphrase: undefined,
+ keyNamedCurve: undefined,
+ ];
+
+ interface X509GenerateJobConstructor {
+ new<M extends CryptoJobRegularMode>(
+ mode: M,
+ ...args: [
+ ...certificate: X509GenerateCertificateArgs,
+ issuer: X509CertificateHandle | undefined,
+ serialBytes: BufferSource,
+ notBefore: string,
+ notAfter: string,
+ extensions: X509ExtensionSpec[],
+ ...signingKey: PreparedAsymmetricKeyArgs,
+ digest: string | null,
+ saltLength: number | undefined,
+ padding: number | undefined,
+ ]
+ ): CryptoJobForMode<M, ArrayBuffer>;
+ }
+
interface TurboShakeJobConstructor {
new(
mode: CryptoJobWebCryptoMode,
@@ -860,6 +913,7 @@ export interface CryptoBinding {
SecretKeyGenJob: InternalCryptoBinding.SecretKeyGenJobConstructor;
SignJob: InternalCryptoBinding.SignJobConstructor;
TurboShakeJob: InternalCryptoBinding.TurboShakeJobConstructor;
+ X509GenerateJob: InternalCryptoBinding.X509GenerateJobConstructor;
CipherBase: new (
isEncrypt: boolean,
@@ -1032,6 +1086,12 @@ export interface CryptoBinding {
passphrase?: InternalCryptoBinding.ByteSource,
): InternalCryptoBinding.PKCS12ParseResult;
parseX509(data: InternalCryptoBinding.ByteSource): InternalCryptoBinding.X509CertificateHandle;
+ parseX509CertificateRequest(
+ data: InternalCryptoBinding.BufferSource,
+ ): [
+ der: InternalCryptoBinding.Buffer,
+ requestedExtensions: InternalCryptoBinding.X509RawExtension[],
+ ];
parseX509Snapshot(
snapshot: ArrayBufferView[],
): InternalCryptoBinding.X509CertificateHandle;