From 18485ecacc40a121ee9734c51f550edd62e9f4b4 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 30 Aug 2026 16:36:34 +0200 Subject: [PATCH] crypto: add X509Certificate inspection getters Expose version, subjectName, issuerName, extensions, tbs, and signature without requiring callers to parse display strings or certificate DER. Names include complete DER and grouped, ordered RDN attributes. Extensions retain their order, duplicate and unknown OIDs, criticality, and raw values. Expose the exact signed TBSCertificate bytes and the signature octets, algorithm OID, and DER parameters. Return fresh objects, arrays, and Buffers from the new getters, leaving existing getter caching unchanged. Report inspection failures as ERR_CRYPTO_INVALID_X509_CERTIFICATE. Fixes: https://github.com/nodejs/node/issues/48730 Assisted-by: Codex Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 167 ++++++++ deps/ncrypto/ncrypto.h | 28 ++ doc/api/crypto.md | 208 ++++++++++ doc/api/errors.md | 10 + lib/internal/crypto/x509.js | 41 ++ lib/internal/errors.js | 2 + src/crypto/crypto_x509.cc | 387 ++++++++++++++++++ src/crypto/crypto_x509.h | 3 + src/env_properties.h | 5 + test/cctest/test_node_crypto.cc | 8 + .../test-crypto-x509-chain-snapshot.js | 34 ++ test/parallel/test-crypto-x509-inspection.js | 281 +++++++++++++ typings/internalBinding/crypto.d.ts | 32 ++ 13 files changed, 1206 insertions(+) create mode 100644 test/parallel/test-crypto-x509-inspection.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index a28b9cd209e9..06c6e3b6064c 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -46,6 +46,15 @@ using BignumCtxPointer = DeleteFnPtr; using BignumGenCallbackPointer = DeleteFnPtr; using NetscapeSPKIPointer = DeleteFnPtr; +struct ASN1SequenceDeleter final { + void operator()(ASN1_SEQUENCE_ANY* sequence) const { + sk_ASN1_TYPE_pop_free(sequence, ASN1_TYPE_free); + } +}; + +using ASN1SequencePointer = + std::unique_ptr; + #if NCRYPTO_USE_OPENSSL_PROVIDER using X509PubKeyPointer = DeleteFnPtr; // OSSL_STORE_close() returns int, so it needs a void-returning adapter to be @@ -1594,6 +1603,164 @@ bool X509View::isCA() const { return X509_check_ca(const_cast(cert_)) == 1; } +namespace { +bool CopyStringBytes(const ASN1_STRING* value, + std::vector* output) { + if (value == nullptr) return false; + const int length = ASN1_STRING_length(value); + const unsigned char* data = ASN1_STRING_get0_data(value); + if (length < 0 || (length > 0 && data == nullptr)) return false; + if (length == 0) { + output->clear(); + } else { + output->assign(data, data + static_cast(length)); + } + return true; +} + +bool CanConvertNameValueToUtf8(const ASN1_STRING* value) { + if (value == nullptr) return false; + switch (ASN1_STRING_type(value)) { + case V_ASN1_UTF8STRING: + case V_ASN1_NUMERICSTRING: + case V_ASN1_PRINTABLESTRING: + case V_ASN1_T61STRING: + case V_ASN1_IA5STRING: + case V_ASN1_UTCTIME: + case V_ASN1_GENERALIZEDTIME: + case V_ASN1_ISO64STRING: + case V_ASN1_UNIVERSALSTRING: + case V_ASN1_BMPSTRING: + return true; + default: + return false; + } +} + +std::optional GetEncodedRdnCount(const X509_NAME* name) { + const unsigned char* der = nullptr; + size_t der_length = 0; + if (X509_NAME_get0_der(name, &der, &der_length) != 1 || der == nullptr || + der_length > static_cast(LONG_MAX)) { + return std::nullopt; + } + + const unsigned char* cursor = der; + ASN1SequencePointer rdns(d2i_ASN1_SEQUENCE_ANY( + nullptr, &cursor, static_cast(der_length))); // NOLINT(runtime/int) + if (!rdns || cursor != der + der_length) return std::nullopt; + + const OPENSSL_SIZE_T count = sk_ASN1_TYPE_num(rdns.get()); + for (OPENSSL_SIZE_T i = 0; i < count; i++) { + const ASN1_TYPE* rdn = sk_ASN1_TYPE_value(rdns.get(), i); + if (rdn == nullptr || ASN1_TYPE_get(rdn) != V_ASN1_SET) { + return std::nullopt; + } + } + return static_cast(count); +} + +std::optional InspectName(const X509_NAME* name) { + if (name == nullptr) return std::nullopt; + const int count = X509_NAME_entry_count(name); + if (count < 0) return std::nullopt; + const auto encoded_rdn_count = GetEncodedRdnCount(name); + if (!encoded_rdn_count.has_value()) return std::nullopt; + + X509NameInspection output; + output.reserve(static_cast(count)); + int previous_set = -1; + for (int i = 0; i < count; i++) { + const X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, i); + if (entry == nullptr) return std::nullopt; + + const ASN1_OBJECT* object = X509_NAME_ENTRY_get_object(entry); + const ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry); + auto oid = ObjectToText(object, true); + const int set = X509_NAME_ENTRY_set(entry); + if (!oid.has_value() || value == nullptr || set < 0 || + (set != previous_set && + (previous_set == INT_MAX || set != previous_set + 1))) { + return std::nullopt; + } + previous_set = set; + + X509NameAttribute attribute{ + .set = set, + .oid = std::move(oid.value()), + }; + if (CanConvertNameValueToUtf8(value)) { + unsigned char* data = nullptr; + const int length = ASN1_STRING_to_UTF8(&data, value); + DataPointer owned(data, length > 0 ? static_cast(length) : 0); + if (length < 0 || (length > 0 && !owned)) return std::nullopt; + attribute.value = + length == 0 ? std::string() + : std::string(reinterpret_cast(owned.get()), + static_cast(length)); + } + output.push_back(std::move(attribute)); + } + const size_t inspected_rdn_count = + previous_set < 0 ? 0 : static_cast(previous_set) + 1; + if (inspected_rdn_count != encoded_rdn_count.value()) return std::nullopt; + return output; +} + +std::optional> InspectExtensions( + const X509* certificate) { + if (certificate == nullptr) return std::nullopt; + const int count = X509_get_ext_count(certificate); + if (count < 0) return std::nullopt; + + std::vector output; + output.reserve(static_cast(count)); + for (int i = 0; i < count; i++) { + const X509_EXTENSION* extension = X509_get_ext(certificate, i); + if (extension == nullptr) return std::nullopt; + + auto* mutable_extension = const_cast(extension); + auto oid = ObjectToText(X509_EXTENSION_get_object(mutable_extension), true); + const ASN1_OCTET_STRING* value = X509_EXTENSION_get_data(mutable_extension); + if (!oid.has_value() || value == nullptr) return std::nullopt; + + X509RawExtension inspected{ + .oid = std::move(oid.value()), + .critical = X509_EXTENSION_get_critical(mutable_extension) != 0, + }; + if (!CopyStringBytes(value, &inspected.value)) return std::nullopt; + output.push_back(std::move(inspected)); + } + return output; +} +} // namespace + +std::optional X509View::getVersion() const { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (cert_ == nullptr) return std::nullopt; + const long version = X509_get_version(cert_); // NOLINT(runtime/int) + if (version < 0 || version > 2) return std::nullopt; + return static_cast(version) + 1; +} + +std::optional X509View::inspectSubjectName() const { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (cert_ == nullptr) return std::nullopt; + return InspectName(X509_get_subject_name(cert_)); +} + +std::optional X509View::inspectIssuerName() const { + MarkPopErrorOnReturn mark_pop_error_on_return; + if (cert_ == nullptr) return std::nullopt; + return InspectName(X509_get_issuer_name(cert_)); +} + +std::optional> X509View::inspectExtensions() + const { + MarkPopErrorOnReturn mark_pop_error_on_return; + return InspectExtensions(cert_); +} + bool X509View::isIssuedBy(const X509View& issuer) const { MarkPopErrorOnReturn mark_pop_error_on_return; if (cert_ == nullptr || issuer.cert_ == nullptr) return false; diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index e45807ec1796..9ebf3ad535a3 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -59,6 +59,15 @@ #define NCRYPTO_USE_LEGACY_KEY_TYPES NCRYPTO_USE_BORINGSSL +// OpenSSL 4 makes ASN1_STRING opaque and provides a checked BIT STRING length +// accessor. Earlier OpenSSL releases and pinned BoringSSL expose the decoded +// unused-bit count through ASN1_STRING::flags. +#if !NCRYPTO_USE_BORINGSSL && OPENSSL_VERSION_PREREQ(4, 0) +#define NCRYPTO_HAS_ASN1_BIT_STRING_GET_LENGTH 1 +#else +#define NCRYPTO_HAS_ASN1_BIT_STRING_GET_LENGTH 0 +#endif + #if NCRYPTO_USE_OPENSSL_PROVIDER #include #include @@ -1618,6 +1627,20 @@ class X509Name final { int total_; }; +struct X509NameAttribute final { + int set = 0; + std::string oid; + std::optional value; +}; + +using X509NameInspection = std::vector; + +struct X509RawExtension final { + std::string oid; + bool critical = false; + std::vector value; +}; + class X509View final { public: static X509View From(const SSLPointer& ssl); @@ -1655,6 +1678,11 @@ class X509View final { Result getPublicKey() const; StackOfASN1 getKeyUsage() const; + std::optional getVersion() const; + std::optional inspectSubjectName() const; + std::optional inspectIssuerName() const; + std::optional> inspectExtensions() const; + bool isCA() const; bool isIssuedBy(const X509View& other) const; bool checkPrivateKey(const EVPKeyPointer& pkey) const; diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 769992815023..d588771d191f 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -3152,6 +3152,47 @@ added: v15.6.0 Checks whether the public key for this certificate is consistent with the given private key. +### `x509.extensions` + + + +* Type: {Object\[]} + * `oid` {string} The canonical dotted-decimal extension OID. + * `critical` {boolean} Whether the extension is critical. + * `value` {Buffer} The bytes contained in the extension's `extnValue` OCTET + STRING. The OCTET STRING tag and length are not included. + +One entry is returned for every extension, in the order in which it appears in +the certificate. Unknown extensions and repeated OIDs are included. The +contents of `value` are not decoded according to `oid`. A certificate with no +extensions produces an empty array. + +Unlike [`x509.subjectAltName`][], [`x509.infoAccess`][], and +[`x509.keyUsage`][] (Extended Key Usage), which decode or format selected +extension types, this property exposes raw extension contents. `value` contains +the exact extension value bytes, not a complete encoded `Extension` record. +The extension-specific syntax is not validated. + +Each access returns a new array containing new objects and `Buffer`s. Modifying +them does not affect the certificate or later accesses. + +For example, a Basic Constraints extension allowing a certificate to act as a +CA can appear as: + +```js +console.log(x509.extensions[0]); +// { +// oid: '2.5.29.19', +// critical: true, +// value: +// } +``` + +Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the +extensions cannot be inspected. + ### `x509.fingerprint` + +* Type: {Object} + * `raw` {Buffer} The complete DER-encoded `Name`. + * `rdns` {Array\} The relative distinguished name sequence. + * `oid` {string} The canonical dotted-decimal object identifier. + * `value` {string} A UTF-8 representation of the attribute value. This + property is omitted when the ASN.1 value type is not supported for text + conversion. + +The issuer distinguished name, also exposed as a formatted string by +[`x509.issuer`][]. The outer `rdns` array contains the relative distinguished +names in encoded order. Each inner array represents one relative distinguished +name and contains its attributes in encoded order. A valid empty name has an +empty `rdns` array. + +`rdns` is a decoded view and does not include ASN.1 value types. Use `raw` for a +lossless representation of the complete name, including attribute values whose +`value` property is omitted from `rdns`. + +Each access returns newly allocated objects, arrays, and a `Buffer`. +Modifying them does not affect the certificate or later accesses. + +For example: + +```js +console.log(x509.issuerName.rdns); +// [ +// [ { oid: '2.5.4.6', value: 'US' } ], +// [ { oid: '2.5.4.10', value: 'Example CA' } ] +// ] +``` + +Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the +issuer name cannot be inspected. + ### `x509.keyUsage` + +* Type: {Object} + * `raw` {Buffer} The complete DER-encoded `Name`. + * `rdns` {Array\} The relative distinguished name sequence. + * `oid` {string} The canonical dotted-decimal object identifier. + * `value` {string} A UTF-8 representation of the attribute value. This + property is omitted when the ASN.1 value type is not supported for text + conversion. + +The subject distinguished name, also exposed as a formatted string by +[`x509.subject`][]. The outer `rdns` array contains the relative distinguished +names in encoded order. Each inner array represents one relative distinguished +name and contains its attributes in encoded order. A valid empty name has an +empty `rdns` array. + +`rdns` is a decoded view and does not include ASN.1 value types. Use `raw` for a +lossless representation of the complete name, including attribute values whose +`value` property is omitted from `rdns`. + +Each access returns newly allocated objects, arrays, and a `Buffer`. +Modifying them does not affect the certificate or later accesses. + +For example, attributes in the same inner array belong to one multi-valued +relative distinguished name: + +```js +console.log(x509.subjectName.rdns); +// [ +// [ { oid: '2.5.4.6', value: 'US' } ], +// [ +// { oid: '2.5.4.11', value: 'Security' }, +// { oid: '2.5.4.10', value: 'Example Corp' } +// ], +// [ { oid: '2.5.4.3', value: 'example.com' } ] +// ] +``` + +Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the +subject name cannot be inspected. + +### `x509.tbs` + + + +* Type: {Buffer} + +Unlike [`x509.raw`][], which contains the whole certificate, this property +contains only the complete encoded `TBSCertificate`: the exact bytes covered +by the certificate's signature. They are copied from the certificate encoding, +not reconstructed from decoded fields. + +Each access returns a new `Buffer`. Modifying it does not affect the certificate +or later accesses. + +Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the +`TBSCertificate` cannot be inspected. + ### `x509.toJSON()` + +* Type: {Object} + * `algorithm` {Object} The certificate-level `signatureAlgorithm` paired with + the `signatureValue`. + * `oid` {string} The canonical dotted-decimal algorithm OID. + * `parameters` {Buffer} The complete DER-encoded ASN.1 parameters. This + property is omitted when the parameters are absent. + * `value` {Buffer} The signature octets contained in the certificate's + `signatureValue` BIT STRING. + +`algorithm.oid` identifies the same certificate-level algorithm as +[`x509.signatureAlgorithmOid`][]. In addition to the algorithm identification +provided by that property and [`x509.signatureAlgorithm`][], this property +exposes the algorithm parameters and signature bytes. + +The parameters are DER-encoded independently; the complete encoded +`AlgorithmIdentifier` and BIT STRING wrappers are not returned. Accessing this +property does not verify the signature. + +Each access returns new outer and `algorithm` objects, with new `Buffer`s for +`value` and `parameters` when present. Modifying them does not affect the +certificate or later accesses. + +For example, a certificate signed using RSA with SHA-256 can produce: + +```js +console.log(x509.signature); +// { +// algorithm: { +// oid: '1.2.840.113549.1.1.11', +// parameters: +// }, +// value: +// } +``` + +Accessing this property throws `ERR_CRYPTO_INVALID_X509_CERTIFICATE` if the +signature cannot be inspected. + ### `x509.signatureAlgorithm` + +* Type: {number} + +The certificate version: `1` for X.509 v1, `2` for X.509 v2, or `3` for X.509 +v3. A certificate with no encoded version field has version `1`. + ### `x509.verify(publicKey)` + +An X.509 certificate field could not be inspected. + ### `ERR_CRYPTO_JOB_INIT_FAILED` diff --git a/lib/internal/crypto/x509.js b/lib/internal/crypto/x509.js index cf1de4474756..68353718b098 100644 --- a/lib/internal/crypto/x509.js +++ b/lib/internal/crypto/x509.js @@ -46,6 +46,7 @@ const { const { codes: { + ERR_CRYPTO_INVALID_X509_CERTIFICATE, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_THIS, @@ -133,22 +134,28 @@ const { fingerprint512: nativeFingerprint512, getIssuerCert: nativeGetIssuerCert, infoAccess: nativeInfoAccess, + issuerName: nativeIssuerName, issuer: nativeIssuer, + extensions: nativeExtensions, keyUsage: nativeKeyUsage, pem: nativePem, publicKey: nativePublicKey, raw: nativeRaw, serialNumber: nativeSerialNumber, + signature: nativeSignature, signatureAlgorithm: nativeSignatureAlgorithm, signatureAlgorithmOid: nativeSignatureAlgorithmOid, subject: nativeSubject, + subjectName: nativeSubjectName, subjectAltName: nativeSubjectAltName, toLegacy: nativeToLegacy, validFrom: nativeValidFrom, validFromDate: nativeValidFromDate, validTo: nativeValidTo, validToDate: nativeValidToDate, + version: nativeVersion, verify: nativeVerify, + tbs: nativeTbs, } = NativeX509Certificate.prototype; let getState; @@ -163,6 +170,16 @@ const { return state[index]; } + function getInspectionValue(certificate, getter) { + getState(certificate); + const value = FunctionPrototypeCall(getter, certificate); + if (value === undefined) { + throw new ERR_CRYPTO_INVALID_X509_CERTIFICATE( + 'the certificate cannot be inspected'); + } + return value; + } + class X509Certificate { constructor(buffer) { if (typeof buffer === 'string') @@ -294,6 +311,30 @@ const { this, kSlotSignatureAlgorithmOid, nativeSignatureAlgorithmOid); } + get version() { + return getInspectionValue(this, nativeVersion); + } + + get subjectName() { + return getInspectionValue(this, nativeSubjectName); + } + + get issuerName() { + return getInspectionValue(this, nativeIssuerName); + } + + get extensions() { + return getInspectionValue(this, nativeExtensions); + } + + get tbs() { + return getInspectionValue(this, nativeTbs); + } + + get signature() { + return getInspectionValue(this, nativeSignature); + } + get raw() { return getCachedValue(this, kSlotRaw, nativeRaw); } diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 88c4102eee4f..e2ee2cdd340d 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1194,6 +1194,8 @@ E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', 'Invalid key object type %s, expected %s.', TypeError); E('ERR_CRYPTO_INVALID_MAC', 'Invalid MAC: %s', TypeError); E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); +E('ERR_CRYPTO_INVALID_X509_CERTIFICATE', + 'Invalid X.509 certificate: %s', Error); E('ERR_CRYPTO_KEM_NOT_SUPPORTED', 'KEM is not supported', Error); E('ERR_CRYPTO_MAC_FINALIZED', 'MAC already finalized', Error); E('ERR_CRYPTO_MAC_NOT_SUPPORTED', 'MAC is not supported', Error); diff --git a/src/crypto/crypto_x509.cc b/src/crypto/crypto_x509.cc index 3c208082f944..044fc55be774 100644 --- a/src/crypto/crypto_x509.cc +++ b/src/crypto/crypto_x509.cc @@ -28,6 +28,7 @@ using ncrypto::Digest; using ncrypto::MarkPopErrorOnReturn; using ncrypto::SSLPointer; using ncrypto::X509Name; +using ncrypto::X509NameInspection; using ncrypto::X509Pointer; using ncrypto::X509View; using v8::Array; @@ -375,6 +376,380 @@ MaybeLocal GetKeyUsage(Environment* env, const X509View& cert) { return Array::New(env->isolate(), vec.data(), vec.size()); } +bool SetValue(Local context, + Local object, + Local name, + Local value) { + return object->CreateDataProperty(context, name, value).FromMaybe(false); +} + +bool SetArrayValue(Local context, + Local array, + uint32_t index, + Local value) { + return array->CreateDataProperty(context, index, value).FromMaybe(false); +} + +MaybeLocal CopyBuffer(Environment* env, + ncrypto::Buffer value) { + const char* data = + value.len == 0 ? "" : reinterpret_cast(value.data); + return Buffer::Copy(env, data, value.len); +} + +MaybeLocal CopyBuffer(Environment* env, + const std::vector& value) { + return CopyBuffer(env, {.data = value.data(), .len = value.size()}); +} + +std::optional OidToText(const ASN1_OBJECT* object) { + if (object == nullptr) return std::nullopt; + const int length = OBJ_obj2txt(nullptr, 0, object, 1); + if (length <= 0 || length == std::numeric_limits::max()) { + return std::nullopt; + } + std::string output(static_cast(length) + 1, '\0'); + if (OBJ_obj2txt(output.data(), length + 1, object, 1) != length) { + return std::nullopt; + } + output.resize(static_cast(length)); + return output; +} + +struct SequenceSpan final { + ncrypto::Buffer encoded; + ncrypto::Buffer content; +}; + +bool ReadSequence(ncrypto::Buffer input, + SequenceSpan* out) { + if (out == nullptr || input.data == nullptr || input.len == 0 || + input.len > static_cast(LONG_MAX)) { + return false; + } + + const unsigned char* const start = input.data; + const unsigned char* content = start; + long content_length = 0; // NOLINT(runtime/int) + int tag = 0; + int tag_class = 0; + const int flags = + ASN1_get_object(&content, + &content_length, + &tag, + &tag_class, + static_cast(input.len)); // NOLINT(runtime/int) + constexpr int kIndefiniteLength = 0x01; + constexpr int kDecodeError = 0x80; + if ((flags & (kIndefiniteLength | kDecodeError)) != 0 || + (flags & V_ASN1_CONSTRUCTED) == 0 || tag != V_ASN1_SEQUENCE || + tag_class != V_ASN1_UNIVERSAL || content_length < 0) { + return false; + } + + const size_t header_length = static_cast(content - start); + const size_t length = static_cast(content_length); + if (header_length > input.len || length > input.len - header_length) { + return false; + } + *out = { + .encoded = {.data = start, .len = header_length + length}, + .content = {.data = content, .len = length}, + }; + return true; +} + +std::optional> GetExactTbs( + const X509CertificateSnapshot::DER& certificate) { + SequenceSpan outer; + const ncrypto::Buffer encoded{ + .data = certificate.data(), + .len = certificate.size(), + }; + if (!ReadSequence(encoded, &outer) || outer.encoded.len != encoded.len) { + return std::nullopt; + } + + SequenceSpan tbs; + if (!ReadSequence(outer.content, &tbs)) return std::nullopt; + return tbs.encoded; +} + +bool GetNameDer(const X509_NAME* name, + ncrypto::Buffer* encoded) { + if (name == nullptr || encoded == nullptr) return false; + const unsigned char* data = nullptr; + size_t length = 0; + if (X509_NAME_get0_der(name, &data, &length) != 1 || + (data == nullptr && length != 0)) { + return false; + } + *encoded = {.data = data, .len = length}; + return true; +} + +MaybeLocal CreateName(Environment* env, + const X509NameInspection& native, + const X509_NAME* name) { + Local context = env->context(); + size_t rdn_count = 0; + for (const auto& attribute : native) { + if (attribute.set < 0) return {}; + const size_t set = static_cast(attribute.set); + if (set == rdn_count) { + rdn_count++; + } else if (rdn_count == 0 || set != rdn_count - 1) { + return {}; + } + } + + if (rdn_count > static_cast(INT_MAX)) return {}; + Local rdns = Array::New(env->isolate(), static_cast(rdn_count)); + LocalVector groups(env->isolate()); + groups.reserve(rdn_count); + std::vector group_lengths(rdn_count, 0); + for (size_t i = 0; i < rdn_count; i++) { + Local group = Array::New(env->isolate()); + if (!SetArrayValue(context, rdns, i, group)) return {}; + groups.push_back(group); + } + + for (const auto& attribute : native) { + Local output = Object::New(env->isolate()); + if (!SetValue(context, + output, + env->oid_string(), + OneByteString(env->isolate(), attribute.oid))) { + return {}; + } + if (attribute.value.has_value()) { + Local value; + if (!ToV8Value(env, attribute.value.value()).ToLocal(&value) || + !SetValue(context, output, env->value_string(), value)) { + return {}; + } + } + const size_t rdn_index = static_cast(attribute.set); + if (!SetArrayValue( + context, groups[rdn_index], group_lengths[rdn_index]++, output)) { + return {}; + } + } + + ncrypto::Buffer encoded; + Local raw; + Local output = Object::New(env->isolate()); + if (!GetNameDer(name, &encoded) || !CopyBuffer(env, encoded).ToLocal(&raw) || + !SetValue(context, output, env->raw_string(), raw) || + !SetValue(context, output, env->rdns_string(), rdns)) { + return {}; + } + return output; +} + +MaybeLocal CreateRawExtensions( + Environment* env, + const std::vector& extensions) { + Local context = env->context(); + if (extensions.size() > static_cast(INT_MAX)) return {}; + Local output = + Array::New(env->isolate(), static_cast(extensions.size())); + for (size_t i = 0; i < extensions.size(); i++) { + const auto& extension = extensions[i]; + Local value = Object::New(env->isolate()); + Local bytes; + if (!CopyBuffer(env, extension.value).ToLocal(&bytes) || + !SetValue(context, + value, + env->oid_string(), + OneByteString(env->isolate(), extension.oid)) || + !SetValue(context, + value, + env->critical_string(), + Boolean::New(env->isolate(), extension.critical)) || + !SetValue(context, value, env->value_string(), bytes) || + !SetArrayValue(context, output, i, value)) { + return {}; + } + } + return output; +} + +MaybeLocal CreateAlgorithmIdentifier(Environment* env, + const X509_ALGOR* native) { + if (native == nullptr) return {}; + const ASN1_OBJECT* native_oid = nullptr; + int parameter_type = V_ASN1_UNDEF; + const void* parameter_value = nullptr; + X509_ALGOR_get0(&native_oid, ¶meter_type, ¶meter_value, native); + auto oid = OidToText(native_oid); + if (!oid.has_value()) return {}; + + Local context = env->context(); + Local output = Object::New(env->isolate()); + if (!SetValue(context, + output, + env->oid_string(), + OneByteString(env->isolate(), oid.value()))) { + return {}; + } + if (parameter_type != V_ASN1_UNDEF) { + ncrypto::DeleteFnPtr native_parameters( + ASN1_TYPE_new()); + if (!native_parameters || + ASN1_TYPE_set1( + native_parameters.get(), parameter_type, parameter_value) != 1) { + return {}; + } + const int length = i2d_ASN1_TYPE(native_parameters.get(), nullptr); + if (length <= 0) return {}; + std::vector encoded(static_cast(length)); + unsigned char* cursor = encoded.data(); + if (i2d_ASN1_TYPE(native_parameters.get(), &cursor) != length || + cursor != encoded.data() + encoded.size()) { + return {}; + } + + Local parameters; + if (!CopyBuffer(env, encoded).ToLocal(¶meters) || + !SetValue(context, output, env->parameters_string(), parameters)) { + return {}; + } + } + return output; +} + +MaybeLocal CreateSignature(Environment* env, + const X509View& certificate) { + const ASN1_BIT_STRING* native_signature = nullptr; + const X509_ALGOR* native_algorithm = nullptr; + X509_get0_signature(&native_signature, &native_algorithm, certificate.get()); + if (native_signature == nullptr || native_algorithm == nullptr || + ASN1_STRING_type(native_signature) != V_ASN1_BIT_STRING) { + return {}; + } + + const int native_length = ASN1_STRING_length(native_signature); + const unsigned char* native_data = ASN1_STRING_get0_data(native_signature); + if (native_length < 0 || (native_length > 0 && native_data == nullptr)) { + return {}; + } +#if NCRYPTO_HAS_ASN1_BIT_STRING_GET_LENGTH + size_t byte_length = 0; + int unused_bits = 0; + if (ASN1_BIT_STRING_get_length( + native_signature, &byte_length, &unused_bits) != 1 || + unused_bits != 0 || byte_length != static_cast(native_length)) { + return {}; + } +#elif NCRYPTO_USE_OPENSSL_PROVIDER || NCRYPTO_USE_BORINGSSL + if ((native_signature->flags & ASN1_STRING_FLAG_BITS_LEFT) != 0 && + (native_signature->flags & 0x07) != 0) { + return {}; + } +#else +#error "Unsupported crypto backend" +#endif + + const ncrypto::Buffer signature{ + .data = native_data, + .len = static_cast(native_length), + }; + + Local algorithm; + Local value; + if (!CreateAlgorithmIdentifier(env, native_algorithm).ToLocal(&algorithm) || + !CopyBuffer(env, signature).ToLocal(&value)) { + return {}; + } + + Local context = env->context(); + Local output = Object::New(env->isolate()); + if (!SetValue(context, output, env->algorithm_string(), algorithm) || + !SetValue(context, output, env->value_string(), value)) { + return {}; + } + return output; +} + +void Version(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + X509Certificate* certificate; + ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This()); + const auto version = certificate->view().getVersion(); + if (!version.has_value()) return; + args.GetReturnValue().Set( + Integer::New(env->isolate(), static_cast(version.value()))); +} + +void StructuredName(const FunctionCallbackInfo& args, bool subject) { + Environment* env = Environment::GetCurrent(args); + X509Certificate* certificate; + ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This()); + + const auto native = subject ? certificate->view().inspectSubjectName() + : certificate->view().inspectIssuerName(); + if (!native.has_value()) return; + + MarkPopErrorOnReturn mark_pop_error_on_return; + const X509_NAME* name = subject ? X509_get_subject_name(certificate->get()) + : X509_get_issuer_name(certificate->get()); + Local output; + if (!CreateName(env, native.value(), name).ToLocal(&output)) { + return; + } + args.GetReturnValue().Set(output); +} + +void SubjectName(const FunctionCallbackInfo& args) { + StructuredName(args, true); +} + +void IssuerName(const FunctionCallbackInfo& args) { + StructuredName(args, false); +} + +void Extensions(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + X509Certificate* certificate; + ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This()); + + const auto extensions = certificate->view().inspectExtensions(); + Local output; + if (!extensions.has_value() || + !CreateRawExtensions(env, extensions.value()).ToLocal(&output)) { + return; + } + args.GetReturnValue().Set(output); +} + +void Tbs(const FunctionCallbackInfo& args) { + MarkPopErrorOnReturn mark_pop_error_on_return; + Environment* env = Environment::GetCurrent(args); + X509Certificate* certificate; + ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This()); + + const auto tbs = GetExactTbs(certificate->snapshot_der()); + Local output; + if (!tbs.has_value() || !CopyBuffer(env, tbs.value()).ToLocal(&output)) { + return; + } + args.GetReturnValue().Set(output); +} + +void Signature(const FunctionCallbackInfo& args) { + MarkPopErrorOnReturn mark_pop_error_on_return; + Environment* env = Environment::GetCurrent(args); + X509Certificate* certificate; + ASSIGN_OR_RETURN_UNWRAP(&certificate, args.This()); + + Local output; + if (!CreateSignature(env, certificate->view()).ToLocal(&output)) { + return; + } + args.GetReturnValue().Set(output); +} + void Pem(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); X509Certificate* cert; @@ -994,6 +1369,12 @@ Local X509Certificate::GetConstructorTemplate( SetProtoMethodNoSideEffect(isolate, tmpl, "serialNumber", SerialNumber); SetProtoMethodNoSideEffect(isolate, tmpl, "pem", Pem); SetProtoMethodNoSideEffect(isolate, tmpl, "raw", Der); + SetProtoMethodNoSideEffect(isolate, tmpl, "version", Version); + SetProtoMethodNoSideEffect(isolate, tmpl, "subjectName", SubjectName); + SetProtoMethodNoSideEffect(isolate, tmpl, "issuerName", IssuerName); + SetProtoMethodNoSideEffect(isolate, tmpl, "extensions", Extensions); + SetProtoMethodNoSideEffect(isolate, tmpl, "tbs", Tbs); + SetProtoMethodNoSideEffect(isolate, tmpl, "signature", Signature); SetProtoMethodNoSideEffect(isolate, tmpl, "publicKey", PublicKey); SetProtoMethodNoSideEffect(isolate, tmpl, "checkCA", CheckCA); SetProtoMethodNoSideEffect(isolate, tmpl, "checkHost", CheckHost); @@ -1356,6 +1737,12 @@ void X509Certificate::RegisterExternalReferences( registry->Register(SerialNumber); registry->Register(Pem); registry->Register(Der); + registry->Register(Version); + registry->Register(SubjectName); + registry->Register(IssuerName); + registry->Register(Extensions); + registry->Register(Tbs); + registry->Register(Signature); registry->Register(PublicKey); registry->Register(CheckCA); registry->Register(CheckHost); diff --git a/src/crypto/crypto_x509.h b/src/crypto/crypto_x509.h index 44714d1bcca2..a618fcac880a 100644 --- a/src/crypto/crypto_x509.h +++ b/src/crypto/crypto_x509.h @@ -124,6 +124,9 @@ class X509Certificate final : public BaseObject { inline ncrypto::X509View view() const { return *cert_; } inline X509* get() { return cert_->get(); } + inline const X509CertificateSnapshot::DER& snapshot_der() const { + return snapshot_->at(snapshot_index_); + } v8::MaybeLocal toObject(Environment* env); static v8::MaybeLocal toObject(Environment* env, diff --git a/src/env_properties.h b/src/env_properties.h index c4e3bc5c72a0..8fffc3b74d36 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -112,6 +112,7 @@ V(code_string, "code") \ V(config_string, "config") \ V(constants_string, "constants") \ + V(critical_string, "critical") \ V(cwd_string, "cwd") \ V(data_string, "data") \ V(database_string, "database") \ @@ -244,6 +245,7 @@ V(node_string, "node") \ V(object_string, "Object") \ V(ocsp_request_string, "OCSPRequest") \ + V(oid_string, "oid") \ V(ok_string, "ok") \ V(oncertcb_string, "oncertcb") \ V(onchange_string, "onchange") \ @@ -276,6 +278,7 @@ V(p256_string, "P-256") \ V(p384_string, "P-384") \ V(p521_string, "P-521") \ + V(parameters_string, "parameters") \ V(parse_error_string, "Parse Error") \ V(password_string, "password") \ V(path_string, "path") \ @@ -301,6 +304,8 @@ V(psk_string, "psk") \ V(public_exponent_string, "publicExponent") \ V(rate_string, "rate") \ + V(raw_string, "raw") \ + V(rdns_string, "rdns") \ V(read_host_object_string, "_readHostObject") \ V(readable_string, "readable") \ V(read_bigints_string, "readBigInts") \ diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc index 45d5b7facc39..bed36b150154 100644 --- a/test/cctest/test_node_crypto.cc +++ b/test/cctest/test_node_crypto.cc @@ -1022,6 +1022,14 @@ TEST(NodeCrypto, X509FormattingPreservesUnrelatedErrors) { const ncrypto::BIOPointer subject = cert.view().getSubject(); ASSERT_TRUE(subject); EXPECT_EQ(ERR_peek_error(), expected); + EXPECT_TRUE(cert.view().getVersion().has_value()); + EXPECT_EQ(ERR_peek_error(), expected); + EXPECT_TRUE(cert.view().inspectSubjectName().has_value()); + EXPECT_EQ(ERR_peek_error(), expected); + EXPECT_TRUE(cert.view().inspectIssuerName().has_value()); + EXPECT_EQ(ERR_peek_error(), expected); + EXPECT_TRUE(cert.view().inspectExtensions().has_value()); + EXPECT_EQ(ERR_peek_error(), expected); ERR_clear_error(); } diff --git a/test/parallel/test-crypto-x509-chain-snapshot.js b/test/parallel/test-crypto-x509-chain-snapshot.js index 5a0f696da0f1..0b1d3c995af0 100644 --- a/test/parallel/test-crypto-x509-chain-snapshot.js +++ b/test/parallel/test-crypto-x509-chain-snapshot.js @@ -20,12 +20,46 @@ const issuer = new X509Certificate(fixtures.readKey('ca3-cert.pem')); function assertChain(actual) { assert(actual instanceof X509Certificate); assert.deepStrictEqual(actual.raw, leaf.raw); + assertInspection(actual, leaf); assert.strictEqual(actual.issuerCertificate, actual.issuerCertificate); assert(actual.issuerCertificate instanceof X509Certificate); assert.deepStrictEqual(actual.issuerCertificate.raw, issuer.raw); + assertInspection(actual.issuerCertificate, issuer); assert.strictEqual(actual.issuerCertificate.issuerCertificate, undefined); } +function assertInspection(actual, expected) { + assert.strictEqual(actual.version, expected.version); + + const subjectName = actual.subjectName; + const issuerName = actual.issuerName; + const extensions = actual.extensions; + const tbs = actual.tbs; + const signature = actual.signature; + + assert.deepStrictEqual(subjectName, expected.subjectName); + assert.deepStrictEqual(issuerName, expected.issuerName); + assert.deepStrictEqual(extensions, expected.extensions); + assert.deepStrictEqual(tbs, expected.tbs); + assert.deepStrictEqual(signature, expected.signature); + + subjectName.raw.fill(0); + issuerName.raw.fill(0); + for (const extension of extensions) extension.value.fill(0); + subjectName.rdns.length = 0; + issuerName.rdns.length = 0; + extensions.length = 0; + tbs.fill(0); + signature.value.fill(0); + signature.algorithm.parameters?.fill(0); + + assert.deepStrictEqual(actual.subjectName, expected.subjectName); + assert.deepStrictEqual(actual.issuerName, expected.issuerName); + assert.deepStrictEqual(actual.extensions, expected.extensions); + assert.deepStrictEqual(actual.tbs, expected.tbs); + assert.deepStrictEqual(actual.signature, expected.signature); +} + function legacyChainRaw(certificate) { const result = []; const seen = new Set(); diff --git a/test/parallel/test-crypto-x509-inspection.js b/test/parallel/test-crypto-x509-inspection.js new file mode 100644 index 000000000000..54ee7dae359a --- /dev/null +++ b/test/parallel/test-crypto-x509-inspection.js @@ -0,0 +1,281 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { X509Certificate } = require('crypto'); +const { isBoringSSL } = require('../common/crypto'); +const fixtures = require('../common/fixtures'); + +function load(name) { + return new X509Certificate(fixtures.readKey(name)); +} + +function readDer(input, offset = 0) { + const start = offset; + const tag = input[offset++]; + let length = input[offset++]; + if ((length & 0x80) !== 0) { + const octets = length & 0x7f; + length = 0; + for (let i = 0; i < octets; i++) + length = (length * 256) + input[offset++]; + } + return { + tag, + start, + contentStart: offset, + end: offset + length, + }; +} + +function readChildren(input, parent) { + const children = []; + for (let offset = parent.contentStart; offset < parent.end;) { + const child = readDer(input, offset); + children.push(child); + offset = child.end; + } + return children; +} + +function encodeDer(tag, content) { + let length; + if (content.length < 128) { + length = Buffer.of(content.length); + } else { + const octets = []; + for (let value = content.length; value !== 0; value >>>= 8) + octets.unshift(value & 0xff); + length = Buffer.of(0x80 | octets.length, ...octets); + } + return Buffer.concat([Buffer.of(tag), length, content]); +} + +function rawElement(input, element) { + return input.subarray(element.start, element.end); +} + +function appendExtensions(raw, additions) { + const certificate = readDer(raw); + const certificateFields = readChildren(raw, certificate); + const tbs = certificateFields[0]; + const tbsFields = readChildren(raw, tbs); + const extensionIndex = tbsFields.findIndex(({ tag }) => tag === 0xa3); + assert.notStrictEqual(extensionIndex, -1); + + const wrapper = tbsFields[extensionIndex]; + const extensionSequence = readChildren(raw, wrapper)[0]; + const extensions = readChildren(raw, extensionSequence) + .map((element) => rawElement(raw, element)); + extensions.push(...additions); + + const newWrapper = encodeDer(0xa3, encodeDer( + 0x30, Buffer.concat(extensions))); + const newTbsFields = tbsFields.map((element, index) => + (index === extensionIndex ? newWrapper : rawElement(raw, element))); + const newTbs = encodeDer(0x30, Buffer.concat(newTbsFields)); + const newCertificateFields = certificateFields.map((element, index) => + (index === 0 ? newTbs : rawElement(raw, element))); + return encodeDer(0x30, Buffer.concat(newCertificateFields)); +} + +function replaceVersion(raw, value) { + const certificate = readDer(raw); + const certificateFields = readChildren(raw, certificate); + const tbs = certificateFields[0]; + const tbsFields = readChildren(raw, tbs); + assert.strictEqual(tbsFields[0].tag, 0xa0); + const version = encodeDer(0xa0, encodeDer(0x02, value)); + const newTbs = encodeDer(0x30, Buffer.concat(tbsFields.map( + (element, index) => (index === 0 ? version : rawElement(raw, element))))); + return encodeDer(0x30, Buffer.concat(certificateFields.map( + (element, index) => (index === 0 ? newTbs : rawElement(raw, element))))); +} + +function replaceSubject(raw, value) { + const certificate = readDer(raw); + const certificateFields = readChildren(raw, certificate); + const tbs = certificateFields[0]; + const tbsFields = readChildren(raw, tbs); + const fieldOffset = tbsFields[0].tag === 0xa0 ? 1 : 0; + const subjectIndex = fieldOffset + 4; + const newTbs = encodeDer(0x30, Buffer.concat(tbsFields.map( + (element, index) => + (index === subjectIndex ? value : rawElement(raw, element))))); + return encodeDer(0x30, Buffer.concat(certificateFields.map( + (element, index) => (index === 0 ? newTbs : rawElement(raw, element))))); +} + +const keyUsageExtension = Buffer.from( + '300b0603551d0f0404030205a0', 'hex'); +const unknownExtension = Buffer.from( + '300906032a030404020500', 'hex'); +const criticalUnknownExtension = Buffer.from( + '300c06032a03040101ff04020500', 'hex'); + +{ + const cert = load('agent1-cert.pem'); + const raw = cert.raw; + const certificate = readDer(raw); + const [tbs] = readChildren(raw, certificate); + const tbsFields = readChildren(raw, tbs); + const fieldOffset = tbsFields[0].tag === 0xa0 ? 1 : 0; + const issuer = tbsFields[fieldOffset + 2]; + const subject = tbsFields[fieldOffset + 4]; + + assert.strictEqual(cert.version, 3); + assert.strictEqual(cert.subjectName.rdns.at(-1)[0].oid, + '1.2.840.113549.1.9.1'); + assert.strictEqual(cert.subjectName.rdns.at(-1)[0].value, + 'ry@tinyclouds.org'); + assert.deepStrictEqual(cert.subjectName.raw, rawElement(raw, subject)); + assert.strictEqual(cert.issuerName.rdns.at(-2)[0].value, 'ca1'); + assert.deepStrictEqual(cert.issuerName.raw, rawElement(raw, issuer)); + + assert.deepStrictEqual(cert.extensions.map(({ oid, critical }) => ({ + oid, + critical, + })), [{ + oid: '1.3.6.1.5.5.7.1.1', + critical: false, + }]); + assert(cert.extensions[0].value.length > 0); + + assert.deepStrictEqual(cert.tbs, rawElement(raw, tbs)); + + assert.deepStrictEqual(cert.signature.algorithm, { + oid: '1.2.840.113549.1.1.11', + parameters: Buffer.from('0500', 'hex'), + }); + assert(cert.signature.value.length > 0); +} + +// Raw inspection preserves extension order, duplicates, criticality, and the +// exact bytes inside extnValue without interpreting the extension syntax. +{ + const source = load('agent1-cert.pem'); + const cert = new X509Certificate(appendExtensions(source.raw, [ + keyUsageExtension, + unknownExtension, + criticalUnknownExtension, + ])); + + assert.deepStrictEqual(cert.extensions.slice(-3), [ + { + oid: '2.5.29.15', + critical: false, + value: Buffer.from('030205a0', 'hex'), + }, + { + oid: '1.2.3.4', + critical: false, + value: Buffer.from('0500', 'hex'), + }, + { + oid: '1.2.3.4', + critical: true, + value: Buffer.from('0500', 'hex'), + }, + ]); +} + +// An empty Name is valid, but an RDN is a nonempty SET OF attributes. OpenSSL +// does not expose empty RDNs through X509_NAME entries, so inspection must not +// silently collapse this malformed Name into a valid empty one. +{ + const source = load('agent1-cert.pem'); + const emptyRdnName = encodeDer(0x30, encodeDer(0x31, Buffer.alloc(0))); + const malformed = replaceSubject(source.raw, emptyRdnName); + let cert; + try { + cert = new X509Certificate(malformed); + } catch (err) { + if (!isBoringSSL || err.code !== 'ERR_OSSL_PEM_NO_START_LINE') { + throw err; + } + } + if (cert !== undefined) { + assert.throws(() => cert.subjectName, { + code: 'ERR_CRYPTO_INVALID_X509_CERTIFICATE', + }); + } +} + +// Reject unsupported versions at whichever native parsing layer observes them +// first. Backends that accept the encoding must reject it before converting +// OpenSSL's long result to the public one-based version number. +{ + const source = load('agent1-cert.pem'); + let cert; + try { + cert = new X509Certificate(replaceVersion( + source.raw, Buffer.from('7fffffffffffffff', 'hex'))); + } catch (err) { + assert.strictEqual(err.code, isBoringSSL ? + 'ERR_OSSL_PEM_NO_START_LINE' : 'ERR_OSSL_X509_INVALID_VERSION'); + } + if (cert !== undefined) { + assert.throws(() => cert.version, { + code: 'ERR_CRYPTO_INVALID_X509_CERTIFICATE', + }); + } +} + +// Inspection uses the immutable native snapshot, not the mutable Buffer cached +// by the legacy raw getter. +{ + const cert = load('agent1-cert.pem'); + cert.raw.fill(0); + assert.strictEqual(cert.version, 3); + assert.strictEqual(cert.subjectName.rdns.at(-1)[0].value, + 'ry@tinyclouds.org'); +} + +// New structured getters return fresh defensive values. Existing legacy +// getters retain their current identity behavior in this backportable change. +{ + const cert = load('leaf-from-intermediate-cert.pem'); + const subjectName = cert.subjectName; + const subjectRaw = subjectName.raw; + const extensions = cert.extensions; + const tbs = cert.tbs; + const signature = cert.signature; + const parameters = signature.algorithm.parameters; + + subjectName.rdns.length = 0; + subjectRaw.fill(0); + extensions.length = 0; + tbs.fill(0); + signature.value.fill(0); + parameters.fill(0); + + assert(cert.subjectName.rdns.length > 0); + assert.notDeepStrictEqual(cert.subjectName.raw, subjectRaw); + assert(cert.extensions.length > 0); + assert.notStrictEqual(cert.tbs[0], 0); + assert.notStrictEqual(cert.signature.value[0], 0); + assert.notStrictEqual(cert.signature.algorithm.parameters[0], 0); + assert.strictEqual(cert.raw, cert.raw); +} + +for (const name of [ + 'version', + 'subjectName', + 'issuerName', + 'extensions', + 'tbs', + 'signature', +]) { + const descriptor = Object.getOwnPropertyDescriptor( + X509Certificate.prototype, name); + assert.strictEqual(typeof descriptor.get, 'function'); + assert.strictEqual(descriptor.set, undefined); + assert.strictEqual(descriptor.enumerable, false); + assert.strictEqual(descriptor.configurable, true); + assert.throws(() => descriptor.get.call({}), { + code: 'ERR_INVALID_THIS', + }); +} diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index 3d5a9fceb2e6..6082eb7010bc 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -723,6 +723,32 @@ declare namespace InternalCryptoBinding { getIssuer(): Buffer | null; } + interface X509NameAttribute { + oid: string; + value?: string; + } + + interface X509Name { + raw: Buffer; + rdns: X509NameAttribute[][]; + } + + interface X509RawExtension { + oid: string; + critical: boolean; + value: Buffer; + } + + interface X509AlgorithmIdentifier { + oid: string; + parameters?: Buffer; + } + + interface X509Signature { + algorithm: X509AlgorithmIdentifier; + value: Buffer; + } + interface X509CertificateHandle { subject(): string; subjectAltName(): string | undefined; @@ -740,6 +766,12 @@ declare namespace InternalCryptoBinding { serialNumber(): string; signatureAlgorithm(): string | undefined; signatureAlgorithmOid(): string | undefined; + version(): number | undefined; + subjectName(): X509Name | undefined; + issuerName(): X509Name | undefined; + extensions(): X509RawExtension[] | undefined; + tbs(): Buffer | undefined; + signature(): X509Signature | undefined; raw(): Buffer; publicKey(): KeyObjectHandle; pem(): string;