kpqc 0.1.0

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 (191) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +174 -0
  5. data/Rakefile +57 -0
  6. data/THIRD_PARTY_NOTICES.md +53 -0
  7. data/ext/kpqc/aimer128f/extconf.rb +4 -0
  8. data/ext/kpqc/aimer128s/extconf.rb +4 -0
  9. data/ext/kpqc/aimer192f/extconf.rb +4 -0
  10. data/ext/kpqc/aimer192s/extconf.rb +4 -0
  11. data/ext/kpqc/aimer256f/extconf.rb +4 -0
  12. data/ext/kpqc/aimer256s/extconf.rb +4 -0
  13. data/ext/kpqc/extconf_helper.rb +93 -0
  14. data/ext/kpqc/haetae2/extconf.rb +4 -0
  15. data/ext/kpqc/haetae3/extconf.rb +4 -0
  16. data/ext/kpqc/haetae5/extconf.rb +4 -0
  17. data/ext/kpqc/module.c +195 -0
  18. data/ext/kpqc/ntruplus1152/extconf.rb +4 -0
  19. data/ext/kpqc/ntruplus768/extconf.rb +4 -0
  20. data/ext/kpqc/ntruplus864/extconf.rb +4 -0
  21. data/ext/kpqc/randombytes.c +107 -0
  22. data/ext/kpqc/smaugt128/extconf.rb +4 -0
  23. data/ext/kpqc/smaugt192/extconf.rb +4 -0
  24. data/ext/kpqc/smaugt256/extconf.rb +4 -0
  25. data/ext/kpqc/timer/extconf.rb +4 -0
  26. data/kpqc.gemspec +41 -0
  27. data/lib/kpqc/aimer.rb +38 -0
  28. data/lib/kpqc/api.rb +129 -0
  29. data/lib/kpqc/haetae.rb +25 -0
  30. data/lib/kpqc/ntruplus.rb +25 -0
  31. data/lib/kpqc/smaugt.rb +29 -0
  32. data/lib/kpqc/types.rb +103 -0
  33. data/lib/kpqc/version.rb +6 -0
  34. data/lib/kpqc.rb +33 -0
  35. data/test/algorithm_test.rb +111 -0
  36. data/test/api_test.rb +52 -0
  37. data/test/interop.mjs +34 -0
  38. data/test/interop_test.rb +82 -0
  39. data/test/kat/aimer128f.rsp +10 -0
  40. data/test/kat/aimer128s.rsp +10 -0
  41. data/test/kat/aimer192f.rsp +10 -0
  42. data/test/kat/aimer192s.rsp +10 -0
  43. data/test/kat/aimer256f.rsp +10 -0
  44. data/test/kat/aimer256s.rsp +10 -0
  45. data/test/kat/haetae2.rsp +10 -0
  46. data/test/kat/haetae3.rsp +10 -0
  47. data/test/kat/haetae5.rsp +10 -0
  48. data/test/kat/ntruplus1152.rsp +8 -0
  49. data/test/kat/ntruplus768.rsp +8 -0
  50. data/test/kat/ntruplus864.rsp +8 -0
  51. data/test/kat/smaugt128.rsp +8 -0
  52. data/test/kat/smaugt192.rsp +8 -0
  53. data/test/kat/smaugt256.rsp +8 -0
  54. data/test/kat/timer.rsp +8 -0
  55. data/test/kat_drbg.rb +155 -0
  56. data/test/kat_test.rb +99 -0
  57. data/test/test_helper.rb +21 -0
  58. data/vendor/AIMer/LICENSE +21 -0
  59. data/vendor/AIMer/aim2.c +163 -0
  60. data/vendor/AIMer/aim2.h +36 -0
  61. data/vendor/AIMer/aim2_constant.h +74 -0
  62. data/vendor/AIMer/api.h +42 -0
  63. data/vendor/AIMer/common/crypto_declassify.h +7 -0
  64. data/vendor/AIMer/common/fips202.c +878 -0
  65. data/vendor/AIMer/common/fips202.h +166 -0
  66. data/vendor/AIMer/common/rng.h +56 -0
  67. data/vendor/AIMer/field.h +43 -0
  68. data/vendor/AIMer/field128.c +454 -0
  69. data/vendor/AIMer/field192.c +485 -0
  70. data/vendor/AIMer/field256.c +564 -0
  71. data/vendor/AIMer/hash.c +50 -0
  72. data/vendor/AIMer/hash.h +37 -0
  73. data/vendor/AIMer/params/params-aimer-128f.h +34 -0
  74. data/vendor/AIMer/params/params-aimer-128s.h +34 -0
  75. data/vendor/AIMer/params/params-aimer-192f.h +34 -0
  76. data/vendor/AIMer/params/params-aimer-192s.h +34 -0
  77. data/vendor/AIMer/params/params-aimer-256f.h +34 -0
  78. data/vendor/AIMer/params/params-aimer-256s.h +34 -0
  79. data/vendor/AIMer/params.h +5 -0
  80. data/vendor/AIMer/sign.c +631 -0
  81. data/vendor/AIMer/sign.h +40 -0
  82. data/vendor/AIMer/tree.c +75 -0
  83. data/vendor/AIMer/tree.h +26 -0
  84. data/vendor/HAETAE/LICENSE +22 -0
  85. data/vendor/HAETAE/include/api.h +54 -0
  86. data/vendor/HAETAE/include/common.h +12 -0
  87. data/vendor/HAETAE/include/config.h +42 -0
  88. data/vendor/HAETAE/include/decompose.h +19 -0
  89. data/vendor/HAETAE/include/encoding.h +20 -0
  90. data/vendor/HAETAE/include/fft.h +26 -0
  91. data/vendor/HAETAE/include/fips202.h +40 -0
  92. data/vendor/HAETAE/include/fixpoint.h +125 -0
  93. data/vendor/HAETAE/include/ntt.h +16 -0
  94. data/vendor/HAETAE/include/packing.h +58 -0
  95. data/vendor/HAETAE/include/params.h +117 -0
  96. data/vendor/HAETAE/include/poly.h +65 -0
  97. data/vendor/HAETAE/include/polyfix.h +65 -0
  98. data/vendor/HAETAE/include/polymat.h +32 -0
  99. data/vendor/HAETAE/include/polyvec.h +131 -0
  100. data/vendor/HAETAE/include/randombytes.h +39 -0
  101. data/vendor/HAETAE/include/rans_byte.h +335 -0
  102. data/vendor/HAETAE/include/reduce.h +31 -0
  103. data/vendor/HAETAE/include/sampler.h +23 -0
  104. data/vendor/HAETAE/include/symmetric.h +52 -0
  105. data/vendor/HAETAE/src/decompose.c +73 -0
  106. data/vendor/HAETAE/src/encoding.c +700 -0
  107. data/vendor/HAETAE/src/fft.c +235 -0
  108. data/vendor/HAETAE/src/fips202.c +745 -0
  109. data/vendor/HAETAE/src/fixpoint.c +143 -0
  110. data/vendor/HAETAE/src/ntt.c +98 -0
  111. data/vendor/HAETAE/src/packing.c +595 -0
  112. data/vendor/HAETAE/src/poly.c +371 -0
  113. data/vendor/HAETAE/src/polyfix.c +298 -0
  114. data/vendor/HAETAE/src/polymat.c +82 -0
  115. data/vendor/HAETAE/src/polyvec.c +534 -0
  116. data/vendor/HAETAE/src/reduce.c +96 -0
  117. data/vendor/HAETAE/src/sampler.c +310 -0
  118. data/vendor/HAETAE/src/sign.c +558 -0
  119. data/vendor/HAETAE/src/symmetric-shake.c +50 -0
  120. data/vendor/NTRUplus/LICENSE +21 -0
  121. data/vendor/NTRUplus/NTRU+1152/api.h +24 -0
  122. data/vendor/NTRUplus/NTRU+1152/fips202/fips202.c +878 -0
  123. data/vendor/NTRUplus/NTRU+1152/fips202/fips202.h +166 -0
  124. data/vendor/NTRUplus/NTRU+1152/kem.c +286 -0
  125. data/vendor/NTRUplus/NTRU+1152/ntt.c +394 -0
  126. data/vendor/NTRUplus/NTRU+1152/ntt.h +16 -0
  127. data/vendor/NTRUplus/NTRU+1152/params.h +18 -0
  128. data/vendor/NTRUplus/NTRU+1152/poly.c +318 -0
  129. data/vendor/NTRUplus/NTRU+1152/poly.h +31 -0
  130. data/vendor/NTRUplus/NTRU+1152/randombytes.h +9 -0
  131. data/vendor/NTRUplus/NTRU+1152/symmetric.c +39 -0
  132. data/vendor/NTRUplus/NTRU+1152/symmetric.h +12 -0
  133. data/vendor/NTRUplus/NTRU+768/api.h +24 -0
  134. data/vendor/NTRUplus/NTRU+768/fips202/fips202.c +878 -0
  135. data/vendor/NTRUplus/NTRU+768/fips202/fips202.h +166 -0
  136. data/vendor/NTRUplus/NTRU+768/kem.c +286 -0
  137. data/vendor/NTRUplus/NTRU+768/ntt.c +379 -0
  138. data/vendor/NTRUplus/NTRU+768/ntt.h +16 -0
  139. data/vendor/NTRUplus/NTRU+768/params.h +18 -0
  140. data/vendor/NTRUplus/NTRU+768/poly.c +320 -0
  141. data/vendor/NTRUplus/NTRU+768/poly.h +31 -0
  142. data/vendor/NTRUplus/NTRU+768/randombytes.h +9 -0
  143. data/vendor/NTRUplus/NTRU+768/symmetric.c +39 -0
  144. data/vendor/NTRUplus/NTRU+768/symmetric.h +12 -0
  145. data/vendor/NTRUplus/NTRU+864/api.h +24 -0
  146. data/vendor/NTRUplus/NTRU+864/fips202/fips202.c +878 -0
  147. data/vendor/NTRUplus/NTRU+864/fips202/fips202.h +166 -0
  148. data/vendor/NTRUplus/NTRU+864/kem.c +286 -0
  149. data/vendor/NTRUplus/NTRU+864/ntt.c +383 -0
  150. data/vendor/NTRUplus/NTRU+864/ntt.h +16 -0
  151. data/vendor/NTRUplus/NTRU+864/params.h +18 -0
  152. data/vendor/NTRUplus/NTRU+864/poly.c +314 -0
  153. data/vendor/NTRUplus/NTRU+864/poly.h +31 -0
  154. data/vendor/NTRUplus/NTRU+864/randombytes.h +9 -0
  155. data/vendor/NTRUplus/NTRU+864/symmetric.c +39 -0
  156. data/vendor/NTRUplus/NTRU+864/symmetric.h +12 -0
  157. data/vendor/SMAUG-T/LICENSE +22 -0
  158. data/vendor/SMAUG-T/include/api.h +37 -0
  159. data/vendor/SMAUG-T/include/cbd.h +15 -0
  160. data/vendor/SMAUG-T/include/ciphertext.h +45 -0
  161. data/vendor/SMAUG-T/include/common.h +12 -0
  162. data/vendor/SMAUG-T/include/config.h +48 -0
  163. data/vendor/SMAUG-T/include/dg.h +28 -0
  164. data/vendor/SMAUG-T/include/fips202.h +41 -0
  165. data/vendor/SMAUG-T/include/hash.h +21 -0
  166. data/vendor/SMAUG-T/include/hwt.h +28 -0
  167. data/vendor/SMAUG-T/include/indcpa.h +33 -0
  168. data/vendor/SMAUG-T/include/key.h +34 -0
  169. data/vendor/SMAUG-T/include/pack.h +78 -0
  170. data/vendor/SMAUG-T/include/packring.h +50 -0
  171. data/vendor/SMAUG-T/include/params.h +84 -0
  172. data/vendor/SMAUG-T/include/poly.h +41 -0
  173. data/vendor/SMAUG-T/include/randombytes.h +11 -0
  174. data/vendor/SMAUG-T/include/toomcook.h +14 -0
  175. data/vendor/SMAUG-T/include/verify.h +16 -0
  176. data/vendor/SMAUG-T/src/cbd.c +159 -0
  177. data/vendor/SMAUG-T/src/ciphertext.c +95 -0
  178. data/vendor/SMAUG-T/src/dg.c +112 -0
  179. data/vendor/SMAUG-T/src/fips202.c +745 -0
  180. data/vendor/SMAUG-T/src/hash.c +15 -0
  181. data/vendor/SMAUG-T/src/hwt.c +121 -0
  182. data/vendor/SMAUG-T/src/indcpa.c +187 -0
  183. data/vendor/SMAUG-T/src/kem.c +194 -0
  184. data/vendor/SMAUG-T/src/key.c +102 -0
  185. data/vendor/SMAUG-T/src/pack.c +349 -0
  186. data/vendor/SMAUG-T/src/packring.c +503 -0
  187. data/vendor/SMAUG-T/src/poly.c +200 -0
  188. data/vendor/SMAUG-T/src/toomcook.c +256 -0
  189. data/vendor/SMAUG-T/src/verify.c +48 -0
  190. data/vendor/SOURCES.json +130 -0
  191. metadata +280 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f95c1887b2a5ec7382d85fc701193a27404ba01a3618d69686530a310cd48259
4
+ data.tar.gz: 71e65d041b8a9c828db977f7676aaf26c3d7e330278498867a6823b4146696a3
5
+ SHA512:
6
+ metadata.gz: 6cfa24eef04e809b60842ee49dd858c950797b33a462dcb87eb59c5db46b7df97fc1180f941a55f84e4ccc96a2168bc07c511591fed9b8876089ba2006a6cd6c
7
+ data.tar.gz: ee190f6a25021501395dbf6403027521bac354433beb5fa7a5f0d5b9cc4db6f4afc530df309b7a1708c0513d48a77bd9623f32f4ffaf1e431d98028025f49b63
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 osuolfou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # KpqC
2
+
3
+ KpqC provides synchronous Ruby APIs for AIMer, HAETAE, NTRU+, and SMAUG-T.
4
+
5
+ ## Runtime support
6
+
7
+ - Ruby 2.6 or newer
8
+ - Windows, macOS, or Linux
9
+ - A C11 compiler and Ruby development headers when building from source
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ gem install kpqc
15
+ ```
16
+
17
+ ## Available schemes
18
+
19
+ | Algorithm | Type | Exports |
20
+ | --- | --- | --- |
21
+ | **AIMer** | Signature | `aimer128f`, `aimer128s`, `aimer192f`, `aimer192s`, `aimer256f`, `aimer256s` |
22
+ | **HAETAE** | Signature | `haetae2`, `haetae3`, `haetae5` |
23
+ | **NTRU+** | Key encapsulation | `ntruplus768`, `ntruplus864`, `ntruplus1152` |
24
+ | **SMAUG‑T** | Key encapsulation | `smaugt128`, `smaugt192`, `smaugt256`, `timer` |
25
+
26
+ Requiring an algorithm family keeps the entry point focused:
27
+
28
+ ```ruby
29
+ require "kpqc/aimer"
30
+
31
+ aimer128f = KpqC::Aimer.aimer128f
32
+ payload = "release-manifest:v3".b
33
+ keys = aimer128f.generate_key_pair
34
+ proof = aimer128f.sign(payload, keys.secret_key)
35
+
36
+ unless aimer128f.verify(payload, proof, keys.public_key)
37
+ raise "Signature verification failed"
38
+ end
39
+ ```
40
+
41
+ ### Signature contexts
42
+
43
+ AIMer and HAETAE accept an optional context. A context separates signatures
44
+ created for different application purposes and may contain up to 255 bytes.
45
+
46
+ ```ruby
47
+ require "kpqc/haetae"
48
+
49
+ haetae3 = KpqC::Haetae.haetae3
50
+ payload = "account=42".b
51
+ context = "audit-record".b
52
+ keys = haetae3.generate_key_pair
53
+
54
+ signature = haetae3.sign(payload, keys.secret_key, context: context)
55
+ valid = haetae3.verify(
56
+ payload,
57
+ signature,
58
+ keys.public_key,
59
+ context: context
60
+ )
61
+
62
+ puts valid # true
63
+ ```
64
+
65
+ Verification fails when the supplied context does not match the one used for
66
+ signing.
67
+
68
+ ### Key encapsulation
69
+
70
+ A KEM creates a shared secret for a sender and a recipient. The public key may
71
+ be distributed; the secret key and resulting shared secret must remain private.
72
+
73
+ ```ruby
74
+ require "kpqc/smaugt"
75
+
76
+ smaugt192 = KpqC::SmaugT.smaugt192
77
+ recipient = smaugt192.generate_key_pair
78
+
79
+ outbound = smaugt192.encapsulate(recipient.public_key)
80
+ # Send outbound.ciphertext to the recipient.
81
+
82
+ inbound_secret = smaugt192.decapsulate(
83
+ outbound.ciphertext,
84
+ recipient.secret_key
85
+ )
86
+
87
+ puts inbound_secret == outbound.shared_secret # true
88
+ ```
89
+
90
+ ## Imports
91
+
92
+ Each family has a dedicated require path:
93
+
94
+ ```ruby
95
+ require "kpqc/aimer"
96
+ require "kpqc/haetae"
97
+ require "kpqc/ntruplus"
98
+ require "kpqc/smaugt"
99
+
100
+ signer = KpqC::Aimer.aimer256s
101
+ another_signer = KpqC::Haetae.haetae5
102
+ key_exchange = KpqC::NtruPlus.ntruplus1152
103
+ another_key_exchange = KpqC::SmaugT.timer
104
+ ```
105
+
106
+ All named algorithms are also available from the package root:
107
+
108
+ ```ruby
109
+ require "kpqc"
110
+
111
+ signer = KpqC.aimer192f
112
+ key_exchange = KpqC.ntruplus864
113
+ ```
114
+
115
+ ## Data and failures
116
+
117
+ Inputs and outputs are binary `String` instances. Each algorithm exposes an
118
+ `id` and an immutable `sizes` object.
119
+
120
+ ### Parameter sizes
121
+
122
+ All sizes are in bytes.
123
+
124
+ #### Signatures
125
+
126
+ | Algorithm | Public key | Secret key | Signature |
127
+ | --- | ---: | ---: | ---: |
128
+ | `aimer128f` | 32 | 48 | 5,888 |
129
+ | `aimer128s` | 32 | 48 | 4,160 |
130
+ | `aimer192f` | 48 | 72 | 13,056 |
131
+ | `aimer192s` | 48 | 72 | 9,120 |
132
+ | `aimer256f` | 64 | 96 | 25,120 |
133
+ | `aimer256s` | 64 | 96 | 17,056 |
134
+ | `haetae2` | 992 | 1,408 | 1,474 |
135
+ | `haetae3` | 1,472 | 2,112 | 2,349 |
136
+ | `haetae5` | 2,080 | 2,752 | 2,948 |
137
+
138
+ #### Key encapsulation
139
+
140
+ | Algorithm | Public key | Secret key | Ciphertext | Shared secret |
141
+ | --- | ---: | ---: | ---: | ---: |
142
+ | `ntruplus768` | 1,152 | 2,336 | 1,152 | 32 |
143
+ | `ntruplus864` | 1,296 | 2,624 | 1,296 | 32 |
144
+ | `ntruplus1152` | 1,728 | 3,488 | 1,728 | 32 |
145
+ | `smaugt128` | 672 | 832 | 672 | 32 |
146
+ | `smaugt192` | 1,088 | 1,312 | 992 | 32 |
147
+ | `smaugt256` | 1,440 | 1,728 | 1,376 | 32 |
148
+ | `timer` | 672 | 832 | 608 | 32 |
149
+
150
+ Methods reject values of the wrong type or size. Signature verification returns
151
+ `false` for an invalid signature. NTRU+ rejects an invalid ciphertext.
152
+ SMAUG-T performs implicit rejection and returns a replacement secret instead;
153
+ that value will not equal the sender's shared secret.
154
+
155
+ ## Distribution
156
+
157
+ The published gem includes Ruby modules, native extension sources, and the
158
+ bundled algorithm sources. Each parameter set is loaded on first use. The gem
159
+ has no runtime dependencies.
160
+
161
+ ## Security
162
+
163
+ The native cores are compiled from the upstream algorithm implementations.
164
+ This gem has not received an independent security audit and does not provide a
165
+ constant-time execution guarantee. Assess those constraints before using it
166
+ with sensitive production keys.
167
+
168
+ Secret keys stored in `KeyPair` and shared secrets stored in
169
+ `EncapsulatedSecret` can be wiped in place on a best-effort basis with
170
+ `clear!`. A `String` returned by `decapsulate` remains the caller's
171
+ responsibility.
172
+
173
+ Third-party licenses and attributions are listed in
174
+ [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+ require "rake/clean"
5
+ require "rake/testtask"
6
+ require_relative "lib/kpqc/version"
7
+
8
+ EXTENSION_DIRS = Dir[File.expand_path("ext/kpqc/*", __dir__)]
9
+ .select { |directory| File.file?(File.join(directory, "extconf.rb")) }
10
+ .sort
11
+
12
+ CLEAN.include(*EXTENSION_DIRS.flat_map do |directory|
13
+ [File.join(directory, "Makefile"), File.join(directory, "mkmf.log")]
14
+ end)
15
+ CLOBBER.include(*EXTENSION_DIRS.flat_map do |directory|
16
+ [File.join(directory, "**", "*.o"), File.join(directory, "**", "*.bundle"),
17
+ File.join(directory, "**", "*.so"), File.join(directory, "**", "*.dSYM")]
18
+ end)
19
+ CLOBBER.include("pkg")
20
+
21
+ def compile_extensions(test_entropy: false)
22
+ make = RbConfig::CONFIG["MAKE"] || "make"
23
+ EXTENSION_DIRS.each do |directory|
24
+ Dir.chdir(directory) do
25
+ environment = test_entropy ? { "KPQC_TEST_ENTROPY" => "1" } : {}
26
+ sh environment, RbConfig.ruby, "extconf.rb"
27
+ sh make, "clean"
28
+ sh make
29
+ end
30
+ end
31
+ end
32
+
33
+ desc "Compile all native parameter-set extensions"
34
+ task :compile do
35
+ compile_extensions
36
+ end
37
+
38
+ task :compile_test do
39
+ compile_extensions(test_entropy: true)
40
+ end
41
+
42
+ Rake::TestTask.new(:test => :compile_test) do |task|
43
+ task.libs << "lib"
44
+ EXTENSION_DIRS.each { |directory| task.libs << directory }
45
+ task.pattern = "test/**/*_test.rb"
46
+ task.warning = true
47
+ end
48
+
49
+ desc "Build the gem package"
50
+ task :build do
51
+ mkdir_p "pkg"
52
+ package = File.join("pkg", "kpqc-#{KpqC::VERSION}.gem")
53
+ sh RbConfig.ruby, "-S", "gem", "build", "kpqc.gemspec",
54
+ "--output", package
55
+ end
56
+
57
+ task :default => :test
@@ -0,0 +1,53 @@
1
+ # Third-party notices
2
+
3
+ The native extensions published in this gem contain code from the following
4
+ KpqC reference implementations:
5
+
6
+ - AIMer reference implementation, by the AIMer team / Samsung SDS
7
+ - HAETAE reference implementation, Copyright (c) 2026 Team HAETAE
8
+ - NTRU+ reference implementation, Copyright (c) 2024–2026 NTRU+ TEAM
9
+ - SMAUG-T reference implementation, Copyright (c) 2026 Team SMAUG-T
10
+
11
+ These components are distributed under the MIT License. Full original license
12
+ texts are included in the gem source distribution.
13
+
14
+ Imported on 2026-09-06. `vendor/SOURCES.json` records the SHA-256 of each
15
+ source file before the shared KpqC binding adaptations, under its destination
16
+ path. Original NIST notices in AIMer headers are preserved; we acknowledge
17
+ NIST as the source of the software identified by those notices.
18
+
19
+ The SHA-3/SHAKE code is based on public-domain implementations by Ronny Van
20
+ Keer and on TweetFips202 by Gilles Van Assche, Daniel J. Bernstein and Peter
21
+ Schwabe.
22
+
23
+ ## Source and Ruby binding adaptations
24
+
25
+ - Production entropy uses operating-system random-number sources through
26
+ `ext/kpqc/randombytes.c`. Upstream RNG/CTR-DRBG/AES implementations and
27
+ benchmark/KAT executables are not compiled or shipped as runtime components.
28
+ - AIMer, HAETAE, NTRU+, and SMAUG-T public operations check RNG return codes.
29
+ NTRU+ RNG declarations return `int` to propagate failure. The optional
30
+ random-seed branch in SMAUG-T's `indcpa_enc` also stops on RNG failure.
31
+ - AIMer and NTRU+ SHA-3/SHAKE contexts use embedded storage and wipe it on
32
+ release, avoiding process termination when heap allocation fails. AIMer
33
+ signing and verification allocation failures propagate to Ruby as
34
+ `KpqC::Error` or `NoMemoryError`.
35
+ - The imported SMAUG-T sources include full-vector initialization in `key.c`,
36
+ corrected output indexing in `dg.c`, and the correct output buffer size in
37
+ `packring.c`.
38
+ - Unused HAETAE helper code and local variables were removed, and a SMAUG-T
39
+ loop counter was changed to `size_t` to match its bound.
40
+ - Parameter sets are built as separate Ruby extensions with their upstream
41
+ configuration macros and hidden C symbols. The binding validates input sizes
42
+ before entering C and wipes unpublished outputs on failure.
43
+
44
+ The bundled implementations are the same adapted cores used to generate the
45
+ npm package's JavaScript runtime.
46
+
47
+ ## Known-answer tests
48
+
49
+ `test/kat` contains the first KAT record for every parameter set. AIMer,
50
+ HAETAE, and NTRU+ records come from their reference implementations. SMAUG-T
51
+ records reflect the documented source fixes also used by the npm and PyPI
52
+ packages. The tests reproduce the NIST AES-256 CTR DRBG and compare keys,
53
+ signatures, ciphertexts, and shared secrets byte-for-byte.
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.aimer("aimer128f", "128f")
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.aimer("aimer128s", "128s")
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.aimer("aimer192f", "192f")
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.aimer("aimer192s", "192s")
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.aimer("aimer256f", "256f")
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.aimer("aimer256s", "256s")
4
+
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rbconfig"
5
+
6
+ module KpqCBuild
7
+ ROOT = File.expand_path("../..", __dir__)
8
+ EXT_ROOT = __dir__
9
+ VENDOR = File.join(ROOT, "vendor")
10
+
11
+ module_function
12
+
13
+ def aimer(name, parameter)
14
+ root = File.join(VENDOR, "AIMer")
15
+ configure(
16
+ name,
17
+ [root, File.join(root, "common")],
18
+ ["aim2.c", "hash.c", "sign.c", "tree.c",
19
+ "field#{parameter[0, 3]}.c", "fips202.c"],
20
+ "PARAMS" => parameter,
21
+ "KPQC_SIGNATURE" => 1,
22
+ "KPQC_AIMER" => 1
23
+ )
24
+ end
25
+
26
+ def haetae(name, mode)
27
+ root = File.join(VENDOR, "HAETAE")
28
+ sources = %w[
29
+ decompose.c encoding.c fft.c fixpoint.c ntt.c packing.c poly.c
30
+ polyfix.c polymat.c polyvec.c reduce.c sampler.c sign.c fips202.c
31
+ symmetric-shake.c
32
+ ]
33
+ configure(
34
+ name,
35
+ [File.join(root, "include"), File.join(root, "src")],
36
+ sources,
37
+ "HAETAE_CONFIG_MODE" => mode,
38
+ "KPQC_SIGNATURE" => 1
39
+ )
40
+ end
41
+
42
+ def ntruplus(name, parameter)
43
+ root = File.join(VENDOR, "NTRUplus", "NTRU+#{parameter}")
44
+ configure(
45
+ name,
46
+ [root, File.join(root, "fips202")],
47
+ %w[kem.c ntt.c poly.c symmetric.c fips202.c],
48
+ {}
49
+ )
50
+ end
51
+
52
+ def smaugt(name, mode)
53
+ root = File.join(VENDOR, "SMAUG-T")
54
+ sources = %w[
55
+ cbd.c ciphertext.c dg.c hash.c hwt.c indcpa.c kem.c key.c pack.c
56
+ packring.c poly.c toomcook.c verify.c fips202.c
57
+ ]
58
+ configure(
59
+ name,
60
+ [File.join(root, "include"), File.join(root, "src")],
61
+ sources,
62
+ "SMAUGT_CONFIG_MODE" => mode
63
+ )
64
+ end
65
+
66
+ def configure(name, source_directories, sources, definitions)
67
+ extension_constant = name.sub(/\A([a-z])/) { Regexp.last_match(1).upcase }
68
+ definitions = definitions.merge(
69
+ "KPQC_NATIVE_INIT" => "#{name}_native",
70
+ "KPQC_NATIVE_CONST" => extension_constant
71
+ )
72
+ definitions["KPQC_TEST_ENTROPY"] = 1 if ENV["KPQC_TEST_ENTROPY"] == "1"
73
+
74
+ $VPATH << EXT_ROOT
75
+ source_directories.each do |directory|
76
+ $VPATH << directory
77
+ $INCFLAGS << " -I#{directory}"
78
+ end
79
+
80
+ $srcs = ["module.c", "randombytes.c"] + sources
81
+ definitions.each { |key, value| $defs << "-D#{key}=#{value}" }
82
+
83
+ if RbConfig::CONFIG["CC"] =~ /(?:^|\s)cl(?:\.exe)?(?:\s|$)/i
84
+ $CFLAGS << " /O2 /W0"
85
+ else
86
+ $CFLAGS << " -std=c11 -O2 -fvisibility=hidden -fno-strict-aliasing -w"
87
+ end
88
+
89
+ have_library("bcrypt") if Gem.win_platform?
90
+ have_library("m") unless Gem.win_platform?
91
+ create_makefile("kpqc/#{name}_native")
92
+ end
93
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.haetae("haetae2", 0)
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.haetae("haetae3", 1)
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.haetae("haetae5", 2)
4
+
data/ext/kpqc/module.c ADDED
@@ -0,0 +1,195 @@
1
+ /* SPDX-License-Identifier: MIT */
2
+ #include <stddef.h>
3
+ #include <stdint.h>
4
+ #include <ruby.h>
5
+ #include "api.h"
6
+
7
+ #define KPQC_JOIN_(left, right) left##right
8
+ #define KPQC_JOIN(left, right) KPQC_JOIN_(left, right)
9
+ #define KPQC_STRING_(value) #value
10
+ #define KPQC_STRING(value) KPQC_STRING_(value)
11
+ #define KPQC_INIT(name) KPQC_JOIN(Init_, name)
12
+ #define KPQC_DATA(value) ((uint8_t *)RSTRING_PTR(value))
13
+
14
+ static VALUE kpqc_error;
15
+ #ifdef KPQC_SIGNATURE
16
+ static const uint8_t kpqc_empty_input = 0;
17
+
18
+ static const uint8_t *kpqc_input(VALUE value) {
19
+ return RSTRING_LEN(value) == 0 ? &kpqc_empty_input : KPQC_DATA(value);
20
+ }
21
+ #endif
22
+
23
+ static void kpqc_require_string(VALUE value, const char *label) {
24
+ if (!RB_TYPE_P(value, T_STRING)) {
25
+ rb_raise(rb_eTypeError, "%s must be a String", label);
26
+ }
27
+ }
28
+
29
+ static void kpqc_require_size(VALUE value, long expected, const char *label) {
30
+ kpqc_require_string(value, label);
31
+ if (RSTRING_LEN(value) != expected) {
32
+ rb_raise(rb_eArgError, "%s must be %ld bytes, received %ld",
33
+ label, expected, RSTRING_LEN(value));
34
+ }
35
+ }
36
+
37
+ #ifdef KPQC_SIGNATURE
38
+ static void kpqc_require_context(VALUE context) {
39
+ kpqc_require_string(context, "context");
40
+ if (RSTRING_LEN(context) > 255) {
41
+ rb_raise(rb_eArgError,
42
+ "context cannot exceed 255 bytes, received %ld",
43
+ RSTRING_LEN(context));
44
+ }
45
+ }
46
+ #endif
47
+
48
+ static void kpqc_wipe(VALUE value) {
49
+ volatile uint8_t *bytes = KPQC_DATA(value);
50
+ long length = RSTRING_LEN(value);
51
+ while (length-- > 0) *bytes++ = 0;
52
+ }
53
+
54
+ #if defined(_MSC_VER)
55
+ #define KPQC_NORETURN __declspec(noreturn)
56
+ #elif defined(__GNUC__) || defined(__clang__)
57
+ #define KPQC_NORETURN __attribute__((noreturn))
58
+ #else
59
+ #define KPQC_NORETURN
60
+ #endif
61
+
62
+ static KPQC_NORETURN void kpqc_failure(int status, const char *operation) {
63
+ if (status == -2) rb_memerror();
64
+ rb_raise(kpqc_error, "%s failed with status %d", operation, status);
65
+ }
66
+
67
+ static VALUE kpqc_keygen(VALUE self) {
68
+ VALUE public_key = rb_str_new(NULL, CRYPTO_PUBLICKEYBYTES);
69
+ VALUE secret_key = rb_str_new(NULL, CRYPTO_SECRETKEYBYTES);
70
+ int status;
71
+ (void)self;
72
+
73
+ #ifdef KPQC_SIGNATURE
74
+ status = crypto_sign_keypair(KPQC_DATA(public_key), KPQC_DATA(secret_key));
75
+ #else
76
+ status = crypto_kem_keypair(KPQC_DATA(public_key), KPQC_DATA(secret_key));
77
+ #endif
78
+ if (status != 0) {
79
+ kpqc_wipe(public_key);
80
+ kpqc_wipe(secret_key);
81
+ kpqc_failure(status, "key generation");
82
+ }
83
+ return rb_ary_new_from_args(2, public_key, secret_key);
84
+ }
85
+
86
+ #ifdef KPQC_SIGNATURE
87
+ static VALUE kpqc_sign(VALUE self, VALUE message, VALUE secret_key,
88
+ VALUE context) {
89
+ VALUE signature;
90
+ size_t signature_length = 0;
91
+ int status;
92
+ (void)self;
93
+
94
+ kpqc_require_string(message, "message");
95
+ kpqc_require_size(secret_key, CRYPTO_SECRETKEYBYTES, "secret_key");
96
+ kpqc_require_context(context);
97
+ signature = rb_str_new(NULL, CRYPTO_BYTES);
98
+
99
+ status = crypto_sign_signature(
100
+ KPQC_DATA(signature), &signature_length,
101
+ kpqc_input(message), (size_t)RSTRING_LEN(message),
102
+ kpqc_input(context), (size_t)RSTRING_LEN(context),
103
+ KPQC_DATA(secret_key));
104
+ if (status != 0) {
105
+ kpqc_wipe(signature);
106
+ kpqc_failure(status, "signing");
107
+ }
108
+ if (signature_length != CRYPTO_BYTES) {
109
+ kpqc_wipe(signature);
110
+ rb_raise(rb_eRuntimeError,
111
+ "core returned an invalid signature size: expected %d, received %lu",
112
+ CRYPTO_BYTES, (unsigned long)signature_length);
113
+ }
114
+ return signature;
115
+ }
116
+
117
+ static VALUE kpqc_verify(VALUE self, VALUE message, VALUE signature,
118
+ VALUE public_key, VALUE context) {
119
+ int status;
120
+ (void)self;
121
+
122
+ kpqc_require_string(message, "message");
123
+ kpqc_require_string(signature, "signature");
124
+ kpqc_require_size(public_key, CRYPTO_PUBLICKEYBYTES, "public_key");
125
+ kpqc_require_context(context);
126
+ if (RSTRING_LEN(signature) != CRYPTO_BYTES) return Qfalse;
127
+
128
+ status = crypto_sign_verify(
129
+ KPQC_DATA(signature), (size_t)RSTRING_LEN(signature),
130
+ kpqc_input(message), (size_t)RSTRING_LEN(message),
131
+ kpqc_input(context), (size_t)RSTRING_LEN(context),
132
+ KPQC_DATA(public_key));
133
+ if (status == -2) kpqc_failure(status, "verification");
134
+ return status == 0 ? Qtrue : Qfalse;
135
+ }
136
+ #else
137
+ static VALUE kpqc_encapsulate(VALUE self, VALUE public_key) {
138
+ VALUE ciphertext;
139
+ VALUE shared_secret;
140
+ int status;
141
+ (void)self;
142
+
143
+ kpqc_require_size(public_key, CRYPTO_PUBLICKEYBYTES, "public_key");
144
+ ciphertext = rb_str_new(NULL, CRYPTO_CIPHERTEXTBYTES);
145
+ shared_secret = rb_str_new(NULL, CRYPTO_BYTES);
146
+ status = crypto_kem_enc(KPQC_DATA(ciphertext), KPQC_DATA(shared_secret),
147
+ KPQC_DATA(public_key));
148
+ if (status != 0) {
149
+ kpqc_wipe(ciphertext);
150
+ kpqc_wipe(shared_secret);
151
+ kpqc_failure(status, "encapsulation");
152
+ }
153
+ return rb_ary_new_from_args(2, ciphertext, shared_secret);
154
+ }
155
+
156
+ static VALUE kpqc_decapsulate(VALUE self, VALUE ciphertext, VALUE secret_key) {
157
+ VALUE shared_secret;
158
+ int status;
159
+ (void)self;
160
+
161
+ kpqc_require_size(ciphertext, CRYPTO_CIPHERTEXTBYTES, "ciphertext");
162
+ kpqc_require_size(secret_key, CRYPTO_SECRETKEYBYTES, "secret_key");
163
+ shared_secret = rb_str_new(NULL, CRYPTO_BYTES);
164
+ status = crypto_kem_dec(KPQC_DATA(shared_secret), KPQC_DATA(ciphertext),
165
+ KPQC_DATA(secret_key));
166
+ if (status != 0) {
167
+ kpqc_wipe(shared_secret);
168
+ kpqc_failure(status, "decapsulation");
169
+ }
170
+ return shared_secret;
171
+ }
172
+ #endif
173
+
174
+ RUBY_FUNC_EXPORTED void KPQC_INIT(KPQC_NATIVE_INIT)(void) {
175
+ VALUE kpqc = rb_define_module("KpqC");
176
+ VALUE native = rb_define_module_under(kpqc, "Native");
177
+ VALUE algorithm = rb_define_module_under(
178
+ native, KPQC_STRING(KPQC_NATIVE_CONST));
179
+
180
+ kpqc_error = rb_define_class_under(kpqc, "Error", rb_eStandardError);
181
+ rb_define_singleton_method(algorithm, "keygen", kpqc_keygen, 0);
182
+ #ifdef KPQC_SIGNATURE
183
+ rb_define_singleton_method(algorithm, "sign", kpqc_sign, 3);
184
+ rb_define_singleton_method(algorithm, "verify", kpqc_verify, 4);
185
+ #else
186
+ rb_define_singleton_method(algorithm, "encapsulate", kpqc_encapsulate, 1);
187
+ rb_define_singleton_method(algorithm, "decapsulate", kpqc_decapsulate, 2);
188
+ #endif
189
+ #ifdef KPQC_TEST_ENTROPY
190
+ {
191
+ void kpqc_randombytes_test_init(VALUE);
192
+ kpqc_randombytes_test_init(algorithm);
193
+ }
194
+ #endif
195
+ }
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.ntruplus("ntruplus1152", 1152)
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.ntruplus("ntruplus768", 768)
4
+
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../extconf_helper"
3
+ KpqCBuild.ntruplus("ntruplus864", 864)
4
+