pq_crypto 0.5.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f34f4ae9f34414fbbc1f6ffc63de9c24306a6a432138e36442c1e43661fa59e
4
- data.tar.gz: 6b2a754c55b9a1be4706cf5d835ee08844b54d75d96d43611f22c8c4f68cb10f
3
+ metadata.gz: d5949078149668609462a1e9a3df7b7f767499311b998e4dd651be1d7865ddfc
4
+ data.tar.gz: 600fc91b9614aec5f7f61d8c3ab6d11643bbbf919941faf0508384e7ede9dce0
5
5
  SHA512:
6
- metadata.gz: 0b72822b4b645f891e8f87693004f736070b9bba37432a48ffc840ec1a114865ba691db7c1ffc12aab7643369c3883892d9fbcf3c4f7cf895b5938d2ee650b1c
7
- data.tar.gz: 13c0263600408685f5d484528032305c48e3e3d551799abad990167c9a9d14d0287aabb70fd88dc00f72d6236f15e70c3d9f6e9572ad02276e11674aafd87774
6
+ metadata.gz: 2ac70b6f37460e0a56b1601b59ba5660dda7e4c0c87cc03211c9497261b06f632856079622546f6c8b6db6cfff97b1a9355db879958985f48dfe296c5d729b80
7
+ data.tar.gz: 9e89256c60b8153518520438b77632eb232660ff88d0b0668cfb8aeea7a50875c2c7297773f3d469952129ccceb6e604e4f619cc4cc2ebbd458fdca9d50e837f
@@ -1,12 +1,30 @@
1
1
  name: CI
2
2
 
3
3
  on:
4
- push:
5
- branches: ["**"]
6
4
  pull_request:
5
+ push:
6
+ branches: [main]
7
7
 
8
8
  jobs:
9
+ vendor-verify:
10
+ name: vendor-verify
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: "3.4"
21
+ bundler-cache: true
22
+
23
+ - name: Verify vendored sources against pinned tree_sha256
24
+ run: bundle exec rake vendor:verify
25
+
9
26
  test:
27
+ needs: vendor-verify
10
28
  runs-on: ${{ matrix.os }}
11
29
 
12
30
  strategy:
@@ -30,6 +48,9 @@ jobs:
30
48
  with:
31
49
  go-version: "1.26.0"
32
50
 
51
+ - name: Verify vendored sources
52
+ run: bundle exec rake vendor:verify
53
+
33
54
  - name: Compile extension
34
55
  run: bundle exec rake compile
35
56
 
@@ -37,6 +58,7 @@ jobs:
37
58
  run: bundle exec rake test
38
59
 
39
60
  interop-openssl-3-5-required:
61
+ needs: vendor-verify
40
62
  name: interop-openssl-3.5-required
41
63
  runs-on: ubuntu-24.04
42
64
 
@@ -77,6 +99,9 @@ jobs:
77
99
  with:
78
100
  go-version: "1.26.0"
79
101
 
102
+ - name: Verify vendored sources
103
+ run: bundle exec rake vendor:verify
104
+
80
105
  - name: Compile extension
81
106
  run: bundle exec rake compile
82
107
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1] - 2026-05-04
4
+
5
+ ### Performance
6
+
7
+ - Enabled native asm/SIMD paths for ML-DSA/ML-KEM where available.
8
+ - Reduced Hybrid KEM X25519 overhead by reusing expanded/native key state and avoiding repeated private/public key reconstruction.
9
+ - Moved hot crypto calls under `rb_nogvl` where applicable.
10
+ - Optimized PEM export path by replacing BIO/streaming base64 with direct encode.
11
+ - Reduced small-buffer streaming overhead by avoiding unnecessary `malloc`/`nogvl` work for tiny chunks.
12
+
13
+ ### Vendoring
14
+
15
+ - Committed `mlkem-native` and `mldsa-native` sources to the repository. Builds no longer require git or network.
16
+ - Pinned upstream by `commit` + `tree_sha256` in `script/vendor_libs.rb`; manifest signed with `manifest_sha256`.
17
+ - Deterministic vendor tree (no symlinks, dotfiles, normalized mtime/permissions).
18
+ - `script/vendor_libs.rb` now supports `--verify`, `--sync`, `--bump`.
19
+
20
+ ### Build
21
+
22
+ - `extconf.rb` no longer auto-fetches upstream. Missing vendor aborts the build; opt in with `PQCRYPTO_AUTO_VENDOR=1`.
23
+
24
+ ### Rakefile
25
+
26
+ - Added `vendor:verify`, `vendor:sync`, `vendor:bump`. Default task runs `vendor:verify` before `compile` and `test`.
27
+
28
+ ### CI
29
+
30
+ - Added `vendor-verify` gating job; `vendor:verify` also runs in each test job before compile.
31
+
32
+ ### Repository
33
+
34
+ - `.gitattributes` enforces `eol=lf` and marks vendor as binary (CRLF protection).
35
+ - `.gitignore` no longer hides `sempls/`.
36
+
3
37
  ## [0.5.0] - 2026-05-04
4
38
 
5
39
  ### Changed — native backend migration
@@ -39,7 +39,45 @@ if SANITIZE && !SANITIZE.strip.empty?
39
39
  $LDFLAGS << " -fsanitize=#{sanitize}"
40
40
  end
41
41
 
42
- NATIVE_ASM = (ENV["PQCRYPTO_NATIVE_ASM"] || "0") == "1"
42
+ def native_asm_supported_by_default?
43
+ host_cpu = RbConfig::CONFIG.fetch("host_cpu", "")
44
+ host_os = RbConfig::CONFIG.fetch("host_os", "")
45
+ return false if host_os =~ /mswin|mingw|cygwin/i
46
+
47
+ host_cpu =~ /\A(?:arm64|aarch64)\z/i
48
+ end
49
+
50
+ def parse_native_asm_env(value)
51
+ return native_asm_supported_by_default? if value.nil? || value.strip.empty? || value == "auto"
52
+
53
+ case value.strip.downcase
54
+ when "1", "true", "yes", "on", "auto"
55
+ true
56
+ when "0", "false", "no", "off"
57
+ false
58
+ else
59
+ abort "Invalid PQCRYPTO_NATIVE_ASM=#{value.inspect}; use 1, 0, or auto"
60
+ end
61
+ end
62
+
63
+ NATIVE_ASM = parse_native_asm_env(ENV["PQCRYPTO_NATIVE_ASM"])
64
+
65
+ def parse_native_backend_env(name)
66
+ value = ENV[name]
67
+ return NATIVE_ASM if value.nil? || value.strip.empty? || value == "auto"
68
+
69
+ case value.strip.downcase
70
+ when "1", "true", "yes", "on"
71
+ true
72
+ when "0", "false", "no", "off"
73
+ false
74
+ else
75
+ abort "Invalid #{name}=#{value.inspect}; use 1, 0, or auto"
76
+ end
77
+ end
78
+
79
+ NATIVE_ARITH = parse_native_backend_env("PQCRYPTO_NATIVE_ARITH")
80
+ NATIVE_FIPS202 = parse_native_backend_env("PQCRYPTO_NATIVE_FIPS202")
43
81
 
44
82
  def configure_compiler_environment
45
83
  return unless RUBY_PLATFORM.include?("darwin")
@@ -66,28 +104,25 @@ def vendor_script_path
66
104
  end
67
105
 
68
106
  def run_vendor_script!(vendor_dir)
69
- script = vendor_script_path
70
- abort <<~MSG unless File.exist?(script)
71
- PQ Code Package vendored sources are missing and script/vendor_libs.rb was not packaged.
107
+ abort <<~MSG if ENV["PQCRYPTO_AUTO_VENDOR"] != "1"
108
+ PQ Code Package vendored sources are missing.
72
109
 
73
110
  Expected:
74
111
  #{native_vendor_sources_for(vendor_dir).join("\n ")}
75
112
 
76
- Rebuild the gem from a repository that includes script/vendor_libs.rb, or run
77
- script/vendor_libs.rb before building the gem package.
78
- MSG
79
-
80
- abort <<~MSG if ENV["PQCRYPTO_AUTO_VENDOR"] == "0"
81
- PQ Code Package vendored sources are missing and PQCRYPTO_AUTO_VENDOR=0 was set.
82
-
83
- Expected:
84
- #{native_vendor_sources_for(vendor_dir).join("\n ")}
113
+ The vendor tree is committed to the repository and shipped with the gem.
114
+ If it is missing, the source tree is incomplete or corrupted.
85
115
 
86
- Run:
116
+ To fetch upstream sources at the pinned commits run:
87
117
  ruby script/vendor_libs.rb
118
+
119
+ Or to allow extconf.rb to do this for you set PQCRYPTO_AUTO_VENDOR=1.
88
120
  MSG
89
121
 
90
- puts "PQ Code Package native sources are missing; vendoring now..."
122
+ script = vendor_script_path
123
+ abort "PQ Code Package vendored sources are missing and script/vendor_libs.rb was not packaged." unless File.exist?(script)
124
+
125
+ puts "PQ Code Package native sources are missing; vendoring now (PQCRYPTO_AUTO_VENDOR=1)..."
91
126
  ok = system(RbConfig.ruby, script)
92
127
  abort <<~MSG unless ok
93
128
  Failed to vendor PQ Code Package native sources.
@@ -210,10 +245,8 @@ def native_flags(kind, level, shared:)
210
245
  flags << "-D#{prefix}_CONFIG_NAMESPACE_PREFIX=#{ns}"
211
246
  flags << "-D#{prefix}_CONFIG_NO_SUPERCOP"
212
247
  flags << (shared ? "-D#{prefix}_CONFIG_MULTILEVEL_WITH_SHARED" : "-D#{prefix}_CONFIG_MULTILEVEL_NO_SHARED")
213
- if NATIVE_ASM
214
- flags << "-D#{prefix}_CONFIG_USE_NATIVE_BACKEND_ARITH"
215
- flags << "-D#{prefix}_CONFIG_USE_NATIVE_BACKEND_FIPS202"
216
- end
248
+ flags << "-D#{prefix}_CONFIG_USE_NATIVE_BACKEND_ARITH" if NATIVE_ARITH
249
+ flags << "-D#{prefix}_CONFIG_USE_NATIVE_BACKEND_FIPS202" if NATIVE_FIPS202
217
250
  flags.join(" ")
218
251
  end
219
252
 
@@ -241,7 +274,7 @@ def inject_native_sources!(config)
241
274
  RULE
242
275
  end
243
276
 
244
- if NATIVE_ASM
277
+ if NATIVE_ARITH || NATIVE_FIPS202
245
278
  [
246
279
  [:mlkem, "512", config[:mlkem_asm], true],
247
280
  [:mlkem, "768", config[:mlkem_asm], false],
@@ -288,7 +321,9 @@ native_config = native_vendor_config(vendor_dir)
288
321
  puts "OpenSSL: system"
289
322
  puts "ML-KEM: mlkem-native vendored"
290
323
  puts "ML-DSA: mldsa-native vendored"
291
- puts "Native asm backends: #{NATIVE_ASM ? 'enabled' : 'disabled'}"
324
+ puts "Native asm auto/forced: #{NATIVE_ASM ? 'enabled' : 'disabled'}"
325
+ puts "Native arithmetic backend: #{NATIVE_ARITH ? 'enabled' : 'disabled'}"
326
+ puts "Native FIPS202 backend: #{NATIVE_FIPS202 ? 'enabled' : 'disabled'}"
292
327
  puts "PQClean fallback: removed"
293
328
  puts "Output: pqcrypto/pqcrypto_secure"
294
329
  puts "===================================="
@@ -78,6 +78,9 @@ int pqcr_mlkem1024_enc(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
78
78
  int pqcr_mlkem1024_enc_derand(uint8_t *ct, uint8_t *ss, const uint8_t *pk, const uint8_t *coins);
79
79
  int pqcr_mlkem1024_dec(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
80
80
 
81
+ void pqcr_mlkem_shake256(uint8_t *output, size_t outlen, const uint8_t *input, size_t inlen);
82
+ void pqcr_mlkem_sha3_256(uint8_t *output, const uint8_t *input, size_t inlen);
83
+
81
84
  /* mldsa-native symbols: namespace prefix pqcr_mldsa + level suffix. */
82
85
  int pqcr_mldsa44_keypair(uint8_t *pk, uint8_t *sk);
83
86
  int pqcr_mldsa44_keypair_internal(uint8_t *pk, uint8_t *sk, const uint8_t seed[MLDSA_SEEDBYTES]);
@@ -5,6 +5,7 @@
5
5
  #include <string.h>
6
6
 
7
7
  #include <openssl/crypto.h>
8
+ #include <openssl/evp.h>
8
9
 
9
10
  #include "pqcrypto_secure.h"
10
11
 
@@ -12,6 +13,8 @@
12
13
  #define RB_NOGVL_OFFLOAD_SAFE 0
13
14
  #endif
14
15
 
16
+ #define PQ_MU_ABSORB_NOGVL_MIN_BYTES 16384
17
+
15
18
  typedef struct {
16
19
  int result;
17
20
  uint8_t *public_key;
@@ -36,6 +39,25 @@ typedef struct {
36
39
  const uint8_t *secret_key;
37
40
  } kem_decapsulate_call_t;
38
41
 
42
+ typedef struct {
43
+ int result;
44
+ uint8_t *expanded_secret_key;
45
+ const uint8_t *secret_key;
46
+ } hybrid_expand_call_t;
47
+
48
+ typedef struct {
49
+ int result;
50
+ uint8_t *shared_secret;
51
+ const uint8_t *ciphertext;
52
+ const uint8_t *expanded_secret_key;
53
+ void *x25519_private_pkey;
54
+ } hybrid_decapsulate_expanded_pkey_call_t;
55
+
56
+ typedef struct {
57
+ uint8_t expanded_secret_key[PQ_HYBRID_EXPANDED_SECRETKEYBYTES];
58
+ EVP_PKEY *x25519_private_pkey;
59
+ } hybrid_expanded_key_wrapper_t;
60
+
39
61
  typedef struct {
40
62
  int result;
41
63
  uint8_t *public_key;
@@ -68,6 +90,49 @@ static VALUE mPQCrypto;
68
90
  static VALUE ePQCryptoError;
69
91
  static VALUE ePQCryptoVerificationError;
70
92
 
93
+ static void hybrid_expanded_key_wrapper_free(void *ptr) {
94
+ hybrid_expanded_key_wrapper_t *wrapper = (hybrid_expanded_key_wrapper_t *)ptr;
95
+ if (!wrapper) {
96
+ return;
97
+ }
98
+ pq_secure_wipe(wrapper->expanded_secret_key, sizeof(wrapper->expanded_secret_key));
99
+ if (wrapper->x25519_private_pkey) {
100
+ EVP_PKEY_free(wrapper->x25519_private_pkey);
101
+ wrapper->x25519_private_pkey = NULL;
102
+ }
103
+ xfree(wrapper);
104
+ }
105
+
106
+ static size_t hybrid_expanded_key_wrapper_size(const void *ptr) {
107
+ (void)ptr;
108
+ return sizeof(hybrid_expanded_key_wrapper_t);
109
+ }
110
+
111
+ static const rb_data_type_t hybrid_expanded_key_data_type = {
112
+ .wrap_struct_name = "PQCrypto::HybridKEM::ExpandedSecretKey",
113
+ .function =
114
+ {
115
+ .dmark = NULL,
116
+ .dfree = hybrid_expanded_key_wrapper_free,
117
+ .dsize = hybrid_expanded_key_wrapper_size,
118
+ .dcompact = NULL,
119
+ .reserved = {NULL},
120
+ },
121
+ .parent = NULL,
122
+ .data = NULL,
123
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
124
+ };
125
+
126
+ static hybrid_expanded_key_wrapper_t *hybrid_expanded_key_unwrap(VALUE obj) {
127
+ hybrid_expanded_key_wrapper_t *wrapper;
128
+ TypedData_Get_Struct(obj, hybrid_expanded_key_wrapper_t, &hybrid_expanded_key_data_type,
129
+ wrapper);
130
+ if (!wrapper || !wrapper->x25519_private_pkey) {
131
+ rb_raise(ePQCryptoError, "hybrid expanded secret key used after release");
132
+ }
133
+ return wrapper;
134
+ }
135
+
71
136
  __attribute__((noreturn)) static void pq_raise_general_error(int err);
72
137
 
73
138
  static const char *const PQC_CONTAINER_ALGORITHMS[] = {
@@ -76,18 +141,35 @@ static const char *const PQC_CONTAINER_ALGORITHMS[] = {
76
141
  "ml_dsa_65",
77
142
  };
78
143
 
144
+ static ID pqc_container_algorithm_ids[sizeof(PQC_CONTAINER_ALGORITHMS) /
145
+ sizeof(PQC_CONTAINER_ALGORITHMS[0])];
146
+
147
+ static void pq_init_algorithm_ids(void) {
148
+ for (size_t i = 0; i < sizeof(PQC_CONTAINER_ALGORITHMS) / sizeof(PQC_CONTAINER_ALGORITHMS[0]);
149
+ ++i) {
150
+ pqc_container_algorithm_ids[i] = rb_intern(PQC_CONTAINER_ALGORITHMS[i]);
151
+ }
152
+ }
153
+
79
154
  static const char *pq_algorithm_symbol_to_cstr(VALUE algorithm) {
80
- ID id;
81
155
  if (SYMBOL_P(algorithm)) {
82
- id = SYM2ID(algorithm);
156
+ ID id = SYM2ID(algorithm);
157
+ for (size_t i = 0; i < sizeof(PQC_CONTAINER_ALGORITHMS) / sizeof(PQC_CONTAINER_ALGORITHMS[0]);
158
+ ++i) {
159
+ if (id == pqc_container_algorithm_ids[i]) {
160
+ return PQC_CONTAINER_ALGORITHMS[i];
161
+ }
162
+ }
83
163
  } else {
84
164
  VALUE str = StringValue(algorithm);
85
- id = rb_intern_str(str);
86
- }
87
- for (size_t i = 0; i < sizeof(PQC_CONTAINER_ALGORITHMS) / sizeof(PQC_CONTAINER_ALGORITHMS[0]);
88
- ++i) {
89
- if (id == rb_intern(PQC_CONTAINER_ALGORITHMS[i])) {
90
- return PQC_CONTAINER_ALGORITHMS[i];
165
+ const char *ptr = RSTRING_PTR(str);
166
+ size_t len = (size_t)RSTRING_LEN(str);
167
+ for (size_t i = 0; i < sizeof(PQC_CONTAINER_ALGORITHMS) / sizeof(PQC_CONTAINER_ALGORITHMS[0]);
168
+ ++i) {
169
+ size_t algorithm_len = strlen(PQC_CONTAINER_ALGORITHMS[i]);
170
+ if (len == algorithm_len && memcmp(ptr, PQC_CONTAINER_ALGORITHMS[i], len) == 0) {
171
+ return PQC_CONTAINER_ALGORITHMS[i];
172
+ }
91
173
  }
92
174
  }
93
175
  rb_raise(rb_eArgError, "Unsupported serialization algorithm");
@@ -97,7 +179,7 @@ static VALUE pq_algorithm_cstr_to_symbol(const char *algorithm) {
97
179
  for (size_t i = 0; i < sizeof(PQC_CONTAINER_ALGORITHMS) / sizeof(PQC_CONTAINER_ALGORITHMS[0]);
98
180
  ++i) {
99
181
  if (strcmp(algorithm, PQC_CONTAINER_ALGORITHMS[i]) == 0) {
100
- return ID2SYM(rb_intern(PQC_CONTAINER_ALGORITHMS[i]));
182
+ return ID2SYM(pqc_container_algorithm_ids[i]);
101
183
  }
102
184
  }
103
185
  rb_raise(rb_eArgError, "Unsupported serialization algorithm");
@@ -175,6 +257,12 @@ static void *pq_hybrid_kem_encapsulate_nogvl(void *arg) {
175
257
  return NULL;
176
258
  }
177
259
 
260
+ static void *pq_hybrid_kem_expand_secret_key_nogvl(void *arg) {
261
+ hybrid_expand_call_t *call = (hybrid_expand_call_t *)arg;
262
+ call->result = pq_hybrid_kem_expand_secret_key(call->expanded_secret_key, call->secret_key);
263
+ return NULL;
264
+ }
265
+
178
266
  static void *pq_hybrid_kem_decapsulate_nogvl(void *arg) {
179
267
  kem_decapsulate_call_t *call = (kem_decapsulate_call_t *)arg;
180
268
  call->result =
@@ -182,6 +270,21 @@ static void *pq_hybrid_kem_decapsulate_nogvl(void *arg) {
182
270
  return NULL;
183
271
  }
184
272
 
273
+ static void *pq_hybrid_kem_decapsulate_expanded_nogvl(void *arg) {
274
+ kem_decapsulate_call_t *call = (kem_decapsulate_call_t *)arg;
275
+ call->result = pq_hybrid_kem_decapsulate_expanded(call->shared_secret, call->ciphertext,
276
+ call->secret_key);
277
+ return NULL;
278
+ }
279
+
280
+ static void *pq_hybrid_kem_decapsulate_expanded_pkey_nogvl(void *arg) {
281
+ hybrid_decapsulate_expanded_pkey_call_t *call =
282
+ (hybrid_decapsulate_expanded_pkey_call_t *)arg;
283
+ call->result = pq_hybrid_kem_decapsulate_expanded_pkey(
284
+ call->shared_secret, call->ciphertext, call->expanded_secret_key, call->x25519_private_pkey);
285
+ return NULL;
286
+ }
287
+
185
288
  #define PQ_DEFINE_SIGN_KEYPAIR_NOGVL(rb_name, c_call) \
186
289
  static void *pq_##rb_name##_nogvl(void *arg) { \
187
290
  sign_keypair_call_t *call = (sign_keypair_call_t *)arg; \
@@ -640,6 +743,66 @@ static VALUE pqcrypto_hybrid_kem_encapsulate(VALUE self, VALUE public_key) {
640
743
  PQ_HYBRID_SHAREDSECRETBYTES);
641
744
  }
642
745
 
746
+ static VALUE pqcrypto_hybrid_kem_expand_secret_key(VALUE self, VALUE secret_key) {
747
+ (void)self;
748
+ hybrid_expand_call_t call = {0};
749
+ VALUE result;
750
+ size_t copied_secret_key_len = 0;
751
+
752
+ pq_validate_bytes_argument(secret_key, PQ_HYBRID_SECRETKEYBYTES, "hybrid secret key");
753
+
754
+ call.secret_key = pq_copy_ruby_string(secret_key, &copied_secret_key_len);
755
+ call.expanded_secret_key = pq_alloc_buffer(PQ_HYBRID_EXPANDED_SECRETKEYBYTES);
756
+
757
+ rb_thread_call_without_gvl(pq_hybrid_kem_expand_secret_key_nogvl, &call, NULL, NULL);
758
+ pq_wipe_and_free((uint8_t *)call.secret_key, copied_secret_key_len);
759
+
760
+ if (call.result != PQ_SUCCESS) {
761
+ pq_wipe_and_free(call.expanded_secret_key, PQ_HYBRID_EXPANDED_SECRETKEYBYTES);
762
+ pq_raise_general_error(call.result);
763
+ }
764
+
765
+ result = pq_string_from_buffer(call.expanded_secret_key, PQ_HYBRID_EXPANDED_SECRETKEYBYTES);
766
+ pq_wipe_and_free(call.expanded_secret_key, PQ_HYBRID_EXPANDED_SECRETKEYBYTES);
767
+ return result;
768
+ }
769
+
770
+ static VALUE pqcrypto_hybrid_kem_expand_secret_key_object(VALUE self, VALUE secret_key) {
771
+ (void)self;
772
+ hybrid_expand_call_t call = {0};
773
+ size_t copied_secret_key_len = 0;
774
+
775
+ pq_validate_bytes_argument(secret_key, PQ_HYBRID_SECRETKEYBYTES, "hybrid secret key");
776
+
777
+ hybrid_expanded_key_wrapper_t *wrapper;
778
+ VALUE obj = TypedData_Make_Struct(rb_cObject, hybrid_expanded_key_wrapper_t,
779
+ &hybrid_expanded_key_data_type, wrapper);
780
+ memset(wrapper->expanded_secret_key, 0, sizeof(wrapper->expanded_secret_key));
781
+ wrapper->x25519_private_pkey = NULL;
782
+
783
+ call.secret_key = pq_copy_ruby_string(secret_key, &copied_secret_key_len);
784
+ call.expanded_secret_key = wrapper->expanded_secret_key;
785
+
786
+ rb_thread_call_without_gvl(pq_hybrid_kem_expand_secret_key_nogvl, &call, NULL, NULL);
787
+ pq_wipe_and_free((uint8_t *)call.secret_key, copied_secret_key_len);
788
+
789
+ if (call.result != PQ_SUCCESS) {
790
+ pq_secure_wipe(wrapper->expanded_secret_key, sizeof(wrapper->expanded_secret_key));
791
+ pq_raise_general_error(call.result);
792
+ }
793
+
794
+ const hybrid_expanded_secret_key_t *expanded =
795
+ (const hybrid_expanded_secret_key_t *)wrapper->expanded_secret_key;
796
+ wrapper->x25519_private_pkey = EVP_PKEY_new_raw_private_key(
797
+ EVP_PKEY_X25519, NULL, expanded->x25519_sk, X25519_SECRETKEYBYTES);
798
+ if (!wrapper->x25519_private_pkey) {
799
+ pq_secure_wipe(wrapper->expanded_secret_key, sizeof(wrapper->expanded_secret_key));
800
+ pq_raise_general_error(PQ_ERROR_OPENSSL);
801
+ }
802
+
803
+ return obj;
804
+ }
805
+
643
806
  static VALUE pqcrypto_hybrid_kem_decapsulate(VALUE self, VALUE ciphertext, VALUE secret_key) {
644
807
  (void)self;
645
808
  return pq_run_kem_decapsulate(pq_hybrid_kem_decapsulate_nogvl, ciphertext,
@@ -647,6 +810,50 @@ static VALUE pqcrypto_hybrid_kem_decapsulate(VALUE self, VALUE ciphertext, VALUE
647
810
  PQ_HYBRID_SHAREDSECRETBYTES);
648
811
  }
649
812
 
813
+ static VALUE pqcrypto_hybrid_kem_decapsulate_expanded(VALUE self, VALUE ciphertext,
814
+ VALUE expanded_secret_key) {
815
+ (void)self;
816
+ return pq_run_kem_decapsulate(pq_hybrid_kem_decapsulate_expanded_nogvl, ciphertext,
817
+ PQ_HYBRID_CIPHERTEXTBYTES, expanded_secret_key,
818
+ PQ_HYBRID_EXPANDED_SECRETKEYBYTES,
819
+ PQ_HYBRID_SHAREDSECRETBYTES);
820
+ }
821
+
822
+ static VALUE pqcrypto_hybrid_kem_decapsulate_expanded_object(VALUE self, VALUE ciphertext,
823
+ VALUE expanded_secret_key_obj) {
824
+ (void)self;
825
+ hybrid_expanded_key_wrapper_t *wrapper = hybrid_expanded_key_unwrap(expanded_secret_key_obj);
826
+ hybrid_decapsulate_expanded_pkey_call_t call = {0};
827
+ VALUE result;
828
+ size_t copied_ciphertext_len = 0;
829
+
830
+ pq_validate_bytes_argument(ciphertext, PQ_HYBRID_CIPHERTEXTBYTES, "ciphertext");
831
+
832
+ call.ciphertext = pq_copy_ruby_string(ciphertext, &copied_ciphertext_len);
833
+ call.expanded_secret_key = wrapper->expanded_secret_key;
834
+ call.shared_secret = pq_alloc_buffer(PQ_HYBRID_SHAREDSECRETBYTES);
835
+
836
+ if (EVP_PKEY_up_ref(wrapper->x25519_private_pkey) != 1) {
837
+ pq_wipe_and_free((uint8_t *)call.ciphertext, copied_ciphertext_len);
838
+ pq_wipe_and_free(call.shared_secret, PQ_HYBRID_SHAREDSECRETBYTES);
839
+ pq_raise_general_error(PQ_ERROR_OPENSSL);
840
+ }
841
+ call.x25519_private_pkey = wrapper->x25519_private_pkey;
842
+
843
+ rb_thread_call_without_gvl(pq_hybrid_kem_decapsulate_expanded_pkey_nogvl, &call, NULL, NULL);
844
+ EVP_PKEY_free((EVP_PKEY *)call.x25519_private_pkey);
845
+ pq_wipe_and_free((uint8_t *)call.ciphertext, copied_ciphertext_len);
846
+
847
+ if (call.result != PQ_SUCCESS) {
848
+ pq_wipe_and_free(call.shared_secret, PQ_HYBRID_SHAREDSECRETBYTES);
849
+ pq_raise_general_error(call.result);
850
+ }
851
+
852
+ result = pq_string_from_buffer(call.shared_secret, PQ_HYBRID_SHAREDSECRETBYTES);
853
+ pq_wipe_and_free(call.shared_secret, PQ_HYBRID_SHAREDSECRETBYTES);
854
+ return result;
855
+ }
856
+
650
857
  static VALUE pqcrypto__test_ml_kem_keypair_from_seed(VALUE self, VALUE seed) {
651
858
  (void)self;
652
859
  StringValue(seed);
@@ -1220,6 +1427,15 @@ static VALUE pqcrypto__native_mldsa_mu_builder_update(VALUE self, VALUE builder_
1220
1427
  return Qnil;
1221
1428
  }
1222
1429
 
1430
+ if (chunk_len < PQ_MU_ABSORB_NOGVL_MIN_BYTES) {
1431
+ int rc = pq_mu_builder_absorb(wrapper->builder, (const uint8_t *)RSTRING_PTR(chunk),
1432
+ chunk_len);
1433
+ if (rc != PQ_SUCCESS) {
1434
+ pq_raise_general_error(rc);
1435
+ }
1436
+ return Qnil;
1437
+ }
1438
+
1223
1439
  uint8_t *copy = pq_alloc_buffer(chunk_len);
1224
1440
  memcpy(copy, RSTRING_PTR(chunk), chunk_len);
1225
1441
 
@@ -1432,6 +1648,7 @@ static VALUE pqcrypto_secret_key_from_pqc_container_pem(VALUE self, VALUE pem) {
1432
1648
 
1433
1649
  void Init_pqcrypto_secure(void) {
1434
1650
  mPQCrypto = rb_define_module("PQCrypto");
1651
+ pq_init_algorithm_ids();
1435
1652
  ePQCryptoError = rb_define_class_under(mPQCrypto, "Error", rb_eStandardError);
1436
1653
 
1437
1654
  ePQCryptoVerificationError =
@@ -1478,8 +1695,16 @@ void Init_pqcrypto_secure(void) {
1478
1695
  rb_define_module_function(mPQCrypto, "hybrid_kem_keypair", pqcrypto_hybrid_kem_keypair, 0);
1479
1696
  rb_define_module_function(mPQCrypto, "hybrid_kem_encapsulate", pqcrypto_hybrid_kem_encapsulate,
1480
1697
  1);
1698
+ rb_define_module_function(mPQCrypto, "hybrid_kem_expand_secret_key",
1699
+ pqcrypto_hybrid_kem_expand_secret_key, 1);
1700
+ rb_define_module_function(mPQCrypto, "hybrid_kem_expand_secret_key_object",
1701
+ pqcrypto_hybrid_kem_expand_secret_key_object, 1);
1481
1702
  rb_define_module_function(mPQCrypto, "hybrid_kem_decapsulate", pqcrypto_hybrid_kem_decapsulate,
1482
1703
  2);
1704
+ rb_define_module_function(mPQCrypto, "hybrid_kem_decapsulate_expanded",
1705
+ pqcrypto_hybrid_kem_decapsulate_expanded, 2);
1706
+ rb_define_module_function(mPQCrypto, "hybrid_kem_decapsulate_expanded_object",
1707
+ pqcrypto_hybrid_kem_decapsulate_expanded_object, 2);
1483
1708
  rb_define_module_function(mPQCrypto, "sign_keypair", pqcrypto_sign_keypair, 0);
1484
1709
  rb_define_module_function(mPQCrypto, "sign", pqcrypto_sign, 2);
1485
1710
  rb_define_module_function(mPQCrypto, "verify", pqcrypto_verify, 3);