nodejs/node · #65649
crypto: fix multi-prime RSA JWKs
deps/ncrypto/ncrypto.cc122 + / 1 −
@@ -130,6 +130,31 @@ struct OpenSSLBufferDeleter { }; using OpenSSLBufferPointer = std::unique_ptr<unsigned char, OpenSSLBufferDeleter>;++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<RsaOtherPrimeParamNames, 8> 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<const BIGNUM*> factors(count);+ std::vector<const BIGNUM*> exponents(count);+ std::vector<const BIGNUM*> 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::PssParams> 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<BIGNUM*> factors;+ std::vector<BIGNUM*> exponents;+ std::vector<BIGNUM*> 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*>(rsa_),+ factors.data(),+ exponents.data(),+ coefficients.data(),+ static_cast<int>(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 }deps/ncrypto/ncrypto.h21 + / 1 −
@@ -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<BIGNUM, BN_clear_free> r;+ DeleteFnPtr<BIGNUM, BN_clear_free> d;+ DeleteFnPtr<BIGNUM, BN_clear_free> t;+ };+ using OtherPrimeInfos = std::vector<OtherPrimeInfo>;+ using OtherPrimeInfoPointers = std::vector<OtherPrimeInfoPointer>; struct PssParams { std::string_view digest = "sha1"; std::optional<std::string_view> 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<PssParams> 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<BIGNUM, BN_clear_free> dp_; DeleteFnPtr<BIGNUM, BN_clear_free> dq_; DeleteFnPtr<BIGNUM, BN_clear_free> qi_;+ OtherPrimeInfoPointers other_prime_infos_; std::optional<PssParams> pss_params_; #else OSSL3_CONST RSA* rsa_;src/crypto/crypto_rsa.cc99 + / 8 −
@@ -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<uint32_t>(other_prime_infos.size());+ Local<Array> oth = Array::New(env->isolate(), count);+ for (uint32_t i = 0; i < count; i++) {+ const auto& info = other_prime_infos[i];+ Local<Object> 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<Object> jwk) { Local<Value> n_value; Local<Value> e_value; Local<Value> d_value;+ Local<Value> 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<Object> 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<Object> jwk) { ByteSource dq = ByteSource::FromEncodedString(env, dq_value.As<String>()); ByteSource qi = ByteSource::FromEncodedString(env, qi_value.As<String>()); - 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<Array> oth = oth_value.As<Array>();+ 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<Value> item_value;+ Local<Value> r_value;+ Local<Value> other_d_value;+ Local<Value> 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<Object> item = item_value.As<Object>();+ 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<String>()).ToBN(),+ ByteSource::FromEncodedString(env, other_d_value.As<String>())+ .ToBN(),+ ByteSource::FromEncodedString(env, t_value.As<String>()).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");src/env_properties.h3 + / 0 −
@@ -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") \test/fixtures/keys/Makefile8 + / 0 −
@@ -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 test/fixtures/keys/rsa_private_2048_3_primes.pemadded29 + / 0 −
@@ -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-----test/parallel/test-crypto-rsa-multiprime-jwk.jsadded62 + / 0 −
@@ -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());