Skip to content

Commit b11b7da

Browse files
committed
crypto: stop swallowing X.509 operation errors
Conversion failures can currently look like absent certificate fields. Invalid validity times produce "Bad time value" or Invalid Date, so date comparisons can fail without reporting the invalid input. Throw for failed field conversion, certificate encoding, fingerprint calculation, and decoding of present AIA or EKU extensions. Stop legacy certificate conversion at the first error. Report failed or unsupported private-key comparisons separately from a key mismatch. In verify(), throw on negative verification results except for a wrong key type or invalid unused bits in the signature BIT STRING. Those cases and a zero verification result return false. Fixes: #67285 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex
1 parent f15ec18 commit b11b7da

8 files changed

Lines changed: 524 additions & 129 deletions

File tree

‎deps/ncrypto/ncrypto.cc‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <openssl/rand.h>
1010
#include <openssl/x509v3.h>
1111
#if NCRYPTO_USE_OPENSSL_PROVIDER
12+
#include <openssl/asn1err.h>
1213
#include <openssl/decoder.h>
1314
#endif
1415
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
@@ -1484,13 +1485,15 @@ BIOPointer X509View::getIssuer() const {
14841485
return bio;
14851486
}
14861487

1487-
BIOPointer X509View::getInfoAccess() const {
1488+
BIOPointer X509View::getInfoAccess(bool* has_extension) const {
14881489
MarkPopErrorOnReturn mark_pop_error_on_return;
1490+
if (has_extension != nullptr) *has_extension = false;
14891491
if (cert_ == nullptr) return {};
1492+
const int index = X509_get_ext_by_NID(cert_, NID_info_access, -1);
1493+
if (index < 0) return {};
1494+
if (has_extension != nullptr) *has_extension = true;
14901495
BIOPointer bio(BIO_new(BIO_s_mem()));
14911496
if (!bio) return {};
1492-
int index = X509_get_ext_by_NID(cert_, NID_info_access, -1);
1493-
if (index < 0) return {};
14941497
const X509_EXTENSION* extension = X509_get_ext(cert_, index);
14951498
if (extension == nullptr || !SafeX509InfoAccessPrint(bio, extension)) {
14961499
return {};
@@ -1850,10 +1853,22 @@ X509View::CheckKeyResult X509View::checkPrivateKey(
18501853
}
18511854
}
18521855

1853-
bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const {
1856+
Result<bool, int> X509View::checkPublicKey(const EVPKeyPointer& pkey) const {
18541857
ClearErrorOnReturn clearErrorOnReturn;
1855-
if (cert_ == nullptr || pkey == nullptr) return false;
1856-
return X509_verify(const_cast<X509*>(cert_), pkey.get()) == 1;
1858+
if (cert_ == nullptr || pkey == nullptr) return Result<bool, int>(-1);
1859+
const int result = X509_verify(const_cast<X509*>(cert_), pkey.get());
1860+
if (result >= 0) return Result<bool, int>(result == 1);
1861+
1862+
const auto error = ERR_peek_last_error();
1863+
#if NCRYPTO_USE_OPENSSL_PROVIDER
1864+
// A wrong key type or malformed signature BIT STRING cannot verify.
1865+
if (ERR_GET_LIB(error) == ERR_LIB_ASN1 &&
1866+
(ERR_GET_REASON(error) == ASN1_R_WRONG_PUBLIC_KEY_TYPE ||
1867+
ERR_GET_REASON(error) == ASN1_R_INVALID_BIT_STRING_BITS_LEFT)) {
1868+
return Result<bool, int>(false);
1869+
}
1870+
#endif
1871+
return Result<bool, int>(int{result}, error);
18571872
}
18581873

18591874
X509View::CheckMatch X509View::checkHost(const std::string_view host,
@@ -1972,15 +1987,19 @@ Result<X509Pointer, int> X509Pointer::Parse(
19721987
return Result<X509Pointer, int>(ERR_get_error());
19731988
}
19741989

1975-
bool X509View::enumUsages(UsageCallback callback) const {
1976-
if (cert_ == nullptr) return false;
1990+
X509View::UsageResult X509View::enumUsages(UsageCallback callback) const {
1991+
if (cert_ == nullptr) return UsageResult::OPERATION_FAILED;
19771992
StackOfASN1 eku;
1993+
int critical = -1;
19781994
{
19791995
MarkPopErrorOnReturn mark_pop_error_on_return;
19801996
eku.reset(static_cast<STACK_OF(ASN1_OBJECT)*>(
1981-
X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr)));
1997+
X509_get_ext_d2i(cert_, NID_ext_key_usage, &critical, nullptr)));
1998+
}
1999+
if (!eku) {
2000+
// The decoder reports absence as -1 and duplicate extensions as -2.
2001+
return critical == -1 ? UsageResult::ABSENT : UsageResult::OPERATION_FAILED;
19822002
}
1983-
if (!eku) return false;
19842003
const int count = sk_ASN1_OBJECT_num(eku.get());
19852004

19862005
for (int i = 0; i < count; i++) {
@@ -1989,10 +2008,10 @@ bool X509View::enumUsages(UsageCallback callback) const {
19892008
MarkPopErrorOnReturn mark_pop_error_on_return;
19902009
usage = ObjectToText(sk_ASN1_OBJECT_value(eku.get(), i), true);
19912010
}
1992-
if (!usage.has_value()) return false;
2011+
if (!usage.has_value()) return UsageResult::OPERATION_FAILED;
19932012
callback(usage->c_str());
19942013
}
1995-
return true;
2014+
return UsageResult::PRESENT;
19962015
}
19972016

19982017
bool X509View::ifRsa(KeyCallback<Rsa> callback) const {

‎deps/ncrypto/ncrypto.h‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,7 +1731,7 @@ class X509View final {
17311731
BIOPointer getSubject() const;
17321732
BIOPointer getSubjectAltName(bool* has_extension = nullptr) const;
17331733
BIOPointer getIssuer() const;
1734-
BIOPointer getInfoAccess() const;
1734+
BIOPointer getInfoAccess(bool* has_extension = nullptr) const;
17351735
BIOPointer getValidFrom() const;
17361736
BIOPointer getValidTo() const;
17371737
std::optional<std::string_view> getSignatureAlgorithm() const;
@@ -1750,7 +1750,7 @@ class X509View final {
17501750
bool isCA() const;
17511751
bool isIssuedBy(const X509View& other) const;
17521752
CheckKeyResult checkPrivateKey(const EVPKeyPointer& pkey) const;
1753-
bool checkPublicKey(const EVPKeyPointer& pkey) const;
1753+
Result<bool, int> checkPublicKey(const EVPKeyPointer& pkey) const;
17541754

17551755
std::optional<std::string> getFingerprint(const Digest& method) const;
17561756

@@ -1768,8 +1768,13 @@ class X509View final {
17681768
CheckMatch checkEmail(std::string_view email, int flags) const;
17691769
CheckMatch checkIp(const Buffer<const unsigned char>& ip, int flags) const;
17701770

1771+
enum class UsageResult {
1772+
ABSENT,
1773+
PRESENT,
1774+
OPERATION_FAILED,
1775+
};
17711776
using UsageCallback = std::function<void(const char*)>;
1772-
bool enumUsages(UsageCallback callback) const;
1777+
UsageResult enumUsages(UsageCallback callback) const;
17731778

17741779
template <typename T>
17751780
using KeyCallback = std::function<bool(const T& t)>;

‎doc/api/crypto.md‎

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2967,6 +2967,11 @@ be passed instead of a public key.
29672967

29682968
<!-- YAML
29692969
added: v15.6.0
2970+
changes:
2971+
- version: REPLACEME
2972+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
2973+
description: Required field conversion, certificate encoding, fingerprint
2974+
calculation, and AIA or EKU decoding failures now throw.
29702975
-->
29712976

29722977
Encapsulates an X509 certificate and provides read-only access to
@@ -3144,6 +3149,10 @@ if (!x509.verify(otherCert.publicKey)) {
31443149

31453150
<!-- YAML
31463151
added: v15.6.0
3152+
changes:
3153+
- version: REPLACEME
3154+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
3155+
description: Unsupported key comparisons and comparison errors now throw.
31473156
-->
31483157

31493158
* `privateKey` {KeyObject} A private key.
@@ -3152,6 +3161,10 @@ added: v15.6.0
31523161
Checks whether the public key for this certificate is consistent with
31533162
the given private key.
31543163

3164+
Returns `false` if the keys do not match. Throws
3165+
`ERR_CRYPTO_UNSUPPORTED_OPERATION` if the keys cannot be compared,
3166+
or `ERR_CRYPTO_OPERATION_FAILED` if the comparison cannot be completed.
3167+
31553168
### `x509.extensions`
31563169

31573170
<!-- YAML
@@ -3203,6 +3216,8 @@ added: v15.6.0
32033216

32043217
The SHA-1 fingerprint of this certificate.
32053218

3219+
Throws if the fingerprint cannot be calculated.
3220+
32063221
Because SHA-1 is cryptographically broken and because the security of SHA-1 is
32073222
significantly worse than that of algorithms that are commonly used to sign
32083223
certificates, consider using [`x509.fingerprint256`][] instead.
@@ -3217,6 +3232,8 @@ added: v15.6.0
32173232

32183233
The SHA-256 fingerprint of this certificate.
32193234

3235+
Throws if the fingerprint cannot be calculated.
3236+
32203237
### `x509.fingerprint512`
32213238

32223239
<!-- YAML
@@ -3229,6 +3246,8 @@ added:
32293246

32303247
The SHA-512 fingerprint of this certificate.
32313248

3249+
Throws if the fingerprint cannot be calculated.
3250+
32323251
Because computing the SHA-256 fingerprint is usually faster and because it is
32333252
only half the size of the SHA-512 fingerprint, [`x509.fingerprint256`][] may be
32343253
a better choice. While SHA-512 presumably provides a higher level of security in
@@ -3248,10 +3267,11 @@ changes:
32483267
in response to CVE-2021-44532.
32493268
-->
32503269

3251-
* Type: {string}
3270+
* Type: {string|undefined}
32523271

32533272
A textual representation of the certificate's authority information access
3254-
extension.
3273+
extension, or `undefined` if the extension is absent. Throws if the extension
3274+
cannot be decoded or formatted.
32553275

32563276
This is a line feed separated list of access descriptions. Each line begins with
32573277
the access method and the kind of the access location, followed by a colon and
@@ -3273,6 +3293,8 @@ added: v15.6.0
32733293

32743294
The issuer identification included in this certificate.
32753295

3296+
Throws if the issuer name cannot be formatted.
3297+
32763298
### `x509.issuerCertificate`
32773299

32783300
<!-- YAML
@@ -3330,9 +3352,10 @@ issuer name cannot be inspected.
33303352
added: v15.6.0
33313353
-->
33323354

3333-
* Type: {string\[]}
3355+
* Type: {string\[]|undefined}
33343356

3335-
An array detailing the key extended usages for this certificate.
3357+
An array of extended key usage OIDs, or `undefined` if the extension is absent.
3358+
Throws if the extension cannot be decoded or an OID cannot be formatted.
33363359

33373360
### `x509.publicKey`
33383361

@@ -3354,6 +3377,8 @@ added: v15.6.0
33543377

33553378
A `Buffer` containing the DER encoding of this certificate.
33563379

3380+
Throws if the certificate cannot be encoded.
3381+
33573382
### `x509.serialNumber`
33583383

33593384
<!-- YAML
@@ -3364,6 +3389,8 @@ added: v15.6.0
33643389

33653390
The serial number of this certificate.
33663391

3392+
Throws if the serial number cannot be formatted.
3393+
33673394
Serial numbers are assigned by certificate authorities and do not uniquely
33683395
identify certificates. Consider using [`x509.fingerprint256`][] as a unique
33693396
identifier instead.
@@ -3378,6 +3405,8 @@ added: v15.6.0
33783405

33793406
The complete subject of this certificate.
33803407

3408+
Throws if the subject name cannot be formatted.
3409+
33813410
### `x509.subjectAltName`
33823411

33833412
<!-- YAML
@@ -3486,6 +3515,8 @@ There is no standard JSON encoding for X509 certificates. The
34863515
`toJSON()` method returns a string containing the PEM encoded
34873516
certificate.
34883517

3518+
Throws if the certificate cannot be encoded.
3519+
34893520
### `x509.toLegacyObject()`
34903521

34913522
<!-- YAML
@@ -3497,6 +3528,10 @@ added: v15.6.0
34973528
Returns information about this certificate using the legacy
34983529
[certificate object][] encoding.
34993530

3531+
Throws if a required field conversion, Authority Information Access or Extended
3532+
Key Usage decoding, fingerprint calculation, or certificate encoding fails.
3533+
Optional public key fields may be omitted as described in the legacy encoding.
3534+
35003535
### `x509.toString()`
35013536

35023537
<!-- YAML
@@ -3507,6 +3542,8 @@ added: v15.6.0
35073542

35083543
Returns the PEM-encoded certificate.
35093544

3545+
Throws if the certificate cannot be encoded.
3546+
35103547
### `x509.validFrom`
35113548

35123549
<!-- YAML
@@ -3517,6 +3554,8 @@ added: v15.6.0
35173554

35183555
The date/time from which this certificate is valid.
35193556

3557+
Throws if the time cannot be converted or formatted.
3558+
35203559
### `x509.validFromDate`
35213560

35223561
<!-- YAML
@@ -3529,6 +3568,8 @@ added:
35293568

35303569
The date/time from which this certificate is valid, encapsulated in a `Date` object.
35313570

3571+
Throws if the time cannot be converted.
3572+
35323573
### `x509.validTo`
35333574

35343575
<!-- YAML
@@ -3539,6 +3580,8 @@ added: v15.6.0
35393580

35403581
The date/time until which this certificate is valid.
35413582

3583+
Throws if the time cannot be converted or formatted.
3584+
35423585
### `x509.validToDate`
35433586

35443587
<!-- YAML
@@ -3551,6 +3594,8 @@ added:
35513594

35523595
The date/time until which this certificate is valid, encapsulated in a `Date` object.
35533596

3597+
Throws if the time cannot be converted.
3598+
35543599
### `x509.signature`
35553600

35563601
<!-- YAML
@@ -3615,6 +3660,8 @@ added: v24.9.0
36153660

36163661
The OID of the algorithm used to sign the certificate.
36173662

3663+
Throws if the OID cannot be formatted.
3664+
36183665
### `x509.version`
36193666

36203667
<!-- YAML
@@ -3630,6 +3677,10 @@ v3. A certificate with no encoded version field has version `1`.
36303677

36313678
<!-- YAML
36323679
added: v15.6.0
3680+
changes:
3681+
- version: REPLACEME
3682+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
3683+
description: Errors reported separately from verification failure now throw.
36333684
-->
36343685

36353686
* `publicKey` {KeyObject} A public key.
@@ -3638,6 +3689,11 @@ added: v15.6.0
36383689
Verifies that this certificate was signed by the given public key.
36393690
Does not perform any other validation checks on the certificate.
36403691

3692+
Returns `true` for a valid signature and `false` for a verification failure or
3693+
an incompatible key type. A signature BIT STRING with invalid unused bits also
3694+
returns `false`. Throws for other errors reported separately from verification
3695+
failure.
3696+
36413697
## `node:crypto` module methods and properties
36423698

36433699
### `crypto.argon2(algorithm, parameters, callback)`

0 commit comments

Comments
 (0)