hpke 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c0ff20003913867f648466cb8136e73218c7dd917087c98f7d86ef8fd2049a11
4
+ data.tar.gz: 47901efa463ad51eebe04bface377e9dda4c24497a4b12039c64bc966b98c2d9
5
+ SHA512:
6
+ metadata.gz: 1bbd503dd86c43bcb19fc188338cc9119b35a2bc5e6812a1404c19f8e198b7c71c0a18d3e1ff81d9ee3658575044995dba5ac092229b61298c7e79ed2e679faf
7
+ data.tar.gz: ac0ac0b079d9a8b8c3d7e60d6d90e0109d31150ae44f433b815d1a62fce9b3bb70dc707112c7a7155f94f3e7df171a11fa3ef97ba4fccbf509a7d911866b170b
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in hpke.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,37 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hpke (0.1.0)
5
+ openssl (~> 3.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.5.0)
11
+ openssl (3.0.2)
12
+ rake (13.0.6)
13
+ rspec (3.12.0)
14
+ rspec-core (~> 3.12.0)
15
+ rspec-expectations (~> 3.12.0)
16
+ rspec-mocks (~> 3.12.0)
17
+ rspec-core (3.12.2)
18
+ rspec-support (~> 3.12.0)
19
+ rspec-expectations (3.12.3)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.12.0)
22
+ rspec-mocks (3.12.6)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.12.0)
25
+ rspec-support (3.12.1)
26
+
27
+ PLATFORMS
28
+ arm64-darwin-22
29
+ x86_64-linux
30
+
31
+ DEPENDENCIES
32
+ hpke!
33
+ rake (~> 13.0)
34
+ rspec (~> 3.0)
35
+
36
+ BUNDLED WITH
37
+ 2.4.10
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Ryo Kajiwara
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # hpke-rb
2
+
3
+ Hybrid Public Key Encryption (HPKE; [RFC 9180](https://datatracker.ietf.org/doc/html/rfc9180)) in Ruby
4
+
5
+ ## Note
6
+
7
+ This is still in very early development, so:
8
+
9
+ - APIs are subject to change
10
+ - Especially the instantation interface of KEM and HPKE suite
11
+ - This is tested against test vectors supplied by the authors of the RFC, but is not formally audited for security. Please be aware of this when using in production.
12
+
13
+ ## Supported Features
14
+
15
+ Supports all modes, KEMs, AEAD functions in RFC 9180.
16
+
17
+ - HPKE Modes
18
+ - Base
19
+ - PSK
20
+ - Auth
21
+ - AuthPSK
22
+ - Key Encapsulation Mechanisms (KEMs)
23
+ - DHKEM(P-256, HKDF-SHA256)
24
+ - DHKEM(P-384, HKDF-SHA384)
25
+ - DHKEM(P-521, HKDF-SHA512)
26
+ - DHKEM(X25519, HKDF-SHA256)
27
+ - DHKEM(X448, HKDF-SHA512)
28
+ - Key Derivation Functions (KDFs)
29
+ - HKDF-SHA256
30
+ - HKDF-SHA384
31
+ - HKDF-SHA512
32
+ - AEAD Functions
33
+ - AES-128-GCM
34
+ - AES-256-GCM
35
+ - ChaCha20-Poly1305
36
+ - Export Only
37
+
38
+ ## Supported Environments
39
+
40
+ - OpenSSL 3.0 or higher
41
+ - This is due to the changes in instantiation of public/private key pairs from OpenSSL 1.1 series to OpenSSL 3.0 series
42
+ - Ruby 3.1 or higher
43
+ - Ruby 3.1 comes with OpenSSL 3.0 support
44
+
45
+ ## Installation
46
+
47
+ Install the gem and add to the application's Gemfile by executing:
48
+
49
+ $ bundle add hpke
50
+
51
+ If bundler is not being used to manage dependencies, install the gem by executing:
52
+
53
+ $ gem install hpke
54
+
55
+ ## Usage
56
+
57
+ (example shows Base mode)
58
+
59
+ ```ruby
60
+ # instantiate HPKE suite
61
+ # first 2 parameters specify the curve and hash to be used in the KEM,
62
+ # third parameter specifies the hash to be used in the KDF (of HPKE suite),
63
+ # fourth parameter specifies the AEAD function
64
+
65
+ # we will generate a different instance just for demonstration to show that nothing secret is stored in the HPKE suite instance
66
+ hpke_s = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
67
+ hpke_r = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
68
+
69
+ # get a OpenSSL::PKey::PKey instance by either generating a key or loading a key from a PEM
70
+ # see https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/PKey.html
71
+ # on the sender's end
72
+ sender_key_pair = OpenSSL::PKey.generate_key('X25519')
73
+ receiver_key_pair = OpenSSL::PKey.generate_key('X25519')
74
+
75
+ # Sender setup
76
+ # Sender knows the receiver's public key (in PEM format, in most cases), so load that into a PKey
77
+ receiver_public_key = OpenSSL::PKey.read(receiver_key_pair.public_to_pem)
78
+ encap_result = hpke_s.setup_base_s(receiver_public_key, 'info')
79
+ # This returns a hash where :enc key contains the key encapsulation,
80
+ # and :context_s contains a HPKE::ContextS instance, which is used for encryption later on.
81
+ context_s = encap_result[:context_s]
82
+ # Note that :enc contains raw bytes, so when passing to the receiver, it is advised to pass the encapsulation using Base64-encoded values
83
+ enc_base64 = Base64.encode64(encap_result[:enc])
84
+
85
+ # Then on the receiver's end
86
+ # decode the encapsulated value
87
+ enc = Base64.decode64(enc_base64)
88
+ # then use that value to generate a HPKE::ContextR instance to use for decryption
89
+ context_r = hpke_r.setup_base_r(enc, receiver_key_pair, 'info')
90
+
91
+ # sender encrypts a message
92
+ # note that the "sequence number" is incremented each time `seal` and `open` is used
93
+ ciphertext = context_s.seal('authentication_associated_data', 'plaintext')
94
+ # this is also in raw bytes, so when sending, encoding with Base64 is advised
95
+
96
+ # then receiver decrypts the ciphertext
97
+ context_r.open('authentication_associated_data', ciphertext)
98
+ ```
99
+
100
+ - Curve names (parameter 1)
101
+ - `:p_256`, `:p_384`, `:p_521`, `:x25519`, `:x448`
102
+ - Note: `:p_256` corresponds to `prime256v1`, `:p_384` corresponds to `secp384r1`, and `:p_521` corresponds to `secp521r1` in OpenSSL
103
+ - Hash names (parameter 2 and 3)
104
+ - `:sha256`, `:sha384`, `:sha512`
105
+ - AEAD function names (parameter 4)
106
+ - `:aes_128_gcm`, `:aes_256_gcm`, `:chacha20_poly1305`, `:`
107
+
108
+ ## Development
109
+
110
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
111
+
112
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
113
+
114
+ ## Contributing
115
+
116
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sylph01/hpke-rb.
117
+
118
+ ## License
119
+
120
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/lib/hpke/dhkem.rb ADDED
@@ -0,0 +1,368 @@
1
+ require 'openssl'
2
+ require 'securerandom'
3
+ require_relative 'hkdf'
4
+ require_relative 'util'
5
+
6
+ class HPKE::DHKEM
7
+ include HPKE::Util
8
+
9
+ def initialize(hash_name)
10
+ @hkdf = HPKE::HKDF.new(hash_name)
11
+ end
12
+
13
+ def encap(pk_r)
14
+ pkey_e = generate_key_pair()
15
+ dh = pkey_e.derive(pk_r)
16
+ enc = serialize_public_key(pkey_e)
17
+
18
+ pkrm = serialize_public_key(pk_r)
19
+ kem_context = enc + pkrm
20
+
21
+ shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
22
+ {
23
+ shared_secret: shared_secret,
24
+ enc: enc
25
+ }
26
+ end
27
+
28
+ def auth_encap(pk_r, sk_s)
29
+ pkey_e = generate_key_pair()
30
+ dh = pkey_e.derive(pk_r) + sk_s.derive(pk_r)
31
+ enc = serialize_public_key(pkey_e)
32
+
33
+ pkrm = serialize_public_key(pk_r)
34
+ pksm = serialize_public_key(sk_s)
35
+ kem_context = enc + pkrm + pksm
36
+
37
+ shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
38
+ {
39
+ shared_secret: shared_secret,
40
+ enc: enc
41
+ }
42
+ end
43
+
44
+ def decap(enc, sk_r)
45
+ pk_e = deserialize_public_key(enc)
46
+ dh = sk_r.derive(pk_e)
47
+
48
+ pkrm = serialize_public_key(sk_r)
49
+ kem_context = enc + pkrm
50
+
51
+ shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
52
+ shared_secret
53
+ end
54
+
55
+ def auth_decap(enc, sk_r, pk_s)
56
+ pk_e = deserialize_public_key(enc)
57
+ dh = sk_r.derive(pk_e) + sk_r.derive(pk_s)
58
+
59
+ pkrm = serialize_public_key(sk_r)
60
+ pksm = serialize_public_key(pk_s)
61
+ kem_context = enc + pkrm + pksm
62
+
63
+ shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
64
+ shared_secret
65
+ end
66
+
67
+ def encap_fixed(pk_r, ikm_e)
68
+ pkey_e = derive_key_pair(ikm_e)
69
+ dh = pkey_e.derive(pk_r)
70
+ enc = serialize_public_key(pkey_e)
71
+
72
+ pkrm = serialize_public_key(pk_r)
73
+ kem_context = enc + pkrm
74
+
75
+ shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
76
+ {
77
+ shared_secret: shared_secret,
78
+ enc: enc
79
+ }
80
+ end
81
+
82
+ def auth_encap_fixed(pk_r, sk_s, ikm_e)
83
+ pkey_e = derive_key_pair(ikm_e)
84
+ dh = pkey_e.derive(pk_r) + sk_s.derive(pk_r)
85
+ enc = serialize_public_key(pkey_e)
86
+
87
+ pkrm = serialize_public_key(pk_r)
88
+ pksm = serialize_public_key(sk_s)
89
+ kem_context = enc + pkrm + pksm
90
+
91
+ shared_secret = extract_and_expand(dh, kem_context, kem_suite_id)
92
+ {
93
+ shared_secret: shared_secret,
94
+ enc: enc
95
+ }
96
+ end
97
+
98
+ def generate_key_pair
99
+ derive_key_pair(SecureRandom.random_bytes(n_sk))
100
+ end
101
+
102
+ # ---- functions for Edwards curves (X25519, X448) ----
103
+ def derive_key_pair(ikm)
104
+ dkp_prk = @hkdf.labeled_extract('', 'dkp_prk', ikm, kem_suite_id)
105
+ sk = @hkdf.labeled_expand(dkp_prk, 'sk', '', n_sk, kem_suite_id)
106
+
107
+ create_key_pair_from_secret(sk)
108
+ end
109
+
110
+ def serialize_public_key(pk)
111
+ pk.public_to_der[-n_pk, n_pk]
112
+ end
113
+
114
+ def deserialize_public_key(serialized_pk)
115
+ asn1_seq_pub = OpenSSL::ASN1.Sequence([
116
+ OpenSSL::ASN1.Sequence([
117
+ OpenSSL::ASN1.ObjectId(asn1_oid)
118
+ ]),
119
+ OpenSSL::ASN1.BitString(serialized_pk)
120
+ ])
121
+
122
+ OpenSSL::PKey.read(asn1_seq_pub.to_der)
123
+ end
124
+
125
+ private
126
+
127
+ def kem_suite_id
128
+ 'KEM' + i2osp(kem_id, 2)
129
+ end
130
+
131
+ def extract_and_expand(dh, kem_context, suite_id)
132
+ eae_prk = @hkdf.labeled_extract('', 'eae_prk', dh, suite_id)
133
+
134
+ @hkdf.labeled_expand(eae_prk, 'shared_secret', kem_context, n_secret, suite_id)
135
+ end
136
+ end
137
+
138
+ class HPKE::DHKEM::EC < HPKE::DHKEM
139
+ def derive_key_pair(ikm)
140
+ dkp_prk = @hkdf.labeled_extract('', 'dkp_prk', ikm, kem_suite_id)
141
+ sk = 0
142
+ counter = 0
143
+ while sk == 0 || sk >= order do
144
+ raise Exception.new('DeriveKeyPairError') if counter > 255
145
+
146
+ bytes = @hkdf.labeled_expand(dkp_prk, 'candidate', i2osp(counter, 1), n_sk, kem_suite_id)
147
+ bytes[0] = (bytes[0].ord & bitmask).chr
148
+ sk = os2ip(bytes)
149
+ counter += 1
150
+ end
151
+
152
+ create_key_pair_from_secret(bytes)
153
+ end
154
+
155
+ def create_key_pair_from_secret(secret)
156
+ asn1_seq = OpenSSL::ASN1.Sequence([
157
+ OpenSSL::ASN1.Integer(1),
158
+ OpenSSL::ASN1.OctetString(secret),
159
+ OpenSSL::ASN1.ObjectId(curve_name, 0, :EXPLICIT)
160
+ ])
161
+
162
+ OpenSSL::PKey.read(asn1_seq.to_der)
163
+ end
164
+
165
+ def serialize_public_key(pk)
166
+ pk.public_key.to_bn.to_s(2)
167
+ end
168
+
169
+ def deserialize_public_key(serialized_pk)
170
+ asn1_seq = OpenSSL::ASN1.Sequence([
171
+ OpenSSL::ASN1.Sequence([
172
+ OpenSSL::ASN1.ObjectId("id-ecPublicKey"),
173
+ OpenSSL::ASN1.ObjectId(curve_name)
174
+ ]),
175
+ OpenSSL::ASN1.BitString(serialized_pk)
176
+ ])
177
+
178
+ OpenSSL::PKey.read(asn1_seq.to_der)
179
+ end
180
+ end
181
+
182
+ class HPKE::DHKEM::EC::P_256 < HPKE::DHKEM::EC
183
+ def kem_id
184
+ 0x0010
185
+ end
186
+
187
+ private
188
+
189
+ def n_secret
190
+ 32
191
+ end
192
+
193
+ def n_enc
194
+ 65
195
+ end
196
+
197
+ def n_pk
198
+ 65
199
+ end
200
+
201
+ def n_sk
202
+ 32
203
+ end
204
+
205
+ def order
206
+ 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
207
+ end
208
+
209
+ def curve_name
210
+ 'prime256v1'
211
+ end
212
+
213
+ def bitmask
214
+ 0xff
215
+ end
216
+ end
217
+
218
+ class HPKE::DHKEM::EC::P_384 < HPKE::DHKEM::EC
219
+ def kem_id
220
+ 0x0011
221
+ end
222
+
223
+ private
224
+
225
+ def n_secret
226
+ 48
227
+ end
228
+
229
+ def n_enc
230
+ 97
231
+ end
232
+
233
+ def n_pk
234
+ 97
235
+ end
236
+
237
+ def n_sk
238
+ 48
239
+ end
240
+
241
+ def order
242
+ 0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973
243
+ end
244
+
245
+ def curve_name
246
+ 'secp384r1'
247
+ end
248
+
249
+ def bitmask
250
+ 0xff
251
+ end
252
+ end
253
+
254
+ class HPKE::DHKEM::EC::P_521 < HPKE::DHKEM::EC
255
+ def kem_id
256
+ 0x0012
257
+ end
258
+
259
+ private
260
+
261
+ def n_secret
262
+ 64
263
+ end
264
+
265
+ def n_enc
266
+ 133
267
+ end
268
+
269
+ def n_pk
270
+ 133
271
+ end
272
+
273
+ def n_sk
274
+ 66
275
+ end
276
+
277
+ def order
278
+ 0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409
279
+ end
280
+
281
+ def curve_name
282
+ 'secp521r1'
283
+ end
284
+
285
+ def bitmask
286
+ 0x01
287
+ end
288
+ end
289
+
290
+ class HPKE::DHKEM::X25519 < HPKE::DHKEM
291
+ def kem_id
292
+ 0x0020
293
+ end
294
+
295
+ def create_key_pair_from_secret(secret)
296
+ asn1_seq = OpenSSL::ASN1.Sequence([
297
+ OpenSSL::ASN1.Integer(0),
298
+ OpenSSL::ASN1.Sequence([
299
+ OpenSSL::ASN1.ObjectId(asn1_oid)
300
+ ]),
301
+ OpenSSL::ASN1.OctetString("\x04\x20" + secret)
302
+ ])
303
+
304
+ OpenSSL::PKey.read(asn1_seq.to_der)
305
+ end
306
+
307
+ private
308
+
309
+ def n_secret
310
+ 32
311
+ end
312
+
313
+ def n_enc
314
+ 32
315
+ end
316
+
317
+ def n_pk
318
+ 32
319
+ end
320
+
321
+ def n_sk
322
+ 32
323
+ end
324
+
325
+ def asn1_oid
326
+ '1.3.101.110'
327
+ end
328
+ end
329
+
330
+ class HPKE::DHKEM::X448 < HPKE::DHKEM
331
+ def kem_id
332
+ 0x0021
333
+ end
334
+
335
+ def create_key_pair_from_secret(secret)
336
+ asn1_seq = OpenSSL::ASN1.Sequence([
337
+ OpenSSL::ASN1.Integer(0),
338
+ OpenSSL::ASN1.Sequence([
339
+ OpenSSL::ASN1.ObjectId(asn1_oid)
340
+ ]),
341
+ OpenSSL::ASN1.OctetString("\x04\x38" + secret)
342
+ ])
343
+
344
+ OpenSSL::PKey.read(asn1_seq.to_der)
345
+ end
346
+
347
+ private
348
+
349
+ def n_secret
350
+ 64
351
+ end
352
+
353
+ def n_enc
354
+ 56
355
+ end
356
+
357
+ def n_pk
358
+ 56
359
+ end
360
+
361
+ def n_sk
362
+ 56
363
+ end
364
+
365
+ def asn1_oid
366
+ '1.3.101.111'
367
+ end
368
+ end
data/lib/hpke/hkdf.rb ADDED
@@ -0,0 +1,100 @@
1
+ require 'openssl'
2
+ require_relative 'util'
3
+
4
+ class HPKE::HKDF
5
+ include HPKE::Util
6
+
7
+ attr_reader :kdf_id
8
+
9
+ ALGORITHMS = {
10
+ sha256: {
11
+ name: 'SHA256',
12
+ kdf_id: 1
13
+ },
14
+ sha384: {
15
+ name: 'SHA384',
16
+ kdf_id: 2
17
+ },
18
+ sha512: {
19
+ name: 'SHA512',
20
+ kdf_id: 3
21
+ }
22
+ }
23
+
24
+ def n_h
25
+ @digest.digest_length
26
+ end
27
+
28
+ def initialize(alg_name)
29
+ if algorithm = ALGORITHMS[alg_name]
30
+ @digest = OpenSSL::Digest.new(algorithm[:name])
31
+ @kdf_id = algorithm[:kdf_id]
32
+ else
33
+ raise Exception.new('Unknown hash algorithm')
34
+ end
35
+ end
36
+
37
+ def hmac(key, data)
38
+ OpenSSL::HMAC.digest(@digest, key, data)
39
+ end
40
+
41
+ def extract(salt, ikm)
42
+ hmac(salt, ikm)
43
+ end
44
+
45
+ def expand(prk, info, len)
46
+ n = (len.to_f / @digest.digest_length).ceil
47
+ t = ['']
48
+ for i in 0..n do
49
+ t << hmac(prk, t[i] + info + (i + 1).chr)
50
+ end
51
+ t_concat = t.join
52
+ t_concat[0..(len - 1)]
53
+ end
54
+
55
+ def labeled_extract(salt, label, ikm, suite_id)
56
+ labeled_ikm = 'HPKE-v1' + suite_id + label + ikm
57
+ extract(salt, labeled_ikm)
58
+ end
59
+
60
+ def labeled_expand(prk, label, info, l, suite_id)
61
+ labeled_info = i2osp(l, 2) + 'HPKE-v1' + suite_id + label + info
62
+ expand(prk, labeled_info, l)
63
+ end
64
+ end
65
+
66
+ class HPKE::HKDF::HMAC_SHA256 < HPKE::HKDF
67
+ private
68
+
69
+ def digest_algorithm
70
+ 'SHA256'
71
+ end
72
+
73
+ def kdf_id
74
+ 1
75
+ end
76
+ end
77
+
78
+ class HPKE::HKDF::HMAC_SHA384 < HPKE::HKDF
79
+ private
80
+
81
+ def digest_algorithm
82
+ 'SHA384'
83
+ end
84
+
85
+ def kdf_id
86
+ 2
87
+ end
88
+ end
89
+
90
+ class HPKE::HKDF::HMAC_SHA512 < HPKE::HKDF
91
+ private
92
+
93
+ def digest_algorithm
94
+ 'SHA512'
95
+ end
96
+
97
+ def kdf_id
98
+ 3
99
+ end
100
+ end
data/lib/hpke/util.rb ADDED
@@ -0,0 +1,26 @@
1
+ module HPKE::Util
2
+ def i2osp(n, w)
3
+ # check n > 0 and n < 256 ** w
4
+ ret = []
5
+ for i in 0..(w - 1)
6
+ ret[w - (i + 1)] = n % 256
7
+ n = n >> 8
8
+ end
9
+ ret.map(&:chr).join
10
+ end
11
+
12
+ def os2ip(x)
13
+ x.bytes.reduce { |a, b| a * 256 + b }
14
+ end
15
+
16
+ def xor(a, b)
17
+ if a.bytesize != b.bytesize
18
+ return false
19
+ end
20
+ c = ""
21
+ for i in 0 .. (a.bytesize - 1)
22
+ c += (a.bytes[i] ^ b.bytes[i]).chr
23
+ end
24
+ c
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HPKE
4
+ VERSION = "0.1.0"
5
+ end
data/lib/hpke.rb ADDED
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hpke/version"
4
+
5
+ require 'openssl'
6
+ require_relative './hpke/dhkem'
7
+ require_relative './hpke/util'
8
+
9
+ class HPKE
10
+ include HPKE::Util
11
+
12
+ attr_reader :kem, :hkdf, :aead_name, :n_k, :n_n, :n_t
13
+
14
+ MODES = {
15
+ base: 0x00,
16
+ psk: 0x01,
17
+ auth: 0x02,
18
+ auth_psk: 0x03
19
+ }
20
+ CIPHERS = {
21
+ aes_128_gcm: {
22
+ name: 'aes-128-gcm',
23
+ aead_id: 0x0001,
24
+ n_k: 16,
25
+ n_n: 12,
26
+ n_t: 16
27
+ },
28
+ aes_256_gcm: {
29
+ name: 'aes-256-gcm',
30
+ aead_id: 0x0002,
31
+ n_k: 32,
32
+ n_n: 12,
33
+ n_t: 16
34
+ },
35
+ chacha20_poly1305: {
36
+ name: 'chacha20-poly1305',
37
+ aead_id: 0x0003,
38
+ n_k: 32,
39
+ n_n: 12,
40
+ n_t: 16
41
+ },
42
+ export_only: {
43
+ aead_id: 0xffff
44
+ }
45
+ }
46
+ HASHES = {
47
+ sha256: {
48
+ name: 'SHA256',
49
+ kdf_id: 1
50
+ },
51
+ sha384: {
52
+ name: 'SHA384',
53
+ kdf_id: 2
54
+ },
55
+ sha512: {
56
+ name: 'SHA512',
57
+ kdf_id: 3
58
+ }
59
+ }
60
+ KEM_CURVES = {
61
+ p_256: DHKEM::EC::P_256,
62
+ p_384: DHKEM::EC::P_384,
63
+ p_521: DHKEM::EC::P_521,
64
+ x25519: DHKEM::X25519,
65
+ x448: DHKEM::X448
66
+ }
67
+
68
+ def initialize(kem_curve_name, kem_hash, kdf_hash, aead_cipher)
69
+ raise Exception.new('Unsupported KEM curve name') if KEM_CURVES[kem_curve_name].nil?
70
+ raise Exception.new('Unsupported AEAD cipher name') if CIPHERS[aead_cipher].nil?
71
+
72
+ @kem = KEM_CURVES[kem_curve_name].new(kem_hash)
73
+ @hkdf = HKDF.new(kdf_hash)
74
+ @aead_name = CIPHERS[aead_cipher][:name]
75
+ @aead_id = CIPHERS[aead_cipher][:aead_id]
76
+ @n_k = CIPHERS[aead_cipher][:n_k]
77
+ @n_n = CIPHERS[aead_cipher][:n_n]
78
+ @n_t = CIPHERS[aead_cipher][:n_t]
79
+ end
80
+
81
+ # public facing APIs
82
+ def setup_base_s(pk_r, info)
83
+ encap_result = @kem.encap(pk_r)
84
+ {
85
+ enc: encap_result[:enc],
86
+ context_s: key_schedule_s(MODES[:base], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
87
+ }
88
+ end
89
+
90
+ def setup_base_r(enc, sk_r, info)
91
+ shared_secret = @kem.decap(enc, sk_r)
92
+ key_schedule_r(MODES[:base], shared_secret, info, DEFAULT_PSK, DEFAULT_PSK_ID)
93
+ end
94
+
95
+ def setup_psk_s(pk_r, info, psk, psk_id)
96
+ encap_result = @kem.encap(pk_r)
97
+ {
98
+ enc: encap_result[:enc],
99
+ context_s: key_schedule_s(MODES[:psk], encap_result[:shared_secret], info, psk, psk_id)
100
+ }
101
+ end
102
+
103
+ def setup_psk_r(enc, sk_r, info, psk, psk_id)
104
+ shared_secret = @kem.decap(enc, sk_r)
105
+ key_schedule_r(MODES[:psk], shared_secret, info, psk, psk_id)
106
+ end
107
+
108
+ def setup_auth_s(pk_r, info, sk_s)
109
+ encap_result = @kem.auth_encap(pk_r, sk_s)
110
+ {
111
+ enc: encap_result[:enc],
112
+ context_s: key_schedule_s(MODES[:auth], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
113
+ }
114
+ end
115
+
116
+ def setup_auth_r(enc, sk_r, info, pk_s)
117
+ shared_secret = @kem.auth_decap(enc, sk_r, pk_s)
118
+ key_schedule_r(MODES[:auth], shared_secret, info, DEFAULT_PSK, DEFAULT_PSK_ID)
119
+ end
120
+
121
+ def setup_auth_psk_s(pk_r, info, psk, psk_id, sk_s)
122
+ encap_result = @kem.auth_encap(pk_r, sk_s)
123
+ {
124
+ enc: encap_result[:enc],
125
+ context_s: key_schedule_s(MODES[:auth_psk], encap_result[:shared_secret], info, psk, psk_id)
126
+ }
127
+ end
128
+
129
+ def setup_auth_psk_r(enc, sk_r, info, psk, psk_id, pk_s)
130
+ shared_secret = @kem.auth_decap(enc, sk_r, pk_s)
131
+ key_schedule_r(MODES[:auth_psk], shared_secret, info, psk, psk_id)
132
+ end
133
+
134
+ # for testing purposes
135
+ def setup_base_s_fixed(pk_r, info, ikm_e)
136
+ encap_result = @kem.encap_fixed(pk_r, ikm_e)
137
+ {
138
+ enc: encap_result[:enc],
139
+ context_s: key_schedule_s(MODES[:base], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
140
+ }
141
+ end
142
+
143
+ def setup_psk_s_fixed(pk_r, info, psk, psk_id, ikm_e)
144
+ encap_result = @kem.encap_fixed(pk_r, ikm_e)
145
+ {
146
+ enc: encap_result[:enc],
147
+ context_s: key_schedule_s(MODES[:psk], encap_result[:shared_secret], info, psk, psk_id)
148
+ }
149
+ end
150
+
151
+ def setup_auth_s_fixed(pk_r, info, sk_s, ikm_e)
152
+ encap_result = @kem.auth_encap_fixed(pk_r, sk_s, ikm_e)
153
+ {
154
+ enc: encap_result[:enc],
155
+ context_s: key_schedule_s(MODES[:auth], encap_result[:shared_secret], info, DEFAULT_PSK, DEFAULT_PSK_ID)
156
+ }
157
+ end
158
+
159
+ def setup_auth_psk_s_fixed(pk_r, info, psk, psk_id, sk_s, ikm_e)
160
+ encap_result = @kem.auth_encap_fixed(pk_r, sk_s, ikm_e)
161
+ {
162
+ enc: encap_result[:enc],
163
+ context_s: key_schedule_s(MODES[:auth_psk], encap_result[:shared_secret], info, psk, psk_id)
164
+ }
165
+ end
166
+
167
+ def export(exporter_secret, exporter_context, len)
168
+ @hkdf.labeled_expand(exporter_secret, 'sec', exporter_context, len, suite_id)
169
+ end
170
+
171
+ private
172
+
173
+ def suite_id
174
+ 'HPKE' + i2osp(@kem.kem_id, 2) + i2osp(@hkdf.kdf_id, 2) + i2osp(@aead_id, 2)
175
+ end
176
+
177
+ DEFAULT_PSK = ''
178
+ DEFAULT_PSK_ID = ''
179
+
180
+ def verify_psk_inputs(mode, psk, psk_id)
181
+ got_psk = (psk != DEFAULT_PSK)
182
+ got_psk_id = (psk_id != DEFAULT_PSK_ID)
183
+
184
+ raise Exception.new('Inconsistent PSK inputs') if got_psk != got_psk_id
185
+ raise Exception.new('PSK input provided when not needed') if got_psk && [MODES[:base], MODES[:auth]].include?(mode)
186
+ raise Exception.new('Missing required PSK input') if !got_psk && [MODES[:psk], MODES[:auth_psk]].include?(mode)
187
+
188
+ true
189
+ end
190
+
191
+ def key_schedule(mode, shared_secret, info, psk = '', psk_id = '')
192
+ verify_psk_inputs(mode, psk, psk_id)
193
+
194
+ psk_id_hash = @hkdf.labeled_extract('', 'psk_id_hash', psk_id, suite_id)
195
+ info_hash = @hkdf.labeled_extract('', 'info_hash', info, suite_id)
196
+ key_schedule_context = mode.chr + psk_id_hash + info_hash
197
+
198
+ secret = @hkdf.labeled_extract(shared_secret, 'secret', psk, suite_id)
199
+
200
+ unless @aead_id == CIPHERS[:export_only][:aead_id]
201
+ key = @hkdf.labeled_expand(secret, 'key', key_schedule_context, @n_k, suite_id)
202
+ base_nonce = @hkdf.labeled_expand(secret, 'base_nonce', key_schedule_context, @n_n, suite_id)
203
+ end
204
+ exporter_secret = @hkdf.labeled_expand(secret, 'exp', key_schedule_context, @hkdf.n_h, suite_id)
205
+
206
+ {
207
+ key: key,
208
+ base_nonce: base_nonce,
209
+ sequence_number: 0,
210
+ exporter_secret: exporter_secret
211
+ }
212
+ end
213
+
214
+ def key_schedule_s(mode, shared_secret, info, psk = '', psk_id = '')
215
+ ks = key_schedule(mode, shared_secret, info, psk, psk_id)
216
+ HPKE::ContextS.new(ks, self)
217
+ end
218
+
219
+ def key_schedule_r(mode, shared_secret, info, psk = '', psk_id = '')
220
+ ks = key_schedule(mode, shared_secret, info, psk, psk_id)
221
+ HPKE::ContextR.new(ks, self)
222
+ end
223
+ end
224
+
225
+ class HPKE::Context
226
+ include HPKE::Util
227
+ attr_reader :key, :base_nonce, :sequence_number, :exporter_secret
228
+
229
+ def initialize(initializer_hash, hpke)
230
+ @hpke = hpke
231
+ @key = initializer_hash[:key]
232
+ @base_nonce = initializer_hash[:base_nonce]
233
+ @sequence_number = initializer_hash[:sequence_number]
234
+ @exporter_secret = initializer_hash[:exporter_secret]
235
+ end
236
+
237
+ def compute_nonce(seq)
238
+ seq_bytes = i2osp(seq, @hpke.n_n)
239
+ xor(@base_nonce, seq_bytes)
240
+ end
241
+
242
+ def increment_seq
243
+ raise Exception.new('MessageLimitReachedError') if @sequence_number >= (1 << (8 * @hpke.n_n)) - 1
244
+
245
+ @sequence_number += 1
246
+ end
247
+
248
+ def export(exporter_context, len)
249
+ @hpke.export(@exporter_secret, exporter_context, len)
250
+ end
251
+ end
252
+
253
+ class HPKE::ContextS < HPKE::Context
254
+ def seal(aad, pt)
255
+ raise Exception.new('AEAD is export only') if @hpke.aead_name == :export_only
256
+
257
+ ct = cipher_seal(@key, compute_nonce(@sequence_number), aad, pt)
258
+ increment_seq
259
+ ct
260
+ end
261
+
262
+ private
263
+
264
+ def cipher_seal(key, nonce, aad, pt)
265
+ cipher = OpenSSL::Cipher.new(@hpke.aead_name)
266
+ cipher.encrypt
267
+ cipher.key = key
268
+ cipher.iv = nonce
269
+ cipher.auth_data = aad
270
+ cipher.padding = 0
271
+ s = cipher.update(pt) << cipher.final
272
+ s + cipher.auth_tag
273
+ end
274
+ end
275
+
276
+ class HPKE::ContextR < HPKE::Context
277
+ def open(aad, ct)
278
+ raise Exception.new('AEAD is export only') if @hpke.aead_name == :export_only
279
+
280
+ pt = cipher_open(@key, compute_nonce(@sequence_number), aad, ct)
281
+ # TODO: catch openerror then send out own openerror
282
+ increment_seq
283
+ pt
284
+ end
285
+
286
+ private
287
+
288
+ def cipher_open(key, nonce, aad, ct)
289
+ ct_body = ct[0, ct.length - @hpke.n_t]
290
+ tag = ct[-@hpke.n_t, @hpke.n_t]
291
+ cipher = OpenSSL::Cipher.new(@hpke.aead_name)
292
+ cipher.decrypt
293
+ cipher.key = key
294
+ cipher.iv = nonce
295
+ cipher.auth_tag = tag
296
+ cipher.auth_data = aad
297
+ cipher.padding = 0
298
+ cipher.update(ct_body) << cipher.final
299
+ end
300
+ end
data/sig/hpke.rbs ADDED
@@ -0,0 +1,4 @@
1
+ class HPKE
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hpke
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryo Kajiwara
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-07-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: openssl
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ description: Hybrid Public Key Encryption (HPKE; RFC 9180) on Ruby
28
+ email:
29
+ - sylph01@s01.ninja
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".rspec"
35
+ - Gemfile
36
+ - Gemfile.lock
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - lib/hpke.rb
41
+ - lib/hpke/dhkem.rb
42
+ - lib/hpke/hkdf.rb
43
+ - lib/hpke/util.rb
44
+ - lib/hpke/version.rb
45
+ - sig/hpke.rbs
46
+ homepage: https://github.com/sylph01/hpke-rb
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ homepage_uri: https://github.com/sylph01/hpke-rb
51
+ source_code_uri: https://github.com/sylph01/hpke-rb
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.1.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.4.10
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Hybrid Public Key Encryption (HPKE; RFC 9180) on Ruby
71
+ test_files: []