diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 8b707b4f89c4..785176571913 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -130,6 +130,31 @@ struct OpenSSLBufferDeleter { }; using OpenSSLBufferPointer = std::unique_ptr; + +struct RsaOtherPrimeParamNames { + const char* factor; + const char* exponent; + const char* coefficient; +}; + +#define RSA_OTHER_PRIME_PARAM_NAMES(prime, coefficient) \ + { \ + OSSL_PKEY_PARAM_RSA_FACTOR #prime, OSSL_PKEY_PARAM_RSA_EXPONENT #prime, \ + OSSL_PKEY_PARAM_RSA_COEFFICIENT #coefficient \ + } + +constexpr std::array kRsaOtherPrimeParamNames = {{ + RSA_OTHER_PRIME_PARAM_NAMES(3, 2), + RSA_OTHER_PRIME_PARAM_NAMES(4, 3), + RSA_OTHER_PRIME_PARAM_NAMES(5, 4), + RSA_OTHER_PRIME_PARAM_NAMES(6, 5), + RSA_OTHER_PRIME_PARAM_NAMES(7, 6), + RSA_OTHER_PRIME_PARAM_NAMES(8, 7), + RSA_OTHER_PRIME_PARAM_NAMES(9, 8), + RSA_OTHER_PRIME_PARAM_NAMES(10, 9), +}}; + +#undef RSA_OTHER_PRIME_PARAM_NAMES #endif static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = @@ -3081,6 +3106,19 @@ EVPKeyPointer EVPKeyPointer::NewRSA(const Rsa& rsa) { bld.get(), OSSL_PKEY_PARAM_RSA_COEFFICIENT1, private_key.qi) != 1) { return {}; } + + const auto other_prime_infos = rsa.getOtherPrimeInfos(); + if (other_prime_infos.size() > kRsaOtherPrimeParamNames.size()) return {}; + for (size_t i = 0; i < other_prime_infos.size(); i++) { + const auto& info = other_prime_infos[i]; + const auto& names = kRsaOtherPrimeParamNames[i]; + if (info.r == nullptr || info.d == nullptr || info.t == nullptr || + OSSL_PARAM_BLD_push_BN(bld.get(), names.factor, info.r) != 1 || + OSSL_PARAM_BLD_push_BN(bld.get(), names.exponent, info.d) != 1 || + OSSL_PARAM_BLD_push_BN(bld.get(), names.coefficient, info.t) != 1) { + return {}; + } + } selection = EVP_PKEY_KEYPAIR; } @@ -6134,6 +6172,11 @@ DataPointer CipherImpl(const EVPKeyPointer& key, } } // namespace +Rsa::OtherPrimeInfoPointer::OtherPrimeInfoPointer(BignumPointer&& r, + BignumPointer&& d, + BignumPointer&& t) + : r(r.release()), d(d.release()), t(t.release()) {} + #if NCRYPTO_USE_OPENSSL3_PROVIDER namespace { int DigestAlgorithmIdentifierToNid(const unsigned char* data, size_t size) { @@ -6362,6 +6405,19 @@ Rsa::Rsa(const EVP_PKEY* pkey) : Rsa() { return; } + for (const auto& names : kRsaOtherPrimeParamNames) { + OtherPrimeInfoPointer info; + if (!GetOptionalPKeyBnParam(pkey, names.factor, &info.r) || + !GetOptionalPKeyBnParam(pkey, names.exponent, &info.d) || + !GetOptionalPKeyBnParam(pkey, names.coefficient, &info.t)) { + return; + } + + if (!info.r && !info.d && !info.t) break; + if (!info.r || !info.d || !info.t) return; + other_prime_infos_.push_back(std::move(info)); + } + if (type == EVP_PKEY_RSA_PSS) { MarkPopErrorOnReturn pop_errors; PssParams params; @@ -6400,6 +6456,35 @@ const Rsa::PrivateKey Rsa::getPrivateKey() const { #endif } +const Rsa::OtherPrimeInfos Rsa::getOtherPrimeInfos() const { + OtherPrimeInfos infos; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + infos.reserve(other_prime_infos_.size()); + for (const auto& info : other_prime_infos_) { + infos.push_back({info.r.get(), info.d.get(), info.t.get()}); + } +#elif NCRYPTO_USE_LEGACY_OPENSSL + if (rsa_ == nullptr) return infos; + const int count = RSA_get_multi_prime_extra_count(rsa_); + if (count <= 0) return infos; + + std::vector factors(count); + std::vector exponents(count); + std::vector coefficients(count); + if (RSA_get0_multi_prime_factors(rsa_, factors.data()) != 1 || + RSA_get0_multi_prime_crt_params( + rsa_, exponents.data(), coefficients.data()) != 1) { + return {}; + } + + infos.reserve(count); + for (int i = 0; i < count; i++) { + infos.push_back({factors[i], exponents[i], coefficients[i]}); + } +#endif + return infos; +} + const std::optional Rsa::getPssParams() const { #if NCRYPTO_USE_OPENSSL3_PROVIDER return pss_params_; @@ -6501,15 +6586,20 @@ bool Rsa::setPrivateKey(BignumPointer&& d, BignumPointer&& p, BignumPointer&& dp, BignumPointer&& dq, - BignumPointer&& qi) { + BignumPointer&& qi, + OtherPrimeInfoPointers&& other_prime_infos) { #if NCRYPTO_USE_OPENSSL3_PROVIDER if (!d || !q || !p || !dp || !dq || !qi) return false; + for (const auto& info : other_prime_infos) { + if (!info.r || !info.d || !info.t) return false; + } d_.reset(d.release()); q_.reset(q.release()); p_.reset(p.release()); dp_.reset(dp.release()); dq_.reset(dq.release()); qi_.reset(qi.release()); + other_prime_infos_ = std::move(other_prime_infos); rsa_ = n_ != nullptr && e_ != nullptr; return rsa_; #else @@ -6531,6 +6621,37 @@ bool Rsa::setPrivateKey(BignumPointer&& d, dp.release(); dq.release(); qi.release(); + +#if NCRYPTO_USE_LEGACY_OPENSSL + if (!other_prime_infos.empty()) { + std::vector factors; + std::vector exponents; + std::vector coefficients; + factors.reserve(other_prime_infos.size()); + exponents.reserve(other_prime_infos.size()); + coefficients.reserve(other_prime_infos.size()); + for (const auto& info : other_prime_infos) { + if (!info.r || !info.d || !info.t) return false; + factors.push_back(info.r.get()); + exponents.push_back(info.d.get()); + coefficients.push_back(info.t.get()); + } + if (RSA_set0_multi_prime_params(const_cast(rsa_), + factors.data(), + exponents.data(), + coefficients.data(), + static_cast(factors.size())) != 1) { + return false; + } + for (auto& info : other_prime_infos) { + info.r.release(); + info.d.release(); + info.t.release(); + } + } +#else + if (!other_prime_infos.empty()) return false; +#endif return true; #endif } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 79f403788cf4..6b1edceed061 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -724,6 +724,23 @@ class Rsa final { const BIGNUM* dq; const BIGNUM* qi; }; + struct OtherPrimeInfo { + const BIGNUM* r; + const BIGNUM* d; + const BIGNUM* t; + }; + struct OtherPrimeInfoPointer { + OtherPrimeInfoPointer() = default; + OtherPrimeInfoPointer(BignumPointer&& r, + BignumPointer&& d, + BignumPointer&& t); + + DeleteFnPtr r; + DeleteFnPtr d; + DeleteFnPtr t; + }; + using OtherPrimeInfos = std::vector; + using OtherPrimeInfoPointers = std::vector; struct PssParams { std::string_view digest = "sha1"; std::optional mgf1_digest = "sha1"; @@ -732,6 +749,7 @@ class Rsa final { const PublicKey getPublicKey() const; const PrivateKey getPrivateKey() const; + const OtherPrimeInfos getOtherPrimeInfos() const; const std::optional getPssParams() const; bool setPublicKey(BignumPointer&& n, BignumPointer&& e); @@ -740,7 +758,8 @@ class Rsa final { BignumPointer&& p, BignumPointer&& dp, BignumPointer&& dq, - BignumPointer&& qi); + BignumPointer&& qi, + OtherPrimeInfoPointers&& other_prime_infos = {}); using CipherParams = Cipher::CipherParams; @@ -765,6 +784,7 @@ class Rsa final { DeleteFnPtr dp_; DeleteFnPtr dq_; DeleteFnPtr qi_; + OtherPrimeInfoPointers other_prime_infos_; std::optional pss_params_; #else OSSL3_CONST RSA* rsa_; diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index e80c70c961df..f6919221a4f2 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -22,6 +22,7 @@ using ncrypto::EVPKeyPointer; #if NCRYPTO_USE_LEGACY_KEY_TYPES using ncrypto::RSAPointer; #endif +using v8::Array; using v8::ArrayBuffer; using v8::BackingStoreInitializationMode; using v8::FunctionCallbackInfo; @@ -39,6 +40,8 @@ using v8::Value; namespace crypto { namespace { +constexpr uint32_t kMaxRsaOtherPrimeInfos = 8; + bool IsRsaPssDigestEncodable(const Digest& digest) { #if NCRYPTO_USE_OPENSSL3_PROVIDER const int nid = EVP_MD_type(digest.get()); @@ -339,6 +342,29 @@ bool ExportJWKRsaKey(Environment* env, .IsNothing()) { return false; } + + const auto other_prime_infos = rsa.getOtherPrimeInfos(); + if (!other_prime_infos.empty()) { + const uint32_t count = static_cast(other_prime_infos.size()); + Local oth = Array::New(env->isolate(), count); + for (uint32_t i = 0; i < count; i++) { + const auto& info = other_prime_infos[i]; + Local item = Object::New(env->isolate()); + if (SetEncodedValue(env, item, env->jwk_r_string(), info.r) + .IsNothing() || + SetEncodedValue(env, item, env->jwk_d_string(), info.d) + .IsNothing() || + SetEncodedValue(env, item, env->jwk_t_string(), info.t) + .IsNothing() || + !oth->Set(env->context(), i, item).FromMaybe(false)) { + return false; + } + } + if (!target->DefineOwnProperty(env->context(), env->jwk_oth_string(), oth) + .FromMaybe(false)) { + return false; + } + } } return true; @@ -348,12 +374,13 @@ KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { Local n_value; Local e_value; Local d_value; + Local oth_value; if (!jwk->Get(env->context(), env->jwk_n_string()).ToLocal(&n_value) || !jwk->Get(env->context(), env->jwk_e_string()).ToLocal(&e_value) || !jwk->Get(env->context(), env->jwk_d_string()).ToLocal(&d_value) || - !n_value->IsString() || - !e_value->IsString()) { + !jwk->Get(env->context(), env->jwk_oth_string()).ToLocal(&oth_value) || + !n_value->IsString() || !e_value->IsString()) { THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); return {}; } @@ -364,6 +391,10 @@ KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { } KeyType type = d_value->IsString() ? kKeyTypePrivate : kKeyTypePublic; + if (type == kKeyTypePublic && !oth_value->IsUndefined()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); + return {}; + } #if NCRYPTO_USE_OPENSSL3_PROVIDER ncrypto::Rsa rsa_view; @@ -417,19 +448,79 @@ KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { ByteSource dq = ByteSource::FromEncodedString(env, dq_value.As()); ByteSource qi = ByteSource::FromEncodedString(env, qi_value.As()); - if (!rsa_view.setPrivateKey( - d.ToBN(), q.ToBN(), p.ToBN(), dp.ToBN(), dq.ToBN(), qi.ToBN())) { + ncrypto::Rsa::OtherPrimeInfoPointers other_prime_infos; + if (!oth_value->IsUndefined()) { + if (!oth_value->IsArray()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); + return {}; + } + + Local oth = oth_value.As(); + const uint32_t length = oth->Length(); + if (length == 0 || length > kMaxRsaOtherPrimeInfos) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); + return {}; + } + other_prime_infos.reserve(length); + for (uint32_t i = 0; i < length; i++) { + Local item_value; + Local r_value; + Local other_d_value; + Local t_value; + if (!oth->Get(env->context(), i).ToLocal(&item_value) || + !item_value->IsObject()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); + return {}; + } + + Local item = item_value.As(); + if (!item->Get(env->context(), env->jwk_r_string()).ToLocal(&r_value) || + !item->Get(env->context(), env->jwk_d_string()) + .ToLocal(&other_d_value) || + !item->Get(env->context(), env->jwk_t_string()).ToLocal(&t_value) || + !r_value->IsString() || !other_d_value->IsString() || + !t_value->IsString()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); + return {}; + } + + other_prime_infos.push_back({ + ByteSource::FromEncodedString(env, r_value.As()).ToBN(), + ByteSource::FromEncodedString(env, other_d_value.As()) + .ToBN(), + ByteSource::FromEncodedString(env, t_value.As()).ToBN(), + }); + } + } + + if (!rsa_view.setPrivateKey(d.ToBN(), + q.ToBN(), + p.ToBN(), + dp.ToBN(), + dq.ToBN(), + qi.ToBN(), + std::move(other_prime_infos))) { THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); return {}; } - // Verify that n == p * q. + // Verify that n is the product of all prime factors. const auto& pub = rsa_view.getPublicKey(); const auto& priv = rsa_view.getPrivateKey(); - auto pq = BignumPointer::New(); + auto product = BignumPointer::New(); BN_CTX* ctx = BN_CTX_new(); - bool n_valid = ctx && pq && BN_mul(pq.get(), priv.p, priv.q, ctx) == 1 && - BN_cmp(pq.get(), pub.n) == 0; + bool n_valid = + ctx && product && BN_mul(product.get(), priv.p, priv.q, ctx) == 1; + for (const auto& info : rsa_view.getOtherPrimeInfos()) { + auto next = BignumPointer::New(); + if (!n_valid || !next || + BN_mul(next.get(), product.get(), info.r, ctx) != 1) { + n_valid = false; + break; + } + product = std::move(next); + } + n_valid = n_valid && BN_cmp(product.get(), pub.n) == 0; BN_CTX_free(ctx); if (!n_valid) { THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key"); diff --git a/src/env_properties.h b/src/env_properties.h index 886d4adba9fc..e8e01fe5bb3c 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -240,12 +240,15 @@ V(jwk_n_string, "n") \ V(jwk_oct_string, "oct") \ V(jwk_okp_string, "OKP") \ + V(jwk_oth_string, "oth") \ V(jwk_p_string, "p") \ V(jwk_priv_string, "priv") \ V(jwk_pub_string, "pub") \ V(jwk_q_string, "q") \ V(jwk_qi_string, "qi") \ + V(jwk_r_string, "r") \ V(jwk_rsa_string, "RSA") \ + V(jwk_t_string, "t") \ V(jwk_x_string, "x") \ V(jwk_y_string, "y") \ V(kill_signal_string, "killSignal") \ diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index 3ef61d00afff..0960e69f36fd 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -70,6 +70,7 @@ all: \ rsa_spkac.spkac \ rsa_spkac_invalid.spkac \ rsa_private_2048.pem \ + rsa_private_2048_3_primes.pem \ rsa_private_4096.pem \ rsa_public_2048.pem \ rsa_public_4096.pem \ @@ -927,6 +928,13 @@ rsa_spkac_invalid.spkac: rsa_spkac.spkac rsa_private_2048.pem: openssl genrsa -out rsa_private_2048.pem 2048 +rsa_private_2048_3_primes.pem: + openssl genpkey \ + -algorithm RSA \ + -pkeyopt rsa_keygen_bits:2048 \ + -pkeyopt rsa_keygen_primes:3 \ + -out rsa_private_2048_3_primes.pem + rsa_private_4096.pem: openssl genrsa -out rsa_private_4096.pem 4096 diff --git a/test/fixtures/keys/rsa_private_2048_3_primes.pem b/test/fixtures/keys/rsa_private_2048_3_primes.pem new file mode 100644 index 000000000000..49535f2cd7f7 --- /dev/null +++ b/test/fixtures/keys/rsa_private_2048_3_primes.pem @@ -0,0 +1,29 @@ +-----BEGIN PRIVATE KEY----- +MIIE8wIBADANBgkqhkiG9w0BAQEFAASCBN0wggTZAgEBAoIBAQC1Lu9a9Hlj8/OA +J3I2h/1aFYV59SxWb1KzbuRb2nAoOVWCqG8ettkT7kGN6qva3C/oCw2pYPIlRxpX +0Vve0fNRqpSCCXcnN9vlKpU0BrHfpIHuXSleT7vwSq70UV5YwjFhL+g5wcCUVCuz +Fkd8dYb0keqVApSKkcydepk5/BwBwQdkfibs3RuIjROkgIbDKSXrm1lFinqyCBTG +umLcaL9XMhaJRxsVIVqkCM6hs+kEXhIBvlFIqcycECKEV3l71nV0VM3RwlbORIjx +iHpSvNEUixGR0Rwx4WdJNU4GiUJGZACKj+RrLX+mh0odOl5r8Up9TkTgWp4UEdUx +GHbQ6lIrAgMBAAECggEAA8mZY1Nc/kPpQTKR5qXtG+b7RVNku+HT3sBgsbLdSVn5 +eaO/vuqfmxUN9cOEGC0mKzmf7XyDU1grKs7LltrYtUpVlGpSPVWjmSTMvauuKDiu +Jqd35+irM+pQerj/AYOs9Lv2lLzzA1ivcSmB0uR2gNyiVvVxBAxydNXiBwCn+v8M ++qosREPVN9eEQ+1j+ZWvZ2j/AkT6EDdOl0qbhkLDkaLgDkO4uhAZfNKjwmuVkBoW +BI9bUiQv9vzsSp7AIVXwd8D+NaX0VuDpbcIq/tIfrl2OxwUDuTkYYsvwlIwvAkZ4 +yMl57x2YjRJu2u4f4t+fuagqCisbbbE+LywM5r+YEQJWB/Z/lOoAXXCRSBSGhI29 +zI+pXWiK4UneitEKTJpdR7i38Var03aciK+q64z4gWDNSBRzYq5eyVWosv0f9WkO +lQFRfOy0HM68dg2KYDCIVnquCwwgaDcCVgZ7lxyKqwirCDYL9VFxRNiBIOXuMLdl +6c4YNpdPTrp/AILBeINJYKBYjpro6fPe0YHcxJpMNf5Izaow17VVAqJKDmXbca7y +SclsgBR6mAnFf6WfFM5rAlYBe2nHp74lphwEVH2tEkyvVFXrotemkv8uyzWliF2w +SbtC2iTlGQCNmFSxvu1J92B9+AB574aI5r6dget+2+zw4uvt2oizi+8b8e09dgsK +PY9DUyB9BQJWBTsT4uPAvW3DYlijyOMsOcV3Vg+rH1AMJ09CPTSwwapYLykMpGJA +pSDPcA8fNrdDNRV9TULRDiyQRhLC+n1tcqVumKFISXEWOIBjmCKTKs9A/9IsZ6EC +VgQG607Arq/ztxJFVKHY4RW0v8Ofx9Q0h+GDcKqqxDlymZfrDsdKJjC1QTofRUR+ +JJiwshahF2jBtlsPwEjX8/InboaF2f2VlIxzxe2+GpzWeRgzIN9yMIIBDDCCAQgC +VgOChKyWwWrVMfjdfwtFnMA/OjwK4KBtR8VWwAapaiJPFhZRM0jh8EMsC03X7xm9 +AbL3hfE3eLU2mRLxw1pPh0RfK2xtKTCZWjgnQDhPznMUUvF+FYxHAlYCZbRvGa1U +Zffy7n7m2Q4+tTD2b8uLZ/jcjess4awvcOs6JQ4tWr07dCroaXPTlahCcBRZdLUK +uRcGvIDc/3L5vTMwpMK8ghIARrbzf3cYXSpl7eOT4wJWAuPXcrk1PE1YuMidPa2v +3OshgH2no6BeVW2vChBDnK9IBstWII/AhjQzDHjbj9S8xjLZA6bqHH89uXc+0Z/2 +hF8lLDsruEceLVckmk7wb1eFAvns/XA= +-----END PRIVATE KEY----- diff --git a/test/parallel/test-crypto-rsa-multiprime-jwk.js b/test/parallel/test-crypto-rsa-multiprime-jwk.js new file mode 100644 index 000000000000..be434c76e828 --- /dev/null +++ b/test/parallel/test-crypto-rsa-multiprime-jwk.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { hasFIPS } = require('../common/crypto'); +const { + createPrivateKey, +} = require('crypto'); +const { subtle } = globalThis.crypto; + +if (process.features.openssl_is_boringssl) + common.skip('multi-prime RSA is not available with BoringSSL'); +if (hasFIPS()) + common.skip('multi-prime RSA is not available in FIPS mode'); + +const privateKey = createPrivateKey( + fixtures.readKey('rsa_private_2048_3_primes.pem')); +const pkcs8 = privateKey.export({ format: 'der', type: 'pkcs8' }); +const jwk = privateKey.export({ format: 'jwk' }); + +assert.strictEqual(jwk.oth.length, 1); +assert.deepStrictEqual(Object.keys(jwk.oth[0]), ['r', 'd', 't']); + +const importedKey = createPrivateKey({ key: jwk, format: 'jwk' }); +assert.deepStrictEqual(importedKey.export({ format: 'jwk' }), jwk); +assert.deepStrictEqual( + importedKey.export({ format: 'der', type: 'pkcs8' }), + pkcs8); + +for (const field of ['r', 'd', 't']) { + const invalidJwk = { + ...jwk, + oth: [{ ...jwk.oth[0] }], + }; + delete invalidJwk.oth[0][field]; + assert.throws( + () => createPrivateKey({ key: invalidJwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); +} + +(async () => { + const algorithm = { name: 'RSA-PSS', hash: 'SHA-256' }; + const cryptoKey = await subtle.importKey( + 'pkcs8', pkcs8, algorithm, true, ['sign']); + const exportedJwk = await subtle.exportKey('jwk', cryptoKey); + + const exportedKeyMaterial = { ...exportedJwk }; + delete exportedKeyMaterial.key_ops; + delete exportedKeyMaterial.ext; + delete exportedKeyMaterial.alg; + assert.deepStrictEqual(exportedKeyMaterial, jwk); + + const importedCryptoKey = await subtle.importKey( + 'jwk', exportedJwk, algorithm, true, ['sign']); + assert.deepStrictEqual( + Buffer.from(await subtle.exportKey('pkcs8', importedCryptoKey)), + pkcs8); +})().then(common.mustCall());