hpke 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +37 -0
- data/LICENSE.txt +21 -0
- data/README.md +120 -0
- data/Rakefile +8 -0
- data/lib/hpke/dhkem.rb +368 -0
- data/lib/hpke/hkdf.rb +100 -0
- data/lib/hpke/util.rb +26 -0
- data/lib/hpke/version.rb +5 -0
- data/lib/hpke.rb +300 -0
- data/sig/hpke.rbs +4 -0
- metadata +71 -0
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
data/Gemfile
ADDED
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
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
|
data/lib/hpke/version.rb
ADDED
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
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: []
|