Commit bd08ba304ea for nodejs
commit bd08ba304ea8cb7f5f3e8fcc9c5bfedd14c9ed34
Author: Filip Skokan <panva.ip@gmail.com>
Date: Sat Nov 14 19:00:51 2026 +0100
crypto: throw on X509Certificate operation errors
X509Certificate can report failed operations as missing fields or
negative checks. Invalid validity times return "Bad time value" or
Invalid Date, making relational date comparisons return false.
Throw when required fields cannot be converted, certificate encoding or
fingerprint calculation fails, or a present AIA or EKU extension cannot
be decoded. Apply this to getters and explicit toLegacyObject() calls.
Report failed or unsupported private-key comparisons as exceptions.
For verify(), return true for 1 and false for 0. Also return false for a
wrong key type or invalid unused bits in the signature BIT STRING.
Throw for other negative results.
Use the existing conversion behavior for legacy TLS certificate objects,
which are also constructed internally during handshakes. Formatting
unrelated metadata must not cause an uncaught exception or reject an
otherwise valid connection.
Fixes: https://github.com/nodejs/node/issues/67285
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/67408
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index ba800fa816c..8fcabdf00d0 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -9,6 +9,7 @@
#include <openssl/rand.h>
#include <openssl/x509v3.h>
#if NCRYPTO_USE_OPENSSL_PROVIDER
+#include <openssl/asn1err.h>
#include <openssl/decoder.h>
#endif
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
@@ -1484,13 +1485,15 @@ BIOPointer X509View::getIssuer() const {
return bio;
}
-BIOPointer X509View::getInfoAccess() const {
+BIOPointer X509View::getInfoAccess(bool* has_extension) const {
MarkPopErrorOnReturn mark_pop_error_on_return;
+ if (has_extension != nullptr) *has_extension = false;
if (cert_ == nullptr) return {};
+ const int index = X509_get_ext_by_NID(cert_, NID_info_access, -1);
+ if (index < 0) return {};
+ if (has_extension != nullptr) *has_extension = true;
BIOPointer bio(BIO_new(BIO_s_mem()));
if (!bio) return {};
- int index = X509_get_ext_by_NID(cert_, NID_info_access, -1);
- if (index < 0) return {};
const X509_EXTENSION* extension = X509_get_ext(cert_, index);
if (extension == nullptr || !SafeX509InfoAccessPrint(bio, extension)) {
return {};
@@ -1850,10 +1853,22 @@ X509View::CheckKeyResult X509View::checkPrivateKey(
}
}
-bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const {
+Result<bool, int> X509View::checkPublicKey(const EVPKeyPointer& pkey) const {
ClearErrorOnReturn clearErrorOnReturn;
- if (cert_ == nullptr || pkey == nullptr) return false;
- return X509_verify(const_cast<X509*>(cert_), pkey.get()) == 1;
+ if (cert_ == nullptr || pkey == nullptr) return Result<bool, int>(-1);
+ const int result = X509_verify(const_cast<X509*>(cert_), pkey.get());
+ if (result >= 0) return Result<bool, int>(result == 1);
+
+ const auto error = ERR_peek_last_error();
+#if NCRYPTO_USE_OPENSSL_PROVIDER
+ // A wrong key type or malformed signature BIT STRING cannot verify.
+ if (ERR_GET_LIB(error) == ERR_LIB_ASN1 &&
+ (ERR_GET_REASON(error) == ASN1_R_WRONG_PUBLIC_KEY_TYPE ||
+ ERR_GET_REASON(error) == ASN1_R_INVALID_BIT_STRING_BITS_LEFT)) {
+ return Result<bool, int>(false);
+ }
+#endif
+ return Result<bool, int>(int{result}, error);
}
X509View::CheckMatch X509View::checkHost(const std::string_view host,
@@ -1972,15 +1987,19 @@ Result<X509Pointer, int> X509Pointer::Parse(
return Result<X509Pointer, int>(ERR_get_error());
}
-bool X509View::enumUsages(UsageCallback callback) const {
- if (cert_ == nullptr) return false;
+X509View::UsageResult X509View::enumUsages(UsageCallback callback) const {
+ if (cert_ == nullptr) return UsageResult::OPERATION_FAILED;
StackOfASN1 eku;
+ int critical = -1;
{
MarkPopErrorOnReturn mark_pop_error_on_return;
eku.reset(static_cast<STACK_OF(ASN1_OBJECT)*>(
- X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr)));
+ X509_get_ext_d2i(cert_, NID_ext_key_usage, &critical, nullptr)));
+ }
+ if (!eku) {
+ // The decoder reports absence as -1 and duplicate extensions as -2.
+ return critical == -1 ? UsageResult::ABSENT : UsageResult::OPERATION_FAILED;
}
- if (!eku) return false;
const int count = sk_ASN1_OBJECT_num(eku.get());
for (int i = 0; i < count; i++) {
@@ -1989,10 +2008,10 @@ bool X509View::enumUsages(UsageCallback callback) const {
MarkPopErrorOnReturn mark_pop_error_on_return;
usage = ObjectToText(sk_ASN1_OBJECT_value(eku.get(), i), true);
}
- if (!usage.has_value()) return false;
+ if (!usage.has_value()) return UsageResult::OPERATION_FAILED;
callback(usage->c_str());
}
- return true;
+ return UsageResult::PRESENT;
}
bool X509View::ifRsa(KeyCallback<Rsa> callback) const {
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index 6e38add4a8b..c814cc1acdf 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -1731,7 +1731,7 @@ class X509View final {
BIOPointer getSubject() const;
BIOPointer getSubjectAltName(bool* has_extension = nullptr) const;
BIOPointer getIssuer() const;
- BIOPointer getInfoAccess() const;
+ BIOPointer getInfoAccess(bool* has_extension = nullptr) const;
BIOPointer getValidFrom() const;
BIOPointer getValidTo() const;
std::optional<std::string_view> getSignatureAlgorithm() const;
@@ -1750,7 +1750,7 @@ class X509View final {
bool isCA() const;
bool isIssuedBy(const X509View& other) const;
CheckKeyResult checkPrivateKey(const EVPKeyPointer& pkey) const;
- bool checkPublicKey(const EVPKeyPointer& pkey) const;
+ Result<bool, int> checkPublicKey(const EVPKeyPointer& pkey) const;
std::optional<std::string> getFingerprint(const Digest& method) const;
@@ -1768,8 +1768,13 @@ class X509View final {
CheckMatch checkEmail(std::string_view email, int flags) const;
CheckMatch checkIp(const Buffer<const unsigned char>& ip, int flags) const;
+ enum class UsageResult {
+ ABSENT,
+ PRESENT,
+ OPERATION_FAILED,
+ };
using UsageCallback = std::function<void(const char*)>;
- bool enumUsages(UsageCallback callback) const;
+ UsageResult enumUsages(UsageCallback callback) const;
template <typename T>
using KeyCallback = std::function<bool(const T& t)>;
diff --git a/doc/api/crypto.md b/doc/api/crypto.md
index 54a2cbef52b..4d4226b2271 100644
--- a/doc/api/crypto.md
+++ b/doc/api/crypto.md
@@ -2967,6 +2967,11 @@ be passed instead of a public key.
<!-- YAML
added: v15.6.0
+changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/67408
+ description: Required field conversion, certificate encoding, fingerprint
+ calculation, and AIA or EKU decoding failures now throw.
-->
Encapsulates an X509 certificate and provides read-only access to
@@ -3144,6 +3149,10 @@ if (!x509.verify(otherCert.publicKey)) {
<!-- YAML
added: v15.6.0
+changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/67408
+ description: Unsupported key comparisons and comparison errors now throw.
-->
* `privateKey` {KeyObject} A private key.
@@ -3152,6 +3161,10 @@ added: v15.6.0
Checks whether the public key for this certificate is consistent with
the given private key.
+Returns `false` if the keys do not match. Throws
+`ERR_CRYPTO_UNSUPPORTED_OPERATION` if the keys cannot be compared,
+or `ERR_CRYPTO_OPERATION_FAILED` if the comparison cannot be completed.
+
### `x509.extensions`
<!-- YAML
@@ -3203,6 +3216,8 @@ added: v15.6.0
The SHA-1 fingerprint of this certificate.
+Throws if the fingerprint cannot be calculated.
+
Because SHA-1 is cryptographically broken and because the security of SHA-1 is
significantly worse than that of algorithms that are commonly used to sign
certificates, consider using [`x509.fingerprint256`][] instead.
@@ -3217,6 +3232,8 @@ added: v15.6.0
The SHA-256 fingerprint of this certificate.
+Throws if the fingerprint cannot be calculated.
+
### `x509.fingerprint512`
<!-- YAML
@@ -3229,6 +3246,8 @@ added:
The SHA-512 fingerprint of this certificate.
+Throws if the fingerprint cannot be calculated.
+
Because computing the SHA-256 fingerprint is usually faster and because it is
only half the size of the SHA-512 fingerprint, [`x509.fingerprint256`][] may be
a better choice. While SHA-512 presumably provides a higher level of security in
@@ -3248,10 +3267,11 @@ changes:
in response to CVE-2021-44532.
-->
-* Type: {string}
+* Type: {string|undefined}
A textual representation of the certificate's authority information access
-extension.
+extension, or `undefined` if the extension is absent. Throws if the extension
+cannot be decoded or formatted.
This is a line feed separated list of access descriptions. Each line begins with
the access method and the kind of the access location, followed by a colon and
@@ -3273,6 +3293,8 @@ added: v15.6.0
The issuer identification included in this certificate.
+Throws if the issuer name cannot be formatted.
+
### `x509.issuerCertificate`
<!-- YAML
@@ -3330,9 +3352,10 @@ issuer name cannot be inspected.
added: v15.6.0
-->
-* Type: {string\[]}
+* Type: {string\[]|undefined}
-An array detailing the key extended usages for this certificate.
+An array of extended key usage OIDs, or `undefined` if the extension is absent.
+Throws if the extension cannot be decoded or an OID cannot be formatted.
### `x509.publicKey`
@@ -3354,6 +3377,8 @@ added: v15.6.0
A `Buffer` containing the DER encoding of this certificate.
+Throws if the certificate cannot be encoded.
+
### `x509.serialNumber`
<!-- YAML
@@ -3364,6 +3389,8 @@ added: v15.6.0
The serial number of this certificate.
+Throws if the serial number cannot be formatted.
+
Serial numbers are assigned by certificate authorities and do not uniquely
identify certificates. Consider using [`x509.fingerprint256`][] as a unique
identifier instead.
@@ -3378,6 +3405,8 @@ added: v15.6.0
The complete subject of this certificate.
+Throws if the subject name cannot be formatted.
+
### `x509.subjectAltName`
<!-- YAML
@@ -3486,6 +3515,8 @@ There is no standard JSON encoding for X509 certificates. The
`toJSON()` method returns a string containing the PEM encoded
certificate.
+Throws if the certificate cannot be encoded.
+
### `x509.toLegacyObject()`
<!-- YAML
@@ -3497,6 +3528,15 @@ added: v15.6.0
Returns information about this certificate using the legacy
[certificate object][] encoding.
+Throws if a required field conversion, Authority Information Access or Extended
+Key Usage decoding, fingerprint calculation, or certificate encoding fails.
+Optional public key fields may be omitted as described in the legacy encoding.
+
+The TLS methods [`tlsSocket.getPeerCertificate()`][] and
+[`tlsSocket.getCertificate()`][] use the legacy conversion behavior:
+unconvertible AIA and EKU extensions are omitted, and invalid validity times
+are returned as `'Bad time value'`.
+
### `x509.toString()`
<!-- YAML
@@ -3507,6 +3547,8 @@ added: v15.6.0
Returns the PEM-encoded certificate.
+Throws if the certificate cannot be encoded.
+
### `x509.validFrom`
<!-- YAML
@@ -3517,8 +3559,7 @@ added: v15.6.0
The date/time from which this certificate is valid.
-If the certificate's `notBefore` time cannot be parsed, this is the string
-`'Bad time value'`.
+Throws if the time cannot be converted or formatted.
### `x509.validFromDate`
@@ -3532,15 +3573,7 @@ added:
The date/time from which this certificate is valid, encapsulated in a `Date` object.
-If the certificate's `notBefore` time cannot be parsed, this property is an
-invalid `Date` whose `getTime()` method returns `NaN`.
-
-Relational comparisons with an invalid `Date` evaluate to `false`. To check
-whether the current time is within the certificate's validity period, use
-`now >= x509.validFromDate && now <= x509.validToDate`. This expression
-evaluates to `false` if either date is invalid. Checking
-`now < x509.validFromDate || now > x509.validToDate` instead can fail to
-reject certificates with invalid dates.
+Throws if the time cannot be converted.
### `x509.validTo`
@@ -3552,8 +3585,7 @@ added: v15.6.0
The date/time until which this certificate is valid.
-If the certificate's `notAfter` time cannot be parsed, this is the string
-`'Bad time value'`.
+Throws if the time cannot be converted or formatted.
### `x509.validToDate`
@@ -3567,15 +3599,7 @@ added:
The date/time until which this certificate is valid, encapsulated in a `Date` object.
-If the certificate's `notAfter` time cannot be parsed, this property is an
-invalid `Date` whose `getTime()` method returns `NaN`.
-
-Relational comparisons with an invalid `Date` evaluate to `false`. To check
-whether the current time is within the certificate's validity period, use
-`now >= x509.validFromDate && now <= x509.validToDate`. This expression
-evaluates to `false` if either date is invalid. Checking
-`now < x509.validFromDate || now > x509.validToDate` instead can fail to
-reject certificates with invalid dates.
+Throws if the time cannot be converted.
### `x509.signature`
@@ -3641,6 +3665,8 @@ added: v24.9.0
The OID of the algorithm used to sign the certificate.
+Throws if the OID cannot be formatted.
+
### `x509.version`
<!-- YAML
@@ -3656,6 +3682,10 @@ v3. A certificate with no encoded version field has version `1`.
<!-- YAML
added: v15.6.0
+changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/67408
+ description: Errors reported separately from verification failure now throw.
-->
* `publicKey` {KeyObject} A public key.
@@ -3664,6 +3694,11 @@ added: v15.6.0
Verifies that this certificate was signed by the given public key.
Does not perform any other validation checks on the certificate.
+Returns `true` for a valid signature and `false` for a verification failure or
+an incompatible key type. A signature BIT STRING with invalid unused bits also
+returns `false`. Throws for other errors reported separately from verification
+failure.
+
## `node:crypto` module methods and properties
### `crypto.argon2(algorithm, parameters, callback)`
@@ -8118,6 +8153,8 @@ See the [list of SSL OP Flags][] for details.
[`stream.Transform`]: stream.md#class-streamtransform
[`stream.Writable` options]: stream.md#new-streamwritableoptions
[`stream.transform` options]: stream.md#new-streamtransformoptions
+[`tlsSocket.getCertificate()`]: tls.md#tlssocketgetcertificate
+[`tlsSocket.getPeerCertificate()`]: tls.md#tlssocketgetpeercertificatedetailed
[`util.promisify()`]: util.md#utilpromisifyoriginal
[`verify.update()`]: #verifyupdatedata-inputencoding
[`verify.verify()`]: #verifyverifykey-signature-signatureencoding
diff --git a/lib/internal/crypto/x509.js b/lib/internal/crypto/x509.js
index 30681da0b12..c6cbb7a9619 100644
--- a/lib/internal/crypto/x509.js
+++ b/lib/internal/crypto/x509.js
@@ -172,6 +172,16 @@ const {
return state[index];
}
+ function getInspectValue(cert, index, getter) {
+ try {
+ return getCachedValue(cert, index, getter);
+ } catch (err) {
+ if (err.code !== 'ERR_CRYPTO_OPERATION_FAILED') throw err;
+ // Legacy inspection fallbacks must not become public getter results.
+ return FunctionPrototypeCall(getter, cert, /* throwOnError */ false);
+ }
+ }
+
function getInspectionValue(certificate, getter) {
getState(certificate);
const value = FunctionPrototypeCall(getter, certificate);
@@ -209,31 +219,31 @@ const {
};
return `X509Certificate ${inspect({
- subject: getCachedValue(this, kSlotSubject, nativeSubject),
+ subject: getInspectValue(this, kSlotSubject, nativeSubject),
subjectAltName:
getCachedValue(this, kSlotSubjectAltName, nativeSubjectAltName),
- issuer: getCachedValue(this, kSlotIssuer, nativeIssuer),
- infoAccess: getCachedValue(this, kSlotInfoAccess, nativeInfoAccess),
- validFrom: getCachedValue(this, kSlotValidFrom, nativeValidFrom),
- validTo: getCachedValue(this, kSlotValidTo, nativeValidTo),
+ issuer: getInspectValue(this, kSlotIssuer, nativeIssuer),
+ infoAccess: getInspectValue(this, kSlotInfoAccess, nativeInfoAccess),
+ validFrom: getInspectValue(this, kSlotValidFrom, nativeValidFrom),
+ validTo: getInspectValue(this, kSlotValidTo, nativeValidTo),
validFromDate:
- getCachedValue(this, kSlotValidFromDate, nativeValidFromDate),
+ getInspectValue(this, kSlotValidFromDate, nativeValidFromDate),
validToDate:
- getCachedValue(this, kSlotValidToDate, nativeValidToDate),
+ getInspectValue(this, kSlotValidToDate, nativeValidToDate),
fingerprint:
- getCachedValue(this, kSlotFingerprint, nativeFingerprint),
+ getInspectValue(this, kSlotFingerprint, nativeFingerprint),
fingerprint256:
- getCachedValue(this, kSlotFingerprint256, nativeFingerprint256),
+ getInspectValue(this, kSlotFingerprint256, nativeFingerprint256),
fingerprint512:
- getCachedValue(this, kSlotFingerprint512, nativeFingerprint512),
- keyUsage: getCachedValue(this, kSlotKeyUsage, nativeKeyUsage),
+ getInspectValue(this, kSlotFingerprint512, nativeFingerprint512),
+ keyUsage: getInspectValue(this, kSlotKeyUsage, nativeKeyUsage),
serialNumber:
- getCachedValue(this, kSlotSerialNumber, nativeSerialNumber),
+ getInspectValue(this, kSlotSerialNumber, nativeSerialNumber),
signatureAlgorithm:
getCachedValue(
this, kSlotSignatureAlgorithm, nativeSignatureAlgorithm),
signatureAlgorithmOid:
- getCachedValue(
+ getInspectValue(
this, kSlotSignatureAlgorithmOid, nativeSignatureAlgorithmOid),
}, opts)}`;
}
diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc
index 2d5a4e4828b..b66e80e1633 100644
--- a/src/crypto/crypto_x509.cc
+++ b/src/crypto/crypto_x509.cc
@@ -173,6 +173,17 @@ void ManagedX509::MemoryInfo(MemoryTracker* tracker) const {
}
namespace {
+enum class X509ConversionMode {
+ kThrow,
+ kLegacy,
+};
+
+X509ConversionMode GetConversionMode(const FunctionCallbackInfo<Value>& args) {
+ // The JS inspector retries failed getters without caching the fallback.
+ return args[0]->IsFalse() ? X509ConversionMode::kLegacy
+ : X509ConversionMode::kThrow;
+}
+
enum class MalformedSubjectAltNameMode {
kThrow,
kReturnLegacySentinel,
@@ -180,14 +191,19 @@ enum class MalformedSubjectAltNameMode {
constexpr std::string_view kMalformedSubjectAltName = "DNS:";
-MaybeLocal<Value> GetFingerprintDigest(Environment* env,
- const Digest& method,
- const X509View& cert) {
+MaybeLocal<Value> GetFingerprintDigest(
+ Environment* env,
+ const Digest& method,
+ const X509View& cert,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
auto fingerprint = cert.getFingerprint(method);
- // Returning an empty string indicates that the digest failed for
- // some reason.
if (!fingerprint.has_value()) [[unlikely]] {
- return Undefined(env->isolate());
+ if (mode == X509ConversionMode::kLegacy) {
+ return Undefined(env->isolate());
+ }
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+ "Failed to calculate X.509 fingerprint");
+ return {};
}
auto& fp = fingerprint.value();
return OneByteString(env->isolate(), fp.data(), fp.length());
@@ -199,7 +215,8 @@ void Fingerprint(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetFingerprintDigest(env, algo, cert->view()).ToLocal(&ret)) {
+ if (GetFingerprintDigest(env, algo, cert->view(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -209,44 +226,49 @@ MaybeLocal<String> ToV8Value(Environment* env, std::string_view val) {
env->isolate(), val.data(), NewStringType::kNormal, val.size());
}
-MaybeLocal<Value> ToV8Value(Local<Context> context, BIOPointer&& bio) {
- if (!bio) [[unlikely]]
- return {};
- BUF_MEM* mem = bio;
- if (!mem) [[unlikely]]
- return {};
- Local<Value> ret;
- if (!String::NewFromUtf8(Isolate::GetCurrent(),
- mem->data,
- NewStringType::kNormal,
- mem->length)
- .ToLocal(&ret))
- return {};
- return ret;
-}
-
-MaybeLocal<Value> ToV8Value(Local<Context> context, const BIOPointer& bio) {
- if (!bio) [[unlikely]]
+MaybeLocal<Value> ToV8Value(
+ Local<Context> context,
+ const BIOPointer& bio,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
+ Environment* env = Environment::GetCurrent(context);
+ if (!bio) [[unlikely]] {
+ if (mode == X509ConversionMode::kThrow) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to format X.509 value");
+ }
return {};
+ }
BUF_MEM* mem = bio;
- if (!mem) [[unlikely]]
+ if (!mem) [[unlikely]] {
+ if (mode == X509ConversionMode::kThrow) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to read X.509 output BIO");
+ }
return {};
+ }
Local<Value> ret;
- if (!String::NewFromUtf8(Isolate::GetCurrent(),
- mem->data,
- NewStringType::kNormal,
- mem->length)
+ if (!String::NewFromUtf8(
+ env->isolate(), mem->data, NewStringType::kNormal, mem->length)
.ToLocal(&ret))
return {};
return ret;
}
-MaybeLocal<Value> ToBuffer(Environment* env, BIOPointer* bio) {
- if (bio == nullptr || !*bio) [[unlikely]]
+MaybeLocal<Value> ToBuffer(
+ Environment* env,
+ BIOPointer* bio,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
+ if (bio == nullptr || !*bio) [[unlikely]] {
+ if (mode == X509ConversionMode::kThrow) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to encode X.509 value");
+ }
return {};
+ }
BUF_MEM* mem = *bio;
- if (!mem) [[unlikely]]
+ if (!mem) [[unlikely]] {
+ if (mode == X509ConversionMode::kThrow) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to read X.509 output BIO");
+ }
return {};
+ }
auto backing = AdoptIntoBackingStore(
env->isolate(),
mem->data,
@@ -265,12 +287,15 @@ MaybeLocal<Value> ToBuffer(Environment* env, BIOPointer* bio) {
return ret;
}
-MaybeLocal<Value> GetDer(Environment* env, const X509View& view) {
+MaybeLocal<Value> GetDer(Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
Local<Value> ret;
auto bio = view.toDER();
- if (!bio) [[unlikely]]
+ if (!bio && mode == X509ConversionMode::kLegacy) {
return Undefined(env->isolate());
- if (!ToBuffer(env, &bio).ToLocal(&ret)) {
+ }
+ if (!ToBuffer(env, &bio, mode).ToLocal(&ret)) {
return {};
}
return ret;
@@ -279,7 +304,8 @@ MaybeLocal<Value> GetDer(Environment* env, const X509View& view) {
MaybeLocal<Value> GetSubjectAltNameString(
Environment* env,
const X509View& view,
- MalformedSubjectAltNameMode malformed_name_mode) {
+ MalformedSubjectAltNameMode malformed_name_mode,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
Local<Value> ret;
bool has_extension = false;
auto bio = view.getSubjectAltName(&has_extension);
@@ -299,57 +325,101 @@ MaybeLocal<Value> GetSubjectAltNameString(
env, "Failed to format subject alternative name");
return {};
}
- if (!ToV8Value(env->context(), bio).ToLocal(&ret)) return {};
+ if (!ToV8Value(env->context(), bio, mode).ToLocal(&ret)) return {};
return ret;
}
-MaybeLocal<Value> GetInfoAccessString(Environment* env, const X509View& view) {
+MaybeLocal<Value> GetInfoAccessString(
+ Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
Local<Value> ret;
- auto bio = view.getInfoAccess();
- if (!bio) [[unlikely]]
- return Undefined(env->isolate());
- if (!ToV8Value(env->context(), bio).ToLocal(&ret)) {
+ bool has_extension = false;
+ auto bio = view.getInfoAccess(&has_extension);
+ if (!bio) [[unlikely]] {
+ if (!has_extension || mode == X509ConversionMode::kLegacy) {
+ return Undefined(env->isolate());
+ }
+ THROW_ERR_CRYPTO_OPERATION_FAILED(
+ env, "Failed to format authority information access");
+ return {};
+ }
+ if (!ToV8Value(env->context(), bio, mode).ToLocal(&ret)) {
return {};
}
return ret;
}
-MaybeLocal<Value> GetValidFrom(Environment* env, const X509View& view) {
+MaybeLocal<Value> GetValidFrom(
+ Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
+ if (mode == X509ConversionMode::kThrow &&
+ !view.getValidFromTime().has_value()) [[unlikely]] {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to convert X.509 notBefore");
+ return {};
+ }
Local<Value> ret;
auto bio = view.getValidFrom();
- if (!bio) [[unlikely]]
+ if (!bio && mode == X509ConversionMode::kLegacy) {
return Undefined(env->isolate());
- if (!ToV8Value(env->context(), bio).ToLocal(&ret)) {
+ }
+ if (!ToV8Value(env->context(), bio, mode).ToLocal(&ret)) {
return {};
}
return ret;
}
-MaybeLocal<Value> GetValidTo(Environment* env, const X509View& view) {
+MaybeLocal<Value> GetValidTo(
+ Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
+ if (mode == X509ConversionMode::kThrow && !view.getValidToTime().has_value())
+ [[unlikely]] {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to convert X.509 notAfter");
+ return {};
+ }
Local<Value> ret;
auto bio = view.getValidTo();
- if (!bio) [[unlikely]]
+ if (!bio && mode == X509ConversionMode::kLegacy) {
return Undefined(env->isolate());
- if (!ToV8Value(env->context(), bio).ToLocal(&ret)) {
+ }
+ if (!ToV8Value(env->context(), bio, mode).ToLocal(&ret)) {
return {};
}
return ret;
}
-MaybeLocal<Value> GetValidFromDate(Environment* env, const X509View& view) {
+MaybeLocal<Value> GetValidFromDate(
+ Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
const auto valid_from_time = view.getValidFromTime();
- const double milliseconds = valid_from_time.has_value()
- ? valid_from_time.value() * 1000.
- : std::numeric_limits<double>::quiet_NaN();
- return Date::New(env->context(), milliseconds);
+ if (!valid_from_time.has_value()) [[unlikely]] {
+ if (mode == X509ConversionMode::kLegacy) {
+ return Date::New(env->context(),
+ std::numeric_limits<double>::quiet_NaN());
+ }
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to convert X.509 notBefore");
+ return {};
+ }
+ return Date::New(env->context(), valid_from_time.value() * 1000.);
}
-MaybeLocal<Value> GetValidToDate(Environment* env, const X509View& view) {
+MaybeLocal<Value> GetValidToDate(
+ Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
const auto valid_to_time = view.getValidToTime();
- const double milliseconds = valid_to_time.has_value()
- ? valid_to_time.value() * 1000.
- : std::numeric_limits<double>::quiet_NaN();
- return Date::New(env->context(), milliseconds);
+ if (!valid_to_time.has_value()) [[unlikely]] {
+ if (mode == X509ConversionMode::kLegacy) {
+ return Date::New(env->context(),
+ std::numeric_limits<double>::quiet_NaN());
+ }
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to convert X.509 notAfter");
+ return {};
+ }
+ return Date::New(env->context(), valid_to_time.value() * 1000.);
}
MaybeLocal<Value> GetSignatureAlgorithm(Environment* env,
@@ -364,11 +434,19 @@ MaybeLocal<Value> GetSignatureAlgorithm(Environment* env,
return ret;
}
-MaybeLocal<Value> GetSignatureAlgorithmOID(Environment* env,
- const X509View& view) {
+MaybeLocal<Value> GetSignatureAlgorithmOID(
+ Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
auto oid = view.getSignatureAlgorithmOID();
- if (!oid.has_value()) [[unlikely]]
- return Undefined(env->isolate());
+ if (!oid.has_value()) [[unlikely]] {
+ if (mode == X509ConversionMode::kLegacy) {
+ return Undefined(env->isolate());
+ }
+ THROW_ERR_CRYPTO_OPERATION_FAILED(
+ env, "Failed to read X.509 signature algorithm OID");
+ return {};
+ }
Local<Value> ret;
if (!ToV8Value(env, oid.value()).ToLocal(&ret)) {
return {};
@@ -376,20 +454,40 @@ MaybeLocal<Value> GetSignatureAlgorithmOID(Environment* env,
return ret;
}
-MaybeLocal<Value> GetSerialNumber(Environment* env, const X509View& view) {
+MaybeLocal<Value> GetSerialNumber(
+ Environment* env,
+ const X509View& view,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
if (auto serial = view.getSerialNumber()) {
return ToV8Value(
env, ToUpper(std::string_view(static_cast<char*>(serial.get()))));
}
- return Undefined(env->isolate());
+ if (mode == X509ConversionMode::kLegacy) {
+ return Undefined(env->isolate());
+ }
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to read X.509 serial number");
+ return {};
}
-MaybeLocal<Value> GetKeyUsage(Environment* env, const X509View& cert) {
+MaybeLocal<Value> GetKeyUsage(
+ Environment* env,
+ const X509View& cert,
+ X509ConversionMode mode = X509ConversionMode::kThrow) {
LocalVector<Value> vec(env->isolate());
- bool res = cert.enumUsages([&](std::string_view view) {
+ const auto result = cert.enumUsages([&](std::string_view view) {
vec.push_back(OneByteString(env->isolate(), view));
});
- if (!res) return Undefined(env->isolate());
+ if (result == X509View::UsageResult::ABSENT) {
+ return Undefined(env->isolate());
+ }
+ if (result == X509View::UsageResult::OPERATION_FAILED) {
+ if (mode == X509ConversionMode::kLegacy) {
+ return Undefined(env->isolate());
+ }
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+ "Failed to parse extended key usage");
+ return {};
+ }
return Array::New(env->isolate(), vec.data(), vec.size());
}
@@ -792,7 +890,9 @@ void Subject(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (ToV8Value(env->context(), cert->view().getSubject()).ToLocal(&ret)) {
+ if (ToV8Value(
+ env->context(), cert->view().getSubject(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -814,7 +914,9 @@ void Issuer(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (ToV8Value(env->context(), cert->view().getIssuer()).ToLocal(&ret)) {
+ if (ToV8Value(
+ env->context(), cert->view().getIssuer(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -824,7 +926,8 @@ void InfoAccess(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetInfoAccessString(env, cert->view()).ToLocal(&ret)) {
+ if (GetInfoAccessString(env, cert->view(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -834,7 +937,7 @@ void ValidFrom(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetValidFrom(env, cert->view()).ToLocal(&ret)) {
+ if (GetValidFrom(env, cert->view(), GetConversionMode(args)).ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -844,7 +947,7 @@ void ValidTo(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetValidTo(env, cert->view()).ToLocal(&ret)) {
+ if (GetValidTo(env, cert->view(), GetConversionMode(args)).ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -854,7 +957,8 @@ void ValidFromDate(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetValidFromDate(env, cert->view()).ToLocal(&ret)) {
+ if (GetValidFromDate(env, cert->view(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -864,7 +968,8 @@ void ValidToDate(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetValidToDate(env, cert->view()).ToLocal(&ret)) {
+ if (GetValidToDate(env, cert->view(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -884,7 +989,8 @@ void SignatureAlgorithmOID(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetSignatureAlgorithmOID(env, cert->view()).ToLocal(&ret)) {
+ if (GetSignatureAlgorithmOID(env, cert->view(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -894,7 +1000,8 @@ void SerialNumber(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetSerialNumber(env, cert->view()).ToLocal(&ret)) {
+ if (GetSerialNumber(env, cert->view(), GetConversionMode(args))
+ .ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -925,7 +1032,7 @@ void KeyUsage(const FunctionCallbackInfo<Value>& args) {
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
Local<Value> ret;
- if (GetKeyUsage(env, cert->view()).ToLocal(&ret)) {
+ if (GetKeyUsage(env, cert->view(), GetConversionMode(args)).ToLocal(&ret)) {
args.GetReturnValue().Set(ret);
}
}
@@ -948,18 +1055,29 @@ void CheckIssued(const FunctionCallbackInfo<Value>& args) {
}
void CheckPrivateKey(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
CHECK(args[0]->IsObject());
KeyObjectHandle* key;
ASSIGN_OR_RETURN_UNWRAP(&key, args[0]);
CHECK_EQ(key->Data().GetKeyType(), kKeyTypePrivate);
- args.GetReturnValue().Set(
- cert->view().checkPrivateKey(key->Data().GetAsymmetricKey()) ==
- X509View::CheckKeyResult::MATCH);
+ switch (cert->view().checkPrivateKey(key->Data().GetAsymmetricKey())) {
+ case X509View::CheckKeyResult::MATCH:
+ return args.GetReturnValue().Set(true);
+ case X509View::CheckKeyResult::NO_MATCH:
+ return args.GetReturnValue().Set(false);
+ case X509View::CheckKeyResult::UNSUPPORTED_OPERATION:
+ return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(
+ env, "Unsupported public key comparison");
+ case X509View::CheckKeyResult::OPERATION_FAILED:
+ return THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+ "Failed to compare public keys");
+ }
}
void CheckPublicKey(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
X509Certificate* cert;
ASSIGN_OR_RETURN_UNWRAP(&cert, args.This());
@@ -969,8 +1087,14 @@ void CheckPublicKey(const FunctionCallbackInfo<Value>& args) {
// A Public Key can be derived from a private key, so we allow both.
CHECK_NE(key->Data().GetKeyType(), kKeyTypeSecret);
- args.GetReturnValue().Set(
- cert->view().checkPublicKey(key->Data().GetAsymmetricKey()));
+ const auto result =
+ cert->view().checkPublicKey(key->Data().GetAsymmetricKey());
+ if (!result) {
+ return ThrowCryptoError(env,
+ result.openssl_error.value_or(0),
+ "Failed to verify X.509 signature");
+ }
+ args.GetReturnValue().Set(result.value);
}
template <typename F>
@@ -1123,8 +1247,14 @@ bool Set(Environment* env,
// Each entry of the name is converted into a property of the object.
// The property value may be a single string or an array of strings.
static MaybeLocal<Value> GetX509NameObject(Environment* env,
- const X509Name& name) {
- if (!name) return {};
+ const X509Name& name,
+ X509ConversionMode mode) {
+ if (!name) {
+ if (mode == X509ConversionMode::kThrow) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to read X.509 name");
+ }
+ return {};
+ }
Local<Value> v8_name;
Local<Value> v8_value;
@@ -1191,30 +1321,36 @@ static MaybeLocal<Value> GetX509NameObject(Environment* env,
return result;
}
-MaybeLocal<Object> GetPubKey(Environment* env, const ncrypto::Rsa& rsa) {
+MaybeLocal<Object> GetPubKey(Environment* env,
+ const ncrypto::Rsa& rsa,
+ X509ConversionMode mode) {
auto bio = rsa.derPublicKey();
Local<Value> ret;
- if (!ToBuffer(env, &bio).ToLocal(&ret)) return {};
+ if (!ToBuffer(env, &bio, mode).ToLocal(&ret)) return {};
return ret.As<Object>();
}
-MaybeLocal<Value> GetModulusString(Environment* env, const BIGNUM* n) {
+MaybeLocal<Value> GetModulusString(Environment* env,
+ const BIGNUM* n,
+ X509ConversionMode mode) {
auto bio = BIOPointer::New(n);
- if (!bio) [[unlikely]]
- return {};
- return ToV8Value(env->context(), bio);
+ return ToV8Value(env->context(), bio, mode);
}
-MaybeLocal<Value> GetExponentString(Environment* env, const BIGNUM* e) {
+MaybeLocal<Value> GetExponentString(Environment* env,
+ const BIGNUM* e,
+ X509ConversionMode mode) {
if (e == nullptr) return Null(env->isolate());
MarkPopErrorOnReturn mark_pop_error_on_return;
auto bio = BIOPointer::NewMem();
- if (!bio) [[unlikely]]
- return {};
- if (!BIOPointer::WriteAll(bio, "0x") || BN_print(bio.get(), e) != 1) {
+ if (!bio || !BIOPointer::WriteAll(bio, "0x") || BN_print(bio.get(), e) != 1)
+ [[unlikely]] {
+ if (mode == X509ConversionMode::kThrow) {
+ THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to format RSA exponent");
+ }
return {};
}
- return ToV8Value(env->context(), bio);
+ return ToV8Value(env->context(), bio, mode);
}
MaybeLocal<Value> GetECPubKey(Environment* env, const ncrypto::Ec& ec) {
@@ -1246,7 +1382,9 @@ MaybeLocal<Value> GetCurveName(Environment* env, const int nid) {
: MaybeLocal<Value>(Undefined(env->isolate()));
}
-MaybeLocal<Object> X509ToObject(Environment* env, const X509View& cert) {
+MaybeLocal<Object> X509ToObject(Environment* env,
+ const X509View& cert,
+ X509ConversionMode mode) {
EscapableHandleScope scope(env->isolate());
auto tmpl = env->x509_dictionary_template();
@@ -1277,38 +1415,59 @@ MaybeLocal<Object> X509ToObject(Environment* env, const X509View& cert) {
}
Local<Value> subject;
- if (!GetX509NameObject(env, cert.getSubjectName()).ToLocal(&subject)) {
+ if (!GetX509NameObject(env, cert.getSubjectName(), mode).ToLocal(&subject)) {
return {};
}
Local<Value> issuer;
- if (!GetX509NameObject(env, cert.getIssuerName()).ToLocal(&issuer)) {
+ if (!GetX509NameObject(env, cert.getIssuerName(), mode).ToLocal(&issuer)) {
return {};
}
MaybeLocal<Value> values[] = {
subject,
issuer,
- GetSubjectAltNameString(
- env, cert, MalformedSubjectAltNameMode::kReturnLegacySentinel),
- GetInfoAccessString(env, cert),
+ Undefined(env->isolate()), // subjectaltname
+ Undefined(env->isolate()), // infoAccess
Boolean::New(env->isolate(), cert.isCA()),
Undefined(env->isolate()), // modulus
Undefined(env->isolate()), // exponent
Undefined(env->isolate()), // pubkey
Undefined(env->isolate()), // bits
- GetValidFrom(env, cert),
- GetValidTo(env, cert),
- GetFingerprintDigest(env, Digest::SHA1, cert),
- GetFingerprintDigest(env, Digest::SHA256, cert),
- GetFingerprintDigest(env, Digest::SHA512, cert),
- GetKeyUsage(env, cert),
- GetSerialNumber(env, cert),
- GetDer(env, cert),
+ Undefined(env->isolate()), // valid_from
+ Undefined(env->isolate()), // valid_to
+ Undefined(env->isolate()), // fingerprint
+ Undefined(env->isolate()), // fingerprint256
+ Undefined(env->isolate()), // fingerprint512
+ Undefined(env->isolate()), // ext_key_usage
+ Undefined(env->isolate()), // serialNumber
+ Undefined(env->isolate()), // raw
Undefined(env->isolate()), // asn1curve
Undefined(env->isolate()), // nistcurve
};
+ values[2] = GetSubjectAltNameString(
+ env, cert, MalformedSubjectAltNameMode::kReturnLegacySentinel, mode);
+ if (values[2].IsEmpty()) return {};
+ values[3] = GetInfoAccessString(env, cert, mode);
+ if (values[3].IsEmpty()) return {};
+ values[9] = GetValidFrom(env, cert, mode);
+ if (values[9].IsEmpty()) return {};
+ values[10] = GetValidTo(env, cert, mode);
+ if (values[10].IsEmpty()) return {};
+ values[11] = GetFingerprintDigest(env, Digest::SHA1, cert, mode);
+ if (values[11].IsEmpty()) return {};
+ values[12] = GetFingerprintDigest(env, Digest::SHA256, cert, mode);
+ if (values[12].IsEmpty()) return {};
+ values[13] = GetFingerprintDigest(env, Digest::SHA512, cert, mode);
+ if (values[13].IsEmpty()) return {};
+ values[14] = GetKeyUsage(env, cert, mode);
+ if (values[14].IsEmpty()) return {};
+ values[15] = GetSerialNumber(env, cert, mode);
+ if (values[15].IsEmpty()) return {};
+ values[16] = GetDer(env, cert, mode);
+ if (values[16].IsEmpty()) return {};
+
// X509_get0_pubkey() fails when the linked OpenSSL cannot decode the
// certificate's public key (e.g. an unsupported curve in FIPS builds).
// ifRsa()/ifEc() assume a decodable key, so leave the key-specific
@@ -1322,34 +1481,41 @@ MaybeLocal<Object> X509ToObject(Environment* env, const X509View& cert) {
NewDictionaryInstance(env->context(), tmpl, values));
}
- cert.ifRsa([&](const ncrypto::Rsa& rsa) {
- auto pub_key = rsa.getPublicKey();
- values[5] = GetModulusString(env, pub_key.n); // modulus
- values[6] = GetExponentString(env, pub_key.e); // exponent
- values[7] = GetPubKey(env, rsa); // pubkey
- values[8] = Integer::New(env->isolate(),
- BignumPointer::GetBitCount(pub_key.n)); // bits
- // TODO(@jasnell): The true response is a left-over from the original
- // non DictionaryTemplate-based implementation. It can be removed later.
- return true;
- });
+ if (!cert.ifRsa([&](const ncrypto::Rsa& rsa) {
+ auto pub_key = rsa.getPublicKey();
+ values[5] = GetModulusString(env, pub_key.n, mode); // modulus
+ if (values[5].IsEmpty()) return false;
+ values[6] = GetExponentString(env, pub_key.e, mode); // exponent
+ if (values[6].IsEmpty()) return false;
+ values[7] = GetPubKey(env, rsa, mode); // pubkey
+ if (values[7].IsEmpty()) return false;
+ values[8] =
+ Integer::New(env->isolate(),
+ BignumPointer::GetBitCount(pub_key.n)); // bits
+ return true;
+ })) {
+ return {};
+ }
- cert.ifEc([&](const ncrypto::Ec& ec) {
- const auto group = ec.getGroup();
- values[7] = GetECPubKey(env, ec); // pubkey
- values[8] = GetECGroupBits(env, group); // bits
- const int nid = ec.getCurve();
- if (nid != 0) {
- // Curve is well-known, get its OID and NIST nick-name (if it has
- // one).
- values[17] = GetCurveName<OBJ_nid2sn>(env, nid); // asn1curve
- values[18] = GetCurveName<EC_curve_nid2nist>(env, nid); // nistcurve
- }
- // Unnamed curves can be described by their mathematical properties,
- // but aren't used much (at all?) with X.509/TLS. Support later if
- // needed.
- return true;
- });
+ if (!cert.ifEc([&](const ncrypto::Ec& ec) {
+ const auto group = ec.getGroup();
+ values[7] = GetECPubKey(env, ec); // pubkey
+ if (values[7].IsEmpty()) return false;
+ values[8] = GetECGroupBits(env, group); // bits
+ const int nid = ec.getCurve();
+ if (nid != 0) {
+ // Curve is well-known, get its OID and NIST nick-name (if it has
+ // one).
+ values[17] = GetCurveName<OBJ_nid2sn>(env, nid); // asn1curve
+ values[18] = GetCurveName<EC_curve_nid2nist>(env, nid); // nistcurve
+ }
+ // Unnamed curves can be described by their mathematical properties,
+ // but aren't used much (at all?) with X.509/TLS. Support later if
+ // needed.
+ return true;
+ })) {
+ return {};
+ }
return scope.EscapeMaybe(NewDictionaryInstance(env->context(), tmpl, values));
}
@@ -1705,14 +1871,18 @@ MaybeLocal<Object> X509Certificate::GetPeerCert(Environment* env,
}
v8::MaybeLocal<v8::Value> X509Certificate::toObject(Environment* env) {
- return toObject(env, view());
+ return X509ToObject(env, view(), X509ConversionMode::kThrow)
+ .FromMaybe(Local<Value>());
}
v8::MaybeLocal<v8::Value> X509Certificate::toObject(Environment* env,
const X509View& cert) {
if (!cert) [[unlikely]]
return {};
- return X509ToObject(env, cert).FromMaybe(Local<Value>());
+ // TLS also builds legacy certificate objects during the handshake. An
+ // inspection failure must not change certificate acceptance there.
+ return X509ToObject(env, cert, X509ConversionMode::kLegacy)
+ .FromMaybe(Local<Value>());
}
X509Certificate::X509Certificate(
diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc
index 6a1ccf09c54..ea601d14c67 100644
--- a/test/cctest/test_node_crypto.cc
+++ b/test/cctest/test_node_crypto.cc
@@ -1366,7 +1366,7 @@ TEST(NodeCrypto, X509UsageVisitorPreservesCallbackErrors) {
ERR_clear_error();
int calls = 0;
- EXPECT_TRUE(cert.view().enumUsages([&calls](const char* oid) {
+ const auto result = cert.view().enumUsages([&calls](const char* oid) {
calls++;
EXPECT_STREQ(oid, "1.3.6.1.5.5.7.3.1");
#if NCRYPTO_USE_OPENSSL_PROVIDER
@@ -1376,12 +1376,70 @@ TEST(NodeCrypto, X509UsageVisitorPreservesCallbackErrors) {
#else
#error "Unsupported crypto backend"
#endif
- }));
+ });
+ EXPECT_EQ(result, ncrypto::X509View::UsageResult::PRESENT);
EXPECT_EQ(calls, 1);
EXPECT_NE(ERR_peek_error(), 0UL);
ERR_clear_error();
}
+TEST(NodeCrypto, X509UsageVisitorReportsFailures) {
+ using UsageResult = ncrypto::X509View::UsageResult;
+ ncrypto::ClearErrorOnReturn clear_errors;
+ ncrypto::X509Pointer cert(X509_new());
+ ASSERT_TRUE(cert);
+ const auto unexpected_usage = [](const char*) {
+ ADD_FAILURE() << "Unexpected extended key usage";
+ };
+ EXPECT_EQ(cert.view().enumUsages(unexpected_usage), UsageResult::ABSENT);
+
+ ncrypto::StackOfASN1 usages(sk_ASN1_OBJECT_new_null());
+ ASSERT_TRUE(usages);
+ X509ExtensionPointer empty_eku(
+ X509V3_EXT_i2d(NID_ext_key_usage, 0, usages.get()));
+ ASSERT_TRUE(empty_eku);
+ ASSERT_EQ(X509_add_ext(cert.get(), empty_eku.get(), -1), 1);
+ EXPECT_EQ(cert.view().enumUsages(unexpected_usage), UsageResult::PRESENT);
+
+ ASSERT_EQ(X509_add_ext(cert.get(), empty_eku.get(), -1), 1);
+ EXPECT_EQ(cert.view().enumUsages(unexpected_usage),
+ UsageResult::OPERATION_FAILED);
+
+ ncrypto::X509Pointer malformed(X509_new());
+ ncrypto::DeleteFnPtr<ASN1_OCTET_STRING, ASN1_OCTET_STRING_free> value(
+ ASN1_OCTET_STRING_new());
+ ASSERT_TRUE(malformed);
+ ASSERT_TRUE(value);
+ const unsigned char truncated_sequence[] = {0x30};
+ ASSERT_EQ(ASN1_OCTET_STRING_set(
+ value.get(), truncated_sequence, sizeof(truncated_sequence)),
+ 1);
+ X509ExtensionPointer malformed_eku(
+ X509_EXTENSION_create_by_NID(nullptr, NID_ext_key_usage, 0, value.get()));
+ ASSERT_TRUE(malformed_eku);
+ ASSERT_EQ(X509_add_ext(malformed.get(), malformed_eku.get(), -1), 1);
+ EXPECT_EQ(malformed.view().enumUsages(unexpected_usage),
+ UsageResult::OPERATION_FAILED);
+}
+
+TEST(NodeCrypto, X509InfoAccessReportsPresence) {
+ ncrypto::ClearErrorOnReturn clear_errors;
+ ncrypto::X509Pointer cert(X509_new());
+ ASSERT_TRUE(cert);
+ bool has_extension = true;
+ EXPECT_FALSE(cert.view().getInfoAccess(&has_extension));
+ EXPECT_FALSE(has_extension);
+
+ AccessDescriptionsPointer descriptions(sk_ACCESS_DESCRIPTION_new_null());
+ ASSERT_TRUE(descriptions);
+ X509ExtensionPointer info_access(
+ X509V3_EXT_i2d(NID_info_access, 0, descriptions.get()));
+ ASSERT_TRUE(info_access);
+ ASSERT_EQ(X509_add_ext(cert.get(), info_access.get(), -1), 1);
+ EXPECT_FALSE(cert.view().getInfoAccess(&has_extension));
+ EXPECT_TRUE(has_extension);
+}
+
TEST(NodeCrypto, PreservesLongX509NameObjectIdentifiers) {
const std::string oid_text = LongObjectIdentifier();
X509NamePointer name(X509_NAME_new());
diff --git a/test/parallel/test-crypto-x509-errors.js b/test/parallel/test-crypto-x509-errors.js
new file mode 100644
index 00000000000..0005170a75e
--- /dev/null
+++ b/test/parallel/test-crypto-x509-errors.js
@@ -0,0 +1,245 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const { isBoringSSL } = require('../common/crypto');
+const fixtures = require('../common/fixtures');
+const {
+ constants,
+ generateKeyPairSync,
+ generateX509CertificateSync,
+ X509Certificate,
+} = require('crypto');
+const { inspect } = require('util');
+
+const keys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+const expectedError = { code: 'ERR_CRYPTO_OPERATION_FAILED' };
+const eku = Buffer.from('300a06082b06010505070301', 'hex');
+
+function generate(overrides = {}) {
+ return generateX509CertificateSync({
+ subject: [{ name: 'CN', value: 'example.test' }],
+ publicKey: keys.publicKey,
+ serialNumber: 1n,
+ notBefore: new Date('2025-01-02T03:04:05Z'),
+ notAfter: new Date('2030-01-02T03:04:05Z'),
+ signature: { key: keys.privateKey, digest: 'sha256' },
+ ...overrides,
+ });
+}
+
+const certificate = generate();
+assert.strictEqual(certificate.infoAccess, undefined);
+assert.strictEqual(certificate.keyUsage, undefined);
+assert.strictEqual(certificate.toLegacyObject().infoAccess, undefined);
+assert.strictEqual(certificate.toLegacyObject().ext_key_usage, undefined);
+assert.strictEqual(certificate.raw, certificate.raw);
+assert.strictEqual(certificate.validFromDate, certificate.validFromDate);
+assert.strictEqual(certificate.validToDate, certificate.validToDate);
+assert.strictEqual(certificate.checkPrivateKey(keys.privateKey), true);
+const otherKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+assert.strictEqual(certificate.checkPrivateKey(otherKeys.privateKey), false);
+assert.strictEqual(certificate.verify(keys.publicKey), true);
+assert.strictEqual(certificate.verify(otherKeys.publicKey), false);
+const rsaKeys = generateKeyPairSync('rsa', { modulusLength: 2048 });
+assert.strictEqual(certificate.verify(rsaKeys.publicKey), false);
+
+{
+ const emptySubject = generate({
+ subject: [],
+ issuer: certificate,
+ extensions: [{
+ type: 'subjectAltName',
+ critical: true,
+ names: [{ type: 'dns', value: 'example.test' }],
+ }],
+ });
+ assert.strictEqual(emptySubject.subject, '');
+ assert.deepStrictEqual(emptySubject.toLegacyObject().subject, {
+ __proto__: null,
+ });
+}
+
+{
+ const aia = generate({
+ extensions: [{
+ type: 'raw',
+ oid: '1.3.6.1.5.5.7.1.1',
+ value: Buffer.from(
+ '301e301c06082b060105050730018610687474703a2f2f6f6373702e74657374',
+ 'hex'),
+ }],
+ });
+ assert.match(aia.infoAccess, /^OCSP - URI:http:\/\/ocsp\.test\n?$/);
+}
+
+for (const [oid, property] of [
+ ['1.3.6.1.5.5.7.1.1', 'infoAccess'],
+ ['2.5.29.37', 'keyUsage'],
+]) {
+ const malformed = generate({
+ extensions: [{ type: 'raw', oid, value: Buffer.from('0500', 'hex') }],
+ });
+ assert.throws(() => malformed[property], expectedError);
+ assert.throws(() => malformed.toLegacyObject(), expectedError);
+ assert(inspect(malformed).includes(`${property}: undefined`));
+ assert.throws(() => malformed[property], expectedError);
+ assert.throws(() => malformed.toLegacyObject(), expectedError);
+}
+
+{
+ const malformedSan = generate({
+ extensions: [{
+ type: 'raw',
+ oid: '2.5.29.17',
+ value: Buffer.from('0500', 'hex'),
+ }],
+ });
+ assert.throws(() => malformedSan.subjectAltName, expectedError);
+ assert.throws(() => inspect(malformedSan), expectedError);
+ assert.throws(() => malformedSan.subjectAltName, expectedError);
+}
+
+for (const value of [eku, Buffer.from('3000', 'hex')]) {
+ const withEku = generate({
+ extensions: [{ type: 'raw', oid: '2.5.29.37', value }],
+ });
+ assert.deepStrictEqual(
+ withEku.keyUsage, value === eku ? ['1.3.6.1.5.5.7.3.1'] : []);
+ assert.strictEqual(withEku.keyUsage, withEku.keyUsage);
+}
+
+{
+ const healthy = generate({
+ extensions: [{ type: 'raw', oid: '2.5.29.37', value: eku }],
+ });
+ const validFrom = healthy.validFromDate;
+ const validTo = healthy.validToDate;
+ const usages = healthy.keyUsage;
+ validFrom.setUTCFullYear(2040);
+ validTo.setUTCFullYear(2045);
+ usages.push('1.2.3.4');
+ const rendered = inspect(healthy);
+ assert(rendered.includes(validFrom.toISOString()));
+ assert(rendered.includes(validTo.toISOString()));
+ assert(rendered.includes("'1.2.3.4'"));
+ assert.strictEqual(healthy.validFromDate, validFrom);
+ assert.strictEqual(healthy.validToDate, validTo);
+ assert.strictEqual(healthy.keyUsage, usages);
+}
+
+{
+ const duplicateEku = generate({
+ extensions: [
+ { type: 'raw', oid: '2.5.29.37', value: eku },
+ { type: 'raw', oid: '2.5.29.99', value: eku },
+ ],
+ });
+ const der = Buffer.from(duplicateEku.raw);
+ const oid = Buffer.from('0603551d63', 'hex');
+ const offset = der.indexOf(oid);
+ assert.notStrictEqual(offset, -1);
+ // Change the second extension's OID without changing its encoded size.
+ der[offset + oid.length - 1] = 37;
+ const malformed = new X509Certificate(der);
+ assert.throws(() => malformed.keyUsage, expectedError);
+ assert.throws(() => malformed.toLegacyObject(), expectedError);
+}
+
+if (!isBoringSSL) {
+ // BoringSSL rejects malformed validity times while parsing the certificate.
+ const fixture = new X509Certificate(fixtures.readKey('agent1-cert.pem'));
+ for (const [encoded, property, dateProperty] of [
+ ['220903214037Z', 'validFrom', 'validFromDate'],
+ ['22960617214037Z', 'validTo', 'validToDate'],
+ ]) {
+ const der = Buffer.from(fixture.raw);
+ const offset = der.indexOf(encoded);
+ assert.notStrictEqual(offset, -1);
+ // Replace the hour while retaining the UTCTime or GeneralizedTime size.
+ der.write('99', offset + encoded.length - 7, 'ascii');
+ const malformed = new X509Certificate(der);
+ assert.throws(() => malformed[property], expectedError);
+ assert.throws(() => malformed[dateProperty], expectedError);
+ assert.throws(() => malformed.toLegacyObject(), expectedError);
+ const rendered = inspect(malformed);
+ assert(rendered.includes(`${property}: 'Bad time value'`));
+ assert(rendered.includes(`${dateProperty}: Invalid Date`));
+ assert.throws(() => malformed[property], expectedError);
+ assert.throws(() => malformed[dateProperty], expectedError);
+ assert.throws(() => malformed.toLegacyObject(), expectedError);
+ }
+
+ const der = Buffer.from(certificate.raw);
+ const algorithm = Buffer.from('06072a8648ce3d0201', 'hex');
+ const offset = der.indexOf(algorithm);
+ assert.notStrictEqual(offset, -1);
+ // Replace id-ecPublicKey with an unknown algorithm of the same encoded size.
+ der[offset + algorithm.length - 1] = 2;
+ const malformed = new X509Certificate(der);
+ assert.throws(() => malformed.checkPrivateKey(keys.privateKey), expectedError);
+}
+
+function assertVerifyError(certificate, publicKey, reason) {
+ // BoringSSL reports verification errors as zero rather than a negative value.
+ if (isBoringSSL) {
+ assert.strictEqual(certificate.verify(publicKey), false);
+ } else {
+ assert.throws(() => certificate.verify(publicKey), { reason });
+ }
+}
+
+{
+ const der = Buffer.from(certificate.raw);
+ const algorithm = Buffer.from('06082a8648ce3d040302', 'hex');
+ const offsets = [der.indexOf(algorithm), der.lastIndexOf(algorithm)];
+ assert.notStrictEqual(offsets[0], -1);
+ assert.notStrictEqual(offsets[0], offsets[1]);
+ // Replace ecdsa-with-SHA256 in both AlgorithmIdentifiers with an unknown OID.
+ for (const offset of offsets)
+ der[offset + algorithm.length - 1] = 0x7f;
+ assertVerifyError(new X509Certificate(der), keys.publicKey,
+ 'unknown signature algorithm');
+}
+
+{
+ const der = Buffer.from(certificate.raw);
+ const signature = certificate.signature.value;
+ const offset = der.lastIndexOf(signature);
+ assert(offset > 0);
+ assert.strictEqual(der[offset - 1], 0);
+ // Encode one unused bit in the signature BIT STRING, with its padding zero.
+ der[offset - 1] = 1;
+ der[offset + signature.length - 1] &= 0xfe;
+ assert.strictEqual(new X509Certificate(der).verify(keys.publicKey), false);
+}
+
+{
+ const pss = generate({
+ publicKey: rsaKeys.publicKey,
+ signature: {
+ key: {
+ key: rsaKeys.privateKey,
+ padding: constants.RSA_PKCS1_PSS_PADDING,
+ saltLength: 32,
+ },
+ digest: 'sha256',
+ },
+ });
+ assert.strictEqual(pss.verify(rsaKeys.publicKey), true);
+ assert.strictEqual(pss.verify(keys.publicKey), false);
+ const der = Buffer.from(pss.raw);
+ const saltLength = Buffer.from('a203020120', 'hex');
+ const offsets = [der.indexOf(saltLength), der.lastIndexOf(saltLength)];
+ assert.notStrictEqual(offsets[0], -1);
+ assert.notStrictEqual(offsets[0], offsets[1]);
+ // Make the RSA-PSS salt length negative in both AlgorithmIdentifiers.
+ for (const offset of offsets)
+ der[offset + saltLength.length - 1] = 0xff;
+ // OpenSSL reports this as a generic internal error, which stays an exception.
+ assertVerifyError(new X509Certificate(der), rsaKeys.publicKey,
+ 'internal error');
+}
diff --git a/test/parallel/test-crypto-x509.js b/test/parallel/test-crypto-x509.js
index 89525b93370..25a32980ac1 100644
--- a/test/parallel/test-crypto-x509.js
+++ b/test/parallel/test-crypto-x509.js
@@ -395,11 +395,9 @@ oans248kpal88CGqsN2so/wZKxVnpiXlPHMdiNL7hRSUqlHkUi07FrP2Htg8kjI=
legacyObjectCheck.serialNumberPattern);
}
-if (!process.features.openssl_is_boringssl) {
+if (!isBoringSSL) {
// BoringSSL rejects certificates with malformed times during parsing. The
- // formatter fallback is covered for all backends by the ncrypto cctest.
- // ASN1_TIME_print() documents "Bad time value" as the representation of an
- // invalid time. Ensure that detecting output errors does not discard it.
+ // native formatter fallback remains covered by the ncrypto cctest.
const invalidTimeDer = Buffer.from(der);
const validFromOffset = invalidTimeDer.indexOf('220903214037Z');
assert.notStrictEqual(validFromOffset, -1);
@@ -407,11 +405,10 @@ if (!process.features.openssl_is_boringssl) {
invalidTimeDer[validFromOffset + 3] = 0x33;
const invalidTimeCert = new X509Certificate(invalidTimeDer);
- assert.strictEqual(invalidTimeCert.validFrom, 'Bad time value');
- assert(Number.isNaN(invalidTimeCert.validFromDate.getTime()));
- assert.strictEqual(
- invalidTimeCert.toLegacyObject().valid_from,
- 'Bad time value');
+ const expectedError = { code: 'ERR_CRYPTO_OPERATION_FAILED' };
+ assert.throws(() => invalidTimeCert.validFrom, expectedError);
+ assert.throws(() => invalidTimeCert.validFromDate, expectedError);
+ assert.throws(() => invalidTimeCert.toLegacyObject(), expectedError);
}
{
diff --git a/test/parallel/test-tls-x509-conversion-errors.js b/test/parallel/test-tls-x509-conversion-errors.js
new file mode 100644
index 00000000000..f549bbf9f0d
--- /dev/null
+++ b/test/parallel/test-tls-x509-conversion-errors.js
@@ -0,0 +1,205 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const { isBoringSSL } = require('../common/crypto');
+const {
+ generateKeyPairSync,
+ generateX509CertificateSync,
+ X509Certificate,
+} = require('crypto');
+const tls = require('tls');
+const fixtures = require('../common/fixtures');
+const { connect } = require(fixtures.path('tls-connect'));
+
+const rootKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+const issuerKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+const serverKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+const clientKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+const malformedAia = {
+ type: 'raw',
+ oid: '1.3.6.1.5.5.7.1.1',
+ value: Buffer.from('0500', 'hex'),
+};
+const caExtensions = [
+ { type: 'basicConstraints', ca: true, critical: true },
+ { type: 'keyUsage', usages: ['keyCertSign', 'crlSign'], critical: true },
+];
+const san = {
+ type: 'subjectAltName',
+ names: [{ type: 'dns', value: 'localhost' }],
+};
+let serialNumber = 1n;
+
+function generate(keys, name, options = {}) {
+ return generateX509CertificateSync({
+ subject: [{ name: 'CN', value: name }],
+ publicKey: keys.publicKey,
+ serialNumber: serialNumber++,
+ notBefore: new Date('2000-01-01T00:00:00Z'),
+ notAfter: new Date('2125-01-01T00:00:00Z'),
+ signature: { key: keys.privateKey, digest: 'sha256' },
+ ...options,
+ });
+}
+
+const root = generate(rootKeys, 'Root CA', { extensions: caExtensions });
+const signature = { key: rootKeys.privateKey, digest: 'sha256' };
+const leaf = generate(serverKeys, 'localhost', {
+ issuer: root,
+ signature,
+ extensions: [san, malformedAia],
+});
+const issuer = generate(issuerKeys, 'Intermediate CA', {
+ issuer: root,
+ signature,
+ extensions: [...caExtensions, malformedAia],
+});
+const chainedLeaf = generate(serverKeys, 'localhost', {
+ issuer,
+ signature: { key: issuerKeys.privateKey, digest: 'sha256' },
+ extensions: [san],
+});
+const clientCertificate = generate(clientKeys, 'client', {
+ issuer: root,
+ signature,
+ extensions: [malformedAia],
+});
+const serverKey = serverKeys.privateKey.export({ type: 'pkcs8', format: 'pem' });
+const clientKey = clientKeys.privateKey.export({ type: 'pkcs8', format: 'pem' });
+const clientOptions = {
+ host: '127.0.0.1',
+ servername: 'localhost',
+ ca: root.toString(),
+};
+const expectedError = { code: 'ERR_CRYPTO_OPERATION_FAILED' };
+
+for (const certificate of [leaf, issuer, clientCertificate]) {
+ assert.throws(() => certificate.infoAccess, expectedError);
+ assert.throws(() => certificate.toLegacyObject(), expectedError);
+}
+
+function assertLegacy(legacy, certificate, name) {
+ assert.strictEqual(legacy.subject.CN, name);
+ assert.strictEqual(legacy.infoAccess, undefined);
+ assert.strictEqual(legacy.fingerprint256, certificate.fingerprint256);
+ assert.deepStrictEqual(legacy.raw, certificate.raw);
+}
+
+// Default hostname verification must not fail on unrelated malformed metadata.
+connect({
+ server: { key: serverKey, cert: leaf.toString() },
+ client: clientOptions,
+}, common.mustSucceed((pair, cleanup) => {
+ assert.strictEqual(pair.client.conn.authorized, true);
+ assertLegacy(pair.client.conn.getPeerCertificate(), leaf, 'localhost');
+ assertLegacy(pair.server.conn.getCertificate(), leaf, 'localhost');
+ const x509 = pair.client.conn.getPeerX509Certificate();
+ assert.throws(() => x509.infoAccess, expectedError);
+ assert.throws(() => x509.toLegacyObject(), expectedError);
+ cleanup();
+}));
+
+// The full legacy chain passed to a custom identity check remains usable.
+connect({
+ server: { key: serverKey, cert: `${chainedLeaf}${issuer}` },
+ client: {
+ ...clientOptions,
+ checkServerIdentity: common.mustCall((hostname, certificate) => {
+ assert.strictEqual(hostname, 'localhost');
+ assert.strictEqual(certificate.subject.CN, 'localhost');
+ assertLegacy(certificate.issuerCertificate, issuer, 'Intermediate CA');
+ return tls.checkServerIdentity(hostname, certificate);
+ }),
+ },
+}, common.mustSucceed((pair, cleanup) => {
+ assert.strictEqual(pair.client.conn.authorized, true);
+ const chain = pair.client.conn.getPeerCertificate(true);
+ assertLegacy(chain.issuerCertificate, issuer, 'Intermediate CA');
+ assert.throws(
+ () => pair.client.conn.getPeerX509Certificate().issuerCertificate.infoAccess,
+ expectedError,
+ );
+ cleanup();
+}));
+
+// Reading an authenticated client's certificate must use the TLS legacy mode.
+connect({
+ server: {
+ key: serverKey,
+ cert: leaf.toString(),
+ ca: root.toString(),
+ requestCert: true,
+ rejectUnauthorized: true,
+ },
+ client: {
+ ...clientOptions,
+ key: clientKey,
+ cert: clientCertificate.toString(),
+ },
+}, common.mustSucceed((pair, cleanup) => {
+ assert.strictEqual(pair.client.conn.authorized, true);
+ assert.strictEqual(pair.server.conn.authorized, true);
+ assertLegacy(pair.server.conn.getPeerCertificate(true),
+ clientCertificate, 'client');
+ assertLegacy(pair.client.conn.getCertificate(), clientCertificate, 'client');
+ assert.throws(
+ () => pair.server.conn.getPeerX509Certificate().infoAccess,
+ expectedError,
+ );
+ cleanup();
+}));
+
+{
+ const malformed = generate(serverKeys, 'localhost', {
+ issuer: root,
+ signature,
+ extensions: [san, {
+ type: 'raw',
+ oid: '2.5.29.37',
+ value: Buffer.from('0500', 'hex'),
+ }],
+ });
+ assert.throws(() => malformed.keyUsage, expectedError);
+ assert.throws(() => malformed.toLegacyObject(), expectedError);
+ connect({
+ server: { key: serverKey, cert: malformed.toString() },
+ client: { ...clientOptions, rejectUnauthorized: false },
+ }, common.mustSucceed((pair, cleanup) => {
+ assert.strictEqual(
+ pair.client.conn.getPeerCertificate().ext_key_usage, undefined);
+ assert.strictEqual(pair.server.conn.getCertificate().ext_key_usage, undefined);
+ assert.throws(
+ () => pair.client.conn.getPeerX509Certificate().keyUsage, expectedError);
+ cleanup();
+ }));
+}
+
+if (!isBoringSSL) {
+ // BoringSSL rejects malformed validity times when parsing the certificate.
+ const der = Buffer.from(generate(serverKeys, 'localhost', {
+ issuer: root,
+ signature,
+ extensions: [san],
+ }).raw);
+ const offset = der.indexOf('000101000000Z');
+ assert.notStrictEqual(offset, -1);
+ der.write('99', offset + 6, 'ascii');
+ const malformed = new X509Certificate(der);
+ assert.throws(() => malformed.validFrom, expectedError);
+ assert.throws(() => malformed.validFromDate, expectedError);
+ assert.throws(() => malformed.toLegacyObject(), expectedError);
+ connect({
+ server: { key: serverKey, cert: malformed.toString() },
+ client: { ...clientOptions, rejectUnauthorized: false },
+ }, common.mustSucceed((pair, cleanup) => {
+ assert.strictEqual(
+ pair.client.conn.getPeerCertificate().valid_from, 'Bad time value');
+ assert.strictEqual(
+ pair.server.conn.getCertificate().valid_from, 'Bad time value');
+ cleanup();
+ }));
+}
diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts
index 63f2f76bd5a..061e838824f 100644
--- a/typings/internalBinding/crypto.d.ts
+++ b/typings/internalBinding/crypto.d.ts
@@ -804,22 +804,31 @@ declare namespace InternalCryptoBinding {
}
interface X509CertificateHandle {
- subject(): string;
+ subject(throwOnError?: true): string;
+ subject(throwOnError: false): string | undefined;
subjectAltName(): string | undefined;
- issuer(): string;
+ issuer(throwOnError?: true): string;
+ issuer(throwOnError: false): string | undefined;
getIssuerCert(): X509CertificateHandle | undefined;
- infoAccess(): string | undefined;
- validFrom(): string;
- validTo(): string;
- validFromDate(): Date;
- validToDate(): Date;
- fingerprint(): string;
- fingerprint256(): string;
- fingerprint512(): string;
- keyUsage(): string[] | undefined;
- serialNumber(): string;
+ infoAccess(throwOnError?: boolean): string | undefined;
+ validFrom(throwOnError?: true): string;
+ validFrom(throwOnError: false): string | undefined;
+ validTo(throwOnError?: true): string;
+ validTo(throwOnError: false): string | undefined;
+ validFromDate(throwOnError?: boolean): Date;
+ validToDate(throwOnError?: boolean): Date;
+ fingerprint(throwOnError?: true): string;
+ fingerprint(throwOnError: false): string | undefined;
+ fingerprint256(throwOnError?: true): string;
+ fingerprint256(throwOnError: false): string | undefined;
+ fingerprint512(throwOnError?: true): string;
+ fingerprint512(throwOnError: false): string | undefined;
+ keyUsage(throwOnError?: boolean): string[] | undefined;
+ serialNumber(throwOnError?: true): string;
+ serialNumber(throwOnError: false): string | undefined;
signatureAlgorithm(): string | undefined;
- signatureAlgorithmOid(): string | undefined;
+ signatureAlgorithmOid(throwOnError?: true): string;
+ signatureAlgorithmOid(throwOnError: false): string | undefined;
version(): number | undefined;
subjectName(): X509Name | undefined;
issuerName(): X509Name | undefined;