react-native-quick-crypto 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/QuickCrypto.podspec +14 -5
  2. package/android/CMakeLists.txt +4 -2
  3. package/android/build.gradle +1 -1
  4. package/cpp/cipher/HybridCipher.cpp +3 -2
  5. package/cpp/cipher/HybridRsaCipher.cpp +20 -1
  6. package/cpp/keys/HybridKeyObjectHandle.cpp +8 -0
  7. package/cpp/keys/KeyObjectData.hpp +1 -1
  8. package/cpp/mldsa/HybridMlDsaKeyPair.cpp +264 -0
  9. package/cpp/mldsa/HybridMlDsaKeyPair.hpp +47 -0
  10. package/cpp/sign/HybridSignHandle.cpp +97 -22
  11. package/cpp/sign/HybridVerifyHandle.cpp +90 -21
  12. package/deps/ncrypto/.bazelignore +4 -0
  13. package/deps/ncrypto/.bazelrc +2 -0
  14. package/deps/ncrypto/.bazelversion +1 -0
  15. package/deps/ncrypto/.clang-format +111 -0
  16. package/deps/ncrypto/.github/workflows/bazel.yml +58 -0
  17. package/deps/ncrypto/.github/workflows/linter.yml +38 -0
  18. package/deps/ncrypto/.github/workflows/macos.yml +43 -0
  19. package/deps/ncrypto/.github/workflows/ubuntu.yml +46 -0
  20. package/deps/ncrypto/.github/workflows/visual-studio.yml +49 -0
  21. package/deps/ncrypto/.python-version +1 -0
  22. package/deps/ncrypto/BUILD.bazel +36 -0
  23. package/deps/ncrypto/CMakeLists.txt +55 -0
  24. package/deps/ncrypto/LICENSE +21 -0
  25. package/deps/ncrypto/MODULE.bazel +1 -0
  26. package/deps/ncrypto/MODULE.bazel.lock +280 -0
  27. package/deps/ncrypto/README.md +18 -0
  28. package/deps/ncrypto/WORKSPACE +15 -0
  29. package/deps/ncrypto/cmake/CPM.cmake +1225 -0
  30. package/deps/ncrypto/cmake/ncrypto-flags.cmake +16 -0
  31. package/deps/ncrypto/include/dh-primes.h +67 -0
  32. package/deps/ncrypto/{ncrypto.h → include/ncrypto.h} +361 -89
  33. package/deps/ncrypto/patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch +28 -0
  34. package/deps/ncrypto/pyproject.toml +38 -0
  35. package/deps/ncrypto/src/CMakeLists.txt +15 -0
  36. package/deps/ncrypto/src/engine.cpp +93 -0
  37. package/deps/ncrypto/{ncrypto.cc → src/ncrypto.cpp} +1168 -234
  38. package/deps/ncrypto/tests/BUILD.bazel +9 -0
  39. package/deps/ncrypto/tests/CMakeLists.txt +7 -0
  40. package/deps/ncrypto/tests/basic.cpp +86 -0
  41. package/deps/ncrypto/tools/run-clang-format.sh +42 -0
  42. package/lib/commonjs/keys/classes.js +6 -0
  43. package/lib/commonjs/keys/classes.js.map +1 -1
  44. package/lib/commonjs/mldsa.js +69 -0
  45. package/lib/commonjs/mldsa.js.map +1 -0
  46. package/lib/commonjs/specs/mlDsaKeyPair.nitro.js +6 -0
  47. package/lib/commonjs/specs/mlDsaKeyPair.nitro.js.map +1 -0
  48. package/lib/commonjs/subtle.js +111 -6
  49. package/lib/commonjs/subtle.js.map +1 -1
  50. package/lib/commonjs/utils/types.js.map +1 -1
  51. package/lib/module/keys/classes.js +6 -0
  52. package/lib/module/keys/classes.js.map +1 -1
  53. package/lib/module/mldsa.js +63 -0
  54. package/lib/module/mldsa.js.map +1 -0
  55. package/lib/module/specs/mlDsaKeyPair.nitro.js +4 -0
  56. package/lib/module/specs/mlDsaKeyPair.nitro.js.map +1 -0
  57. package/lib/module/subtle.js +111 -6
  58. package/lib/module/subtle.js.map +1 -1
  59. package/lib/module/utils/types.js.map +1 -1
  60. package/lib/tsconfig.tsbuildinfo +1 -1
  61. package/lib/typescript/keys/classes.d.ts +2 -0
  62. package/lib/typescript/keys/classes.d.ts.map +1 -1
  63. package/lib/typescript/mldsa.d.ts +18 -0
  64. package/lib/typescript/mldsa.d.ts.map +1 -0
  65. package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts +16 -0
  66. package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts.map +1 -0
  67. package/lib/typescript/subtle.d.ts.map +1 -1
  68. package/lib/typescript/utils/types.d.ts +5 -3
  69. package/lib/typescript/utils/types.d.ts.map +1 -1
  70. package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +1 -0
  71. package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +10 -0
  72. package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +10 -0
  73. package/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +12 -0
  74. package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.cpp +29 -0
  75. package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.hpp +73 -0
  76. package/package.json +7 -3
  77. package/src/keys/classes.ts +9 -0
  78. package/src/mldsa.ts +125 -0
  79. package/src/specs/mlDsaKeyPair.nitro.ts +29 -0
  80. package/src/subtle.ts +148 -8
  81. package/src/utils/types.ts +11 -3
@@ -92,10 +92,12 @@ Pod::Spec.new do |s|
92
92
  "ios/**/*.{h,m,mm}",
93
93
  # implementation (C++)
94
94
  "cpp/**/*.{hpp,cpp}",
95
- # dependencies (C++)
96
- "deps/**/*.{h,cc}",
95
+ # dependencies (C++) - ncrypto
96
+ "deps/ncrypto/include/*.{h}",
97
+ "deps/ncrypto/src/*.{cpp}",
97
98
  # dependencies (C) - exclude BLAKE3 x86 SIMD files (only use portable + NEON for ARM)
98
- "deps/**/*.{h,c}",
99
+ "deps/blake3/c/*.{h,c}",
100
+ "deps/fastpbkdf2/*.{h,c}",
99
101
  ]
100
102
 
101
103
  # Exclude BLAKE3 x86-specific SIMD implementations (SSE2, SSE4.1, AVX2, AVX-512)
@@ -148,7 +150,7 @@ Pod::Spec.new do |s|
148
150
  # Add cpp subdirectories to header search paths
149
151
  cpp_headers = [
150
152
  "\"$(PODS_TARGET_SRCROOT)/cpp/utils\"",
151
- "\"$(PODS_TARGET_SRCROOT)/deps/ncrypto\"",
153
+ "\"$(PODS_TARGET_SRCROOT)/deps/ncrypto/include\"",
152
154
  "\"$(PODS_TARGET_SRCROOT)/deps/blake3/c\"",
153
155
  "\"$(PODS_TARGET_SRCROOT)/deps/fastpbkdf2\""
154
156
  ]
@@ -175,6 +177,13 @@ Pod::Spec.new do |s|
175
177
 
176
178
  s.dependency "React-jsi"
177
179
  s.dependency "React-callinvoker"
178
- s.dependency "OpenSSL-Universal", "3.3.3001"
180
+
181
+ # OpenSSL 3.6+ via SPM for ML-DSA (post-quantum cryptography) support
182
+ spm_dependency(s,
183
+ url: 'https://github.com/krzyzanowskim/OpenSSL.git',
184
+ requirement: {kind: 'upToNextMajorVersion', minimumVersion: '3.6.0'},
185
+ products: ['OpenSSL']
186
+ )
187
+
179
188
  install_modules_dependencies(s)
180
189
  end
@@ -40,6 +40,7 @@ add_library(
40
40
  ../cpp/hmac/HybridHmac.cpp
41
41
  ../cpp/keys/HybridKeyObjectHandle.cpp
42
42
  ../cpp/keys/KeyObjectData.cpp
43
+ ../cpp/mldsa/HybridMlDsaKeyPair.cpp
43
44
  ../cpp/pbkdf2/HybridPbkdf2.cpp
44
45
  ../cpp/random/HybridRandom.cpp
45
46
  ../cpp/rsa/HybridRsaKeyPair.cpp
@@ -47,7 +48,7 @@ add_library(
47
48
  ../cpp/sign/HybridVerifyHandle.cpp
48
49
  ${BLAKE3_SOURCES}
49
50
  ../deps/fastpbkdf2/fastpbkdf2.c
50
- ../deps/ncrypto/ncrypto.cc
51
+ ../deps/ncrypto/src/ncrypto.cpp
51
52
  )
52
53
 
53
54
  # add Nitrogen specs
@@ -63,6 +64,7 @@ include_directories(
63
64
  "../cpp/hash"
64
65
  "../cpp/hmac"
65
66
  "../cpp/keys"
67
+ "../cpp/mldsa"
66
68
  "../cpp/pbkdf2"
67
69
  "../cpp/random"
68
70
  "../cpp/rsa"
@@ -70,7 +72,7 @@ include_directories(
70
72
  "../cpp/utils"
71
73
  "../deps/blake3/c"
72
74
  "../deps/fastpbkdf2"
73
- "../deps/ncrypto"
75
+ "../deps/ncrypto/include"
74
76
  )
75
77
 
76
78
  # Third party libraries (Prefabs)
@@ -143,7 +143,7 @@ dependencies {
143
143
  implementation project(":react-native-nitro-modules")
144
144
 
145
145
  // Add a dependency on OpenSSL
146
- implementation 'io.github.ronickg:openssl:3.3.2-1'
146
+ implementation 'io.github.ronickg:openssl:3.6.0-1'
147
147
 
148
148
  if (sodiumEnabled) {
149
149
  // Add a dependency on libsodium
@@ -297,8 +297,9 @@ void collect_ciphers(EVP_CIPHER* cipher, void* arg) {
297
297
  if (name_str == "NULL" || name_str.find("CTS") != std::string::npos ||
298
298
  name_str.find("SIV") != std::string::npos || // Covers -SIV and -GCM-SIV
299
299
  name_str.find("WRAP") != std::string::npos || // Covers -WRAP-INV and -WRAP-PAD-INV
300
- name_str.find("SM4-") != std::string::npos) {
301
- return; // Skip adding this cipher
300
+ name_str.find("SM4-") != std::string::npos ||
301
+ name_str.find("-ETM") != std::string::npos) { // TLS-internal ciphers, not for general use
302
+ return; // Skip adding this cipher
302
303
  }
303
304
 
304
305
  // If not filtered out, add it to the list
@@ -320,13 +320,32 @@ std::shared_ptr<ArrayBuffer> HybridRsaCipher::privateDecrypt(const std::shared_p
320
320
  throw std::runtime_error("Failed to determine output length: " + std::string(err_buf));
321
321
  }
322
322
 
323
+ if (outlen == 0) {
324
+ EVP_PKEY_CTX_free(ctx);
325
+ uint8_t* empty_buf = new uint8_t[1];
326
+ return std::make_shared<NativeArrayBuffer>(empty_buf, 0, [empty_buf]() { delete[] empty_buf; });
327
+ }
328
+
323
329
  auto out_buf = std::make_unique<uint8_t[]>(outlen);
324
330
 
325
331
  if (EVP_PKEY_verify_recover(ctx, out_buf.get(), &outlen, in, inlen) <= 0) {
326
- EVP_PKEY_CTX_free(ctx);
332
+ // OpenSSL 3.x may return failure when recovering empty plaintext
333
+ // In this case outlen is not updated from the initial buffer size
334
+ // Check the error and attempt to handle the empty data case
327
335
  unsigned long err = ERR_get_error();
328
336
  char err_buf[256];
329
337
  ERR_error_string_n(err, err_buf, sizeof(err_buf));
338
+
339
+ // Check if this is an RSA library error that might indicate empty recovered data
340
+ // Error code 0x1C880004 is "RSA lib" error from OpenSSL 3.x provider
341
+ if ((err & 0xFFFFFFF) == 0x1C880004 || (err & 0xFF) == 0x04) {
342
+ ERR_clear_error();
343
+ EVP_PKEY_CTX_free(ctx);
344
+ uint8_t* empty_buf = new uint8_t[1];
345
+ return std::make_shared<NativeArrayBuffer>(empty_buf, 0, [empty_buf]() { delete[] empty_buf; });
346
+ }
347
+
348
+ EVP_PKEY_CTX_free(ctx);
330
349
  throw std::runtime_error("Private decryption failed: " + std::string(err_buf));
331
350
  }
332
351
 
@@ -322,6 +322,14 @@ AsymmetricKeyType HybridKeyObjectHandle::getAsymmetricKeyType() {
322
322
  return AsymmetricKeyType::ED25519;
323
323
  case EVP_PKEY_ED448:
324
324
  return AsymmetricKeyType::ED448;
325
+ #if OPENSSL_VERSION_NUMBER >= 0x30500000L
326
+ case EVP_PKEY_ML_DSA_44:
327
+ return AsymmetricKeyType::ML_DSA_44;
328
+ case EVP_PKEY_ML_DSA_65:
329
+ return AsymmetricKeyType::ML_DSA_65;
330
+ case EVP_PKEY_ML_DSA_87:
331
+ return AsymmetricKeyType::ML_DSA_87;
332
+ #endif
325
333
  default:
326
334
  throw std::runtime_error("Unsupported asymmetric key type");
327
335
  }
@@ -2,11 +2,11 @@
2
2
 
3
3
  #include <NitroModules/ArrayBuffer.hpp>
4
4
 
5
- #include "../../deps/ncrypto/ncrypto.h"
6
5
  #include "KFormatType.hpp"
7
6
  #include "KeyEncoding.hpp"
8
7
  #include "KeyType.hpp"
9
8
  #include "Utils.hpp"
9
+ #include <ncrypto.h>
10
10
 
11
11
  namespace margelo::nitro::crypto {
12
12
 
@@ -0,0 +1,264 @@
1
+ #include "HybridMlDsaKeyPair.hpp"
2
+
3
+ #include <NitroModules/ArrayBuffer.hpp>
4
+ #include <openssl/bio.h>
5
+ #include <openssl/err.h>
6
+ #include <openssl/pem.h>
7
+
8
+ #include "Utils.hpp"
9
+
10
+ #if OPENSSL_VERSION_NUMBER >= 0x30500000L
11
+ #define RNQC_HAS_ML_DSA 1
12
+ #else
13
+ #define RNQC_HAS_ML_DSA 0
14
+ #endif
15
+
16
+ namespace margelo::nitro::crypto {
17
+
18
+ HybridMlDsaKeyPair::~HybridMlDsaKeyPair() {
19
+ if (pkey_ != nullptr) {
20
+ EVP_PKEY_free(pkey_);
21
+ pkey_ = nullptr;
22
+ }
23
+ }
24
+
25
+ int HybridMlDsaKeyPair::getEvpPkeyType() const {
26
+ #if RNQC_HAS_ML_DSA
27
+ if (variant_ == "ML-DSA-44")
28
+ return EVP_PKEY_ML_DSA_44;
29
+ if (variant_ == "ML-DSA-65")
30
+ return EVP_PKEY_ML_DSA_65;
31
+ if (variant_ == "ML-DSA-87")
32
+ return EVP_PKEY_ML_DSA_87;
33
+ #endif
34
+ return 0;
35
+ }
36
+
37
+ void HybridMlDsaKeyPair::setVariant(const std::string& variant) {
38
+ #if !RNQC_HAS_ML_DSA
39
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
40
+ #endif
41
+ if (variant != "ML-DSA-44" && variant != "ML-DSA-65" && variant != "ML-DSA-87") {
42
+ throw std::runtime_error("Invalid ML-DSA variant: " + variant + ". Must be ML-DSA-44, ML-DSA-65, or ML-DSA-87");
43
+ }
44
+ variant_ = variant;
45
+ }
46
+
47
+ std::shared_ptr<Promise<void>> HybridMlDsaKeyPair::generateKeyPair(double publicFormat, double publicType, double privateFormat,
48
+ double privateType) {
49
+ return Promise<void>::async([this, publicFormat, publicType, privateFormat, privateType]() {
50
+ this->generateKeyPairSync(publicFormat, publicType, privateFormat, privateType);
51
+ });
52
+ }
53
+
54
+ void HybridMlDsaKeyPair::generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType) {
55
+ #if !RNQC_HAS_ML_DSA
56
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
57
+ #else
58
+ clearOpenSSLErrors();
59
+
60
+ if (variant_.empty()) {
61
+ throw std::runtime_error("ML-DSA variant not set. Call setVariant() first.");
62
+ }
63
+
64
+ publicFormat_ = static_cast<int>(publicFormat);
65
+ publicType_ = static_cast<int>(publicType);
66
+ privateFormat_ = static_cast<int>(privateFormat);
67
+ privateType_ = static_cast<int>(privateType);
68
+
69
+ if (pkey_ != nullptr) {
70
+ EVP_PKEY_free(pkey_);
71
+ pkey_ = nullptr;
72
+ }
73
+
74
+ EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr);
75
+ if (pctx == nullptr) {
76
+ throw std::runtime_error("Failed to create key context for " + variant_ + ": " + getOpenSSLError());
77
+ }
78
+
79
+ if (EVP_PKEY_keygen_init(pctx) <= 0) {
80
+ EVP_PKEY_CTX_free(pctx);
81
+ throw std::runtime_error("Failed to initialize keygen: " + getOpenSSLError());
82
+ }
83
+
84
+ if (EVP_PKEY_keygen(pctx, &pkey_) <= 0) {
85
+ EVP_PKEY_CTX_free(pctx);
86
+ throw std::runtime_error("Failed to generate ML-DSA key pair: " + getOpenSSLError());
87
+ }
88
+
89
+ EVP_PKEY_CTX_free(pctx);
90
+ #endif
91
+ }
92
+
93
+ std::shared_ptr<ArrayBuffer> HybridMlDsaKeyPair::getPublicKey() {
94
+ #if !RNQC_HAS_ML_DSA
95
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
96
+ #else
97
+ checkKeyPair();
98
+
99
+ BIO* bio = BIO_new(BIO_s_mem());
100
+ if (!bio) {
101
+ throw std::runtime_error("Failed to create BIO for public key export");
102
+ }
103
+
104
+ int result;
105
+ if (publicFormat_ == 1) {
106
+ result = PEM_write_bio_PUBKEY(bio, pkey_);
107
+ } else {
108
+ result = i2d_PUBKEY_bio(bio, pkey_);
109
+ }
110
+
111
+ if (result != 1) {
112
+ BIO_free(bio);
113
+ throw std::runtime_error("Failed to export public key: " + getOpenSSLError());
114
+ }
115
+
116
+ BUF_MEM* bptr;
117
+ BIO_get_mem_ptr(bio, &bptr);
118
+
119
+ uint8_t* data = new uint8_t[bptr->length];
120
+ memcpy(data, bptr->data, bptr->length);
121
+ size_t len = bptr->length;
122
+
123
+ BIO_free(bio);
124
+
125
+ return std::make_shared<NativeArrayBuffer>(data, len, [=]() { delete[] data; });
126
+ #endif
127
+ }
128
+
129
+ std::shared_ptr<ArrayBuffer> HybridMlDsaKeyPair::getPrivateKey() {
130
+ #if !RNQC_HAS_ML_DSA
131
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
132
+ #else
133
+ checkKeyPair();
134
+
135
+ BIO* bio = BIO_new(BIO_s_mem());
136
+ if (!bio) {
137
+ throw std::runtime_error("Failed to create BIO for private key export");
138
+ }
139
+
140
+ int result;
141
+ if (privateFormat_ == 1) {
142
+ result = PEM_write_bio_PrivateKey(bio, pkey_, nullptr, nullptr, 0, nullptr, nullptr);
143
+ } else {
144
+ // Use PKCS8 format for DER export (not raw private key format)
145
+ result = i2d_PKCS8PrivateKey_bio(bio, pkey_, nullptr, nullptr, 0, nullptr, nullptr);
146
+ }
147
+
148
+ if (result != 1) {
149
+ BIO_free(bio);
150
+ throw std::runtime_error("Failed to export private key: " + getOpenSSLError());
151
+ }
152
+
153
+ BUF_MEM* bptr;
154
+ BIO_get_mem_ptr(bio, &bptr);
155
+
156
+ uint8_t* data = new uint8_t[bptr->length];
157
+ memcpy(data, bptr->data, bptr->length);
158
+ size_t len = bptr->length;
159
+
160
+ BIO_free(bio);
161
+
162
+ return std::make_shared<NativeArrayBuffer>(data, len, [=]() { delete[] data; });
163
+ #endif
164
+ }
165
+
166
+ std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> HybridMlDsaKeyPair::sign(const std::shared_ptr<ArrayBuffer>& message) {
167
+ auto nativeMessage = ToNativeArrayBuffer(message);
168
+ return Promise<std::shared_ptr<ArrayBuffer>>::async([this, nativeMessage]() { return this->signSync(nativeMessage); });
169
+ }
170
+
171
+ std::shared_ptr<ArrayBuffer> HybridMlDsaKeyPair::signSync(const std::shared_ptr<ArrayBuffer>& message) {
172
+ #if !RNQC_HAS_ML_DSA
173
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
174
+ #else
175
+ clearOpenSSLErrors();
176
+ checkKeyPair();
177
+
178
+ EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
179
+ if (md_ctx == nullptr) {
180
+ throw std::runtime_error("Failed to create signing context");
181
+ }
182
+
183
+ EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr);
184
+ if (pkey_ctx == nullptr) {
185
+ EVP_MD_CTX_free(md_ctx);
186
+ throw std::runtime_error("Failed to create signing context for " + variant_);
187
+ }
188
+
189
+ if (EVP_DigestSignInit(md_ctx, &pkey_ctx, nullptr, nullptr, pkey_) <= 0) {
190
+ EVP_MD_CTX_free(md_ctx);
191
+ EVP_PKEY_CTX_free(pkey_ctx);
192
+ throw std::runtime_error("Failed to initialize signing: " + getOpenSSLError());
193
+ }
194
+
195
+ size_t sig_len = 0;
196
+ if (EVP_DigestSign(md_ctx, nullptr, &sig_len, message->data(), message->size()) <= 0) {
197
+ EVP_MD_CTX_free(md_ctx);
198
+ throw std::runtime_error("Failed to calculate signature size: " + getOpenSSLError());
199
+ }
200
+
201
+ uint8_t* sig = new uint8_t[sig_len];
202
+
203
+ if (EVP_DigestSign(md_ctx, sig, &sig_len, message->data(), message->size()) <= 0) {
204
+ EVP_MD_CTX_free(md_ctx);
205
+ delete[] sig;
206
+ throw std::runtime_error("Failed to sign message: " + getOpenSSLError());
207
+ }
208
+
209
+ EVP_MD_CTX_free(md_ctx);
210
+
211
+ return std::make_shared<NativeArrayBuffer>(sig, sig_len, [=]() { delete[] sig; });
212
+ #endif
213
+ }
214
+
215
+ std::shared_ptr<Promise<bool>> HybridMlDsaKeyPair::verify(const std::shared_ptr<ArrayBuffer>& signature,
216
+ const std::shared_ptr<ArrayBuffer>& message) {
217
+ auto nativeSignature = ToNativeArrayBuffer(signature);
218
+ auto nativeMessage = ToNativeArrayBuffer(message);
219
+ return Promise<bool>::async([this, nativeSignature, nativeMessage]() { return this->verifySync(nativeSignature, nativeMessage); });
220
+ }
221
+
222
+ bool HybridMlDsaKeyPair::verifySync(const std::shared_ptr<ArrayBuffer>& signature, const std::shared_ptr<ArrayBuffer>& message) {
223
+ #if !RNQC_HAS_ML_DSA
224
+ throw std::runtime_error("ML-DSA requires OpenSSL 3.5+");
225
+ #else
226
+ clearOpenSSLErrors();
227
+ checkKeyPair();
228
+
229
+ EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
230
+ if (md_ctx == nullptr) {
231
+ throw std::runtime_error("Failed to create verify context");
232
+ }
233
+
234
+ EVP_PKEY_CTX* pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, variant_.c_str(), nullptr);
235
+ if (pkey_ctx == nullptr) {
236
+ EVP_MD_CTX_free(md_ctx);
237
+ throw std::runtime_error("Failed to create verify context for " + variant_);
238
+ }
239
+
240
+ if (EVP_DigestVerifyInit(md_ctx, &pkey_ctx, nullptr, nullptr, pkey_) <= 0) {
241
+ EVP_MD_CTX_free(md_ctx);
242
+ EVP_PKEY_CTX_free(pkey_ctx);
243
+ throw std::runtime_error("Failed to initialize verification: " + getOpenSSLError());
244
+ }
245
+
246
+ int result = EVP_DigestVerify(md_ctx, signature->data(), signature->size(), message->data(), message->size());
247
+
248
+ EVP_MD_CTX_free(md_ctx);
249
+
250
+ if (result < 0) {
251
+ throw std::runtime_error("Verification error: " + getOpenSSLError());
252
+ }
253
+
254
+ return result == 1;
255
+ #endif
256
+ }
257
+
258
+ void HybridMlDsaKeyPair::checkKeyPair() {
259
+ if (pkey_ == nullptr) {
260
+ throw std::runtime_error("Key pair not initialized");
261
+ }
262
+ }
263
+
264
+ } // namespace margelo::nitro::crypto
@@ -0,0 +1,47 @@
1
+ #pragma once
2
+
3
+ #include <memory>
4
+ #include <openssl/evp.h>
5
+ #include <string>
6
+
7
+ #include "HybridMlDsaKeyPairSpec.hpp"
8
+
9
+ namespace margelo::nitro::crypto {
10
+
11
+ class HybridMlDsaKeyPair : public HybridMlDsaKeyPairSpec {
12
+ public:
13
+ HybridMlDsaKeyPair() : HybridObject(TAG) {}
14
+ ~HybridMlDsaKeyPair();
15
+
16
+ std::shared_ptr<Promise<void>> generateKeyPair(double publicFormat, double publicType, double privateFormat, double privateType) override;
17
+
18
+ void generateKeyPairSync(double publicFormat, double publicType, double privateFormat, double privateType) override;
19
+
20
+ std::shared_ptr<ArrayBuffer> getPublicKey() override;
21
+ std::shared_ptr<ArrayBuffer> getPrivateKey() override;
22
+
23
+ std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> sign(const std::shared_ptr<ArrayBuffer>& message) override;
24
+
25
+ std::shared_ptr<ArrayBuffer> signSync(const std::shared_ptr<ArrayBuffer>& message) override;
26
+
27
+ std::shared_ptr<Promise<bool>> verify(const std::shared_ptr<ArrayBuffer>& signature,
28
+ const std::shared_ptr<ArrayBuffer>& message) override;
29
+
30
+ bool verifySync(const std::shared_ptr<ArrayBuffer>& signature, const std::shared_ptr<ArrayBuffer>& message) override;
31
+
32
+ void setVariant(const std::string& variant) override;
33
+
34
+ private:
35
+ std::string variant_;
36
+ EVP_PKEY* pkey_ = nullptr;
37
+
38
+ int publicFormat_ = -1;
39
+ int publicType_ = -1;
40
+ int privateFormat_ = -1;
41
+ int privateType_ = -1;
42
+
43
+ void checkKeyPair();
44
+ int getEvpPkeyType() const;
45
+ };
46
+
47
+ } // namespace margelo::nitro::crypto
@@ -9,6 +9,12 @@
9
9
  #include <openssl/evp.h>
10
10
  #include <openssl/rsa.h>
11
11
 
12
+ #if OPENSSL_VERSION_NUMBER >= 0x30500000L
13
+ #define RNQC_HAS_ML_DSA 1
14
+ #else
15
+ #define RNQC_HAS_ML_DSA 0
16
+ #endif
17
+
12
18
  namespace margelo::nitro::crypto {
13
19
 
14
20
  using margelo::nitro::NativeArrayBuffer;
@@ -22,17 +28,28 @@ HybridSignHandle::~HybridSignHandle() {
22
28
 
23
29
  void HybridSignHandle::init(const std::string& algorithm) {
24
30
  algorithm_name = algorithm;
25
- md = getDigestByName(algorithm);
26
31
 
27
- md_ctx = EVP_MD_CTX_new();
28
- if (!md_ctx) {
29
- throw std::runtime_error("Failed to create message digest context");
30
- }
32
+ // For ML-DSA and other pure signature schemes, algorithm may be empty/null
33
+ if (!algorithm.empty()) {
34
+ md = getDigestByName(algorithm);
31
35
 
32
- if (EVP_DigestInit_ex(md_ctx, md, nullptr) <= 0) {
33
- EVP_MD_CTX_free(md_ctx);
34
- md_ctx = nullptr;
35
- throw std::runtime_error("Failed to initialize message digest");
36
+ md_ctx = EVP_MD_CTX_new();
37
+ if (!md_ctx) {
38
+ throw std::runtime_error("Failed to create message digest context");
39
+ }
40
+
41
+ if (EVP_DigestInit_ex(md_ctx, md, nullptr) <= 0) {
42
+ EVP_MD_CTX_free(md_ctx);
43
+ md_ctx = nullptr;
44
+ throw std::runtime_error("Failed to initialize message digest");
45
+ }
46
+ } else {
47
+ // No digest for pure signature schemes like ML-DSA
48
+ md = nullptr;
49
+ md_ctx = EVP_MD_CTX_new();
50
+ if (!md_ctx) {
51
+ throw std::runtime_error("Failed to create message digest context");
52
+ }
36
53
  }
37
54
  }
38
55
 
@@ -43,22 +60,60 @@ void HybridSignHandle::update(const std::shared_ptr<ArrayBuffer>& data) {
43
60
 
44
61
  auto native_data = ToNativeArrayBuffer(data);
45
62
 
46
- // Accumulate raw data for potential one-shot signing (Ed25519/Ed448)
63
+ // Accumulate raw data for potential one-shot signing (Ed25519/Ed448/ML-DSA)
47
64
  const uint8_t* ptr = reinterpret_cast<const uint8_t*>(native_data->data());
48
65
  data_buffer.insert(data_buffer.end(), ptr, ptr + native_data->size());
49
66
 
50
- if (EVP_DigestUpdate(md_ctx, native_data->data(), native_data->size()) <= 0) {
51
- unsigned long err = ERR_get_error();
52
- char err_buf[256];
53
- ERR_error_string_n(err, err_buf, sizeof(err_buf));
54
- throw std::runtime_error("Failed to update digest: " + std::string(err_buf));
67
+ // Only update digest if we have one (not needed for pure signature schemes)
68
+ if (md != nullptr) {
69
+ if (EVP_DigestUpdate(md_ctx, native_data->data(), native_data->size()) <= 0) {
70
+ unsigned long err = ERR_get_error();
71
+ char err_buf[256];
72
+ ERR_error_string_n(err, err_buf, sizeof(err_buf));
73
+ throw std::runtime_error("Failed to update digest: " + std::string(err_buf));
74
+ }
55
75
  }
56
76
  }
57
77
 
58
- // Check if key type requires one-shot signing (Ed25519, Ed448)
78
+ // Check if key type requires one-shot signing (Ed25519, Ed448, ML-DSA)
59
79
  static bool isOneShotVariant(EVP_PKEY* pkey) {
60
80
  int type = EVP_PKEY_id(pkey);
81
+ #if RNQC_HAS_ML_DSA
82
+ return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448 || type == EVP_PKEY_ML_DSA_44 || type == EVP_PKEY_ML_DSA_65 ||
83
+ type == EVP_PKEY_ML_DSA_87;
84
+ #else
61
85
  return type == EVP_PKEY_ED25519 || type == EVP_PKEY_ED448;
86
+ #endif
87
+ }
88
+
89
+ // Get the algorithm name for creating PKEY_CTX (for ML-DSA variants)
90
+ static const char* getAlgorithmName(EVP_PKEY* pkey) {
91
+ int type = EVP_PKEY_id(pkey);
92
+ #if RNQC_HAS_ML_DSA
93
+ switch (type) {
94
+ case EVP_PKEY_ML_DSA_44:
95
+ return "ML-DSA-44";
96
+ case EVP_PKEY_ML_DSA_65:
97
+ return "ML-DSA-65";
98
+ case EVP_PKEY_ML_DSA_87:
99
+ return "ML-DSA-87";
100
+ case EVP_PKEY_ED25519:
101
+ return "ED25519";
102
+ case EVP_PKEY_ED448:
103
+ return "ED448";
104
+ default:
105
+ return nullptr;
106
+ }
107
+ #else
108
+ switch (type) {
109
+ case EVP_PKEY_ED25519:
110
+ return "ED25519";
111
+ case EVP_PKEY_ED448:
112
+ return "ED448";
113
+ default:
114
+ return nullptr;
115
+ }
116
+ #endif
62
117
  }
63
118
 
64
119
  std::shared_ptr<ArrayBuffer> HybridSignHandle::sign(const std::shared_ptr<HybridKeyObjectHandleSpec>& keyHandle,
@@ -78,18 +133,35 @@ std::shared_ptr<ArrayBuffer> HybridSignHandle::sign(const std::shared_ptr<Hybrid
78
133
  size_t sig_len = 0;
79
134
  std::unique_ptr<uint8_t[]> sig_buf;
80
135
 
81
- // Ed25519/Ed448 require one-shot signing with EVP_DigestSign
82
- if (isOneShotVariant(pkey)) {
136
+ int pkey_type = EVP_PKEY_id(pkey);
137
+ bool is_one_shot = isOneShotVariant(pkey);
138
+
139
+ // Ed25519/Ed448/ML-DSA require one-shot signing with EVP_DigestSign
140
+ // Also use one-shot path if no digest was specified (md == nullptr)
141
+ if (is_one_shot || md == nullptr) {
83
142
  // Create a new context for one-shot signing
84
143
  EVP_MD_CTX* sign_ctx = EVP_MD_CTX_new();
85
144
  if (!sign_ctx) {
86
145
  throw std::runtime_error("Failed to create signing context");
87
146
  }
88
147
 
89
- // Initialize for one-shot signing (pass nullptr for md - Ed25519/Ed448 have built-in hash)
90
- if (EVP_DigestSignInit(sign_ctx, nullptr, nullptr, nullptr, pkey) <= 0) {
148
+ // Get algorithm name and create PKEY_CTX for ML-DSA
149
+ const char* alg_name = getAlgorithmName(pkey);
150
+ EVP_PKEY_CTX* pkey_ctx = nullptr;
151
+ if (alg_name != nullptr) {
152
+ pkey_ctx = EVP_PKEY_CTX_new_from_name(nullptr, alg_name, nullptr);
153
+ if (!pkey_ctx) {
154
+ EVP_MD_CTX_free(sign_ctx);
155
+ throw std::runtime_error(std::string("Failed to create signing context for ") + alg_name);
156
+ }
157
+ }
158
+
159
+ // Initialize for one-shot signing (pass nullptr for md - these algorithms have built-in hash)
160
+ if (EVP_DigestSignInit(sign_ctx, pkey_ctx ? &pkey_ctx : nullptr, nullptr, nullptr, pkey) <= 0) {
91
161
  EVP_MD_CTX_free(sign_ctx);
92
- throw std::runtime_error("Failed to initialize Ed signing");
162
+ if (pkey_ctx)
163
+ EVP_PKEY_CTX_free(pkey_ctx);
164
+ throw std::runtime_error("Failed to initialize one-shot signing");
93
165
  }
94
166
 
95
167
  // Get the accumulated data from the digest context
@@ -130,7 +202,10 @@ std::shared_ptr<ArrayBuffer> HybridSignHandle::sign(const std::shared_ptr<Hybrid
130
202
 
131
203
  if (EVP_PKEY_sign_init(pkey_ctx) <= 0) {
132
204
  EVP_PKEY_CTX_free(pkey_ctx);
133
- throw std::runtime_error("Failed to initialize signing");
205
+ char err_buf[512];
206
+ snprintf(err_buf, sizeof(err_buf), "Failed to initialize signing for key type %d (expected one-shot: %s, RNQC_HAS_ML_DSA=%d)",
207
+ pkey_type, is_one_shot ? "true" : "false", RNQC_HAS_ML_DSA);
208
+ throw std::runtime_error(std::string(err_buf) + ": " + getOpenSSLError());
134
209
  }
135
210
 
136
211
  if (padding.has_value()) {