tinydtls 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/tinydtls/context.rb +18 -3
- data/lib/tinydtls/security_conf.rb +67 -11
- data/lib/tinydtls/session.rb +20 -14
- data/lib/tinydtls/session_manager.rb +33 -22
- data/lib/tinydtls/udpsocket.rb +55 -28
- data/lib/tinydtls/wrapper.rb +1 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b48d5e01a994d57069810a71731fd775130cb0231a3fc3e8d3ac2e7fb574d7a
|
4
|
+
data.tar.gz: 2379fc9678ee3ebdac8c6221f78e6e90287693709268bb3fde5792abdf6478bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b844e51a0fc9c877b2ca94083af2b043c3654f78d7538fa7d5dfc2e0f83af2546a73328c89a820bc232aaa41e0b05418504dfbc7894a2225a403129b8bacff0
|
7
|
+
data.tar.gz: '0585c7d6f6aaa33295807097b30567399ac1de499bb2fc16def2b0a57f0defa22836daa1f438ef9caf8e6ae6fc78548fdd6af95e4f29b0de92a4d30f8d961d0e'
|
data/lib/tinydtls/context.rb
CHANGED
@@ -19,11 +19,14 @@ module TinyDTLS
|
|
19
19
|
@sendfn = sendfn
|
20
20
|
@queue = queue
|
21
21
|
@secconf = secconf
|
22
|
+
|
23
|
+
@ffi_struct = Wrapper::DTLSContextStruct.new(
|
24
|
+
Wrapper::dtls_new_context(FFI::Pointer.new(key)))
|
22
25
|
end
|
23
26
|
|
24
|
-
#
|
25
|
-
# dtls_context_t`. Such a pointer is,
|
26
|
-
# various tinydtls callback functions.
|
27
|
+
# Retrieves an instance of this class from the TinyDTLS::CONTEXT_MAP
|
28
|
+
# using a pointer to a `struct dtls_context_t`. Such a pointer is,
|
29
|
+
# for instance, passed to the various tinydtls callback functions.
|
27
30
|
#
|
28
31
|
# The `struct dtls_context_t` which the given pointer points to must
|
29
32
|
# have been created by TinyDTLS::UDPSocket#initialize.
|
@@ -31,5 +34,17 @@ module TinyDTLS
|
|
31
34
|
obj = Wrapper::DTLSContextStruct.new(ptr)
|
32
35
|
return CONTEXT_MAP[Wrapper::dtls_get_app_data(obj).to_i]
|
33
36
|
end
|
37
|
+
|
38
|
+
# Returns a key which should be used to store this context in the
|
39
|
+
# global TinyDTLS::CONTEXT_MAP.
|
40
|
+
def key
|
41
|
+
object_id
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns an FFI::Struct for this object, representing the
|
45
|
+
# underlying `dtls_context_t`.
|
46
|
+
def to_ffi
|
47
|
+
@ffi_struct
|
48
|
+
end
|
34
49
|
end
|
35
50
|
end
|
@@ -4,6 +4,10 @@ module TinyDTLS
|
|
4
4
|
# function pointer used in the `dtls_handler_t` struct which is used
|
5
5
|
# by tinydtls to retrieve keys and identities.
|
6
6
|
#
|
7
|
+
# The API of this class is quite strict and raises lots of exceptions
|
8
|
+
# because it is quite annoying to debug errors occuring in the
|
9
|
+
# GetPSKInfo callback.
|
10
|
+
#
|
7
11
|
# XXX: Currently this function doesn't map IP address to keys/identities.
|
8
12
|
class SecurityConfig
|
9
13
|
# Implementation of the `get_psk_info` function pointer as used by
|
@@ -44,27 +48,43 @@ module TinyDTLS
|
|
44
48
|
end
|
45
49
|
end
|
46
50
|
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
@default_key = default_key
|
53
|
-
|
51
|
+
# Creates a new instance of this class. At least one key/identity
|
52
|
+
# pair need to be added to the new instance of this class using
|
53
|
+
# #add_client otherwise the #default_id and #default_key methods
|
54
|
+
# always raise an error causing TinyDTLS handshakes to fail.
|
55
|
+
def initialize
|
54
56
|
@identity_map = Hash.new
|
55
57
|
end
|
56
58
|
|
57
|
-
# Adds a security configuration for the given identity
|
59
|
+
# Adds a security configuration for the given identity, key must be
|
60
|
+
# non-null otherwise a TypeError is raise.
|
58
61
|
def add_client(id, key)
|
59
|
-
|
62
|
+
if key.nil?
|
63
|
+
raise TypeError.new("Key must be non-nil")
|
64
|
+
else
|
65
|
+
@identity_map[id] = key
|
66
|
+
end
|
60
67
|
end
|
61
68
|
|
62
|
-
# Retrieves the key associated with the given identity
|
69
|
+
# Retrieves the key associated with the given identity, nil is
|
70
|
+
# returned if no key was specified for the given identity.
|
63
71
|
def get_key(id)
|
64
|
-
@identity_map
|
72
|
+
if @identity_map.has_key? id
|
73
|
+
@identity_map[id]
|
74
|
+
end
|
65
75
|
end
|
66
76
|
|
77
|
+
# Retrieves the default identity used for establishing new
|
78
|
+
# handshakes. If a #default_id hasn't been explicitly set it returns
|
79
|
+
# the first identity added using #add_client.
|
80
|
+
#
|
81
|
+
# At least one identity must have been added to the instance,
|
82
|
+
# otherwise this methods raises a TypeError.
|
67
83
|
def default_id
|
84
|
+
if @identity_map.empty?
|
85
|
+
raise TypeError.new("Cannot retrieve a default identity from an empty store")
|
86
|
+
end
|
87
|
+
|
68
88
|
if @default_id.nil?
|
69
89
|
@identity_map.to_a.first.first
|
70
90
|
else
|
@@ -72,12 +92,48 @@ module TinyDTLS
|
|
72
92
|
end
|
73
93
|
end
|
74
94
|
|
95
|
+
# Changes the default identity, the given identity must already
|
96
|
+
# exist in the store.
|
97
|
+
def default_id=(id)
|
98
|
+
if @identity_map.has_key? id
|
99
|
+
@default_id = id
|
100
|
+
else
|
101
|
+
raise TypeError.new("Default identity must already exist")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Retrieves the default key used when a call to #GetPSKInfo didn't
|
106
|
+
# specify a key. If a #default_key hasn't been explicitly set it
|
107
|
+
# returns the first key added using #add_client.
|
108
|
+
#
|
109
|
+
# At least one key must have been added to the instance,
|
110
|
+
# otherwise this methods raises a TypeError.
|
75
111
|
def default_key
|
112
|
+
if @identity_map.empty?
|
113
|
+
raise TypeError.new("Cannot retrieve a default key from an empty store")
|
114
|
+
end
|
115
|
+
|
76
116
|
if @default_key.nil?
|
77
117
|
@identity_map.to_a.first.last
|
78
118
|
else
|
79
119
|
@default_key
|
80
120
|
end
|
81
121
|
end
|
122
|
+
|
123
|
+
# Changes the default key, the given key must already exist in the store.
|
124
|
+
def default_key=(key)
|
125
|
+
if @identity_map.key(key)
|
126
|
+
@default_key = key
|
127
|
+
else
|
128
|
+
raise TypeError.new("Default key must already exist")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Sets the #default_id and #default_key, restrictions mentioned in
|
133
|
+
# #default_id= and #default_key= apply.
|
134
|
+
def set_defaults(id, key)
|
135
|
+
default_id = id
|
136
|
+
default_key = key
|
137
|
+
end
|
82
138
|
end
|
83
139
|
end
|
data/lib/tinydtls/session.rb
CHANGED
@@ -3,11 +3,14 @@ module TinyDTLS
|
|
3
3
|
class Session
|
4
4
|
attr_reader :addrinfo
|
5
5
|
|
6
|
-
# Creates a new instance of this class from
|
6
|
+
# Creates a new instance of this class from a given Addrinfo
|
7
|
+
# instance. This functions allocates memory for the underlying
|
8
|
+
# `session_t` type which needs to be freed explicitly freed using
|
9
|
+
# #close.
|
7
10
|
def initialize(addrinfo)
|
8
11
|
@addrinfo = addrinfo
|
9
12
|
unless @addrinfo.is_a? Addrinfo
|
10
|
-
raise TypeError
|
13
|
+
raise TypeError.new("Expected Addrinfo or FFI::Pointer")
|
11
14
|
end
|
12
15
|
|
13
16
|
sockaddr = @addrinfo.to_sockaddr
|
@@ -17,15 +20,13 @@ module TinyDTLS
|
|
17
20
|
end
|
18
21
|
end
|
19
22
|
|
20
|
-
#
|
21
|
-
#
|
22
|
-
|
23
|
-
def self.from_ptr(ptr)
|
23
|
+
# Extracts an Addrinfo instance of a FFI::Pointer to a `session_t`
|
24
|
+
# as returned by #to_ptr.
|
25
|
+
def self.addr_from_ptr(ptr)
|
24
26
|
lenptr = Wrapper::SocklenPtr.new
|
25
27
|
sockaddr = Wrapper::dtls_session_addr(ptr, lenptr)
|
26
28
|
|
27
|
-
|
28
|
-
return Session.new(addrinfo)
|
29
|
+
Addrinfo.new(sockaddr.read_string(lenptr[:value]))
|
29
30
|
end
|
30
31
|
|
31
32
|
# Converts the object into a C pointer to a `session_t` tinydtls
|
@@ -35,13 +36,18 @@ module TinyDTLS
|
|
35
36
|
@session
|
36
37
|
end
|
37
38
|
|
38
|
-
# Frees all resources associated with the underlying `session_t
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
# Frees all resources associated with the underlying `session_t`.
|
40
|
+
# Optionally it also resets all peer connections associated with the
|
41
|
+
# session (if any). In order to do so a TinyDTLS::Context needs to
|
42
|
+
# be passed.
|
43
|
+
def close(ctx = nil)
|
44
|
+
unless ctx.nil?
|
45
|
+
peer = Wrapper::dtls_get_peer(ctx.to_ffi, @session)
|
46
|
+
Wrapper::dtls_reset_peer(ctx.to_ffi, peer) unless peer.null?
|
44
47
|
end
|
48
|
+
|
49
|
+
Wrapper::dtls_free_session(@session)
|
50
|
+
@session = nil
|
45
51
|
end
|
46
52
|
end
|
47
53
|
end
|
@@ -12,16 +12,24 @@ module TinyDTLS
|
|
12
12
|
# Default timeout for the cleanup thread in seconds.
|
13
13
|
DEFAULT_TIMEOUT = (5 * 60).freeze
|
14
14
|
|
15
|
-
|
15
|
+
# Timeout used by the cleanup thread. If a session hasn't been used
|
16
|
+
# within `timeout * 2` seconds it will be freed automatically.
|
17
|
+
attr_reader :timeout
|
16
18
|
|
17
19
|
# Creates a new instance of this class. A tinydtls `context_t`
|
18
20
|
# pointer is required to free sessions in the background thread.
|
19
|
-
|
21
|
+
#
|
22
|
+
# Memory for sessions created using #[] needs to be explicitly freed
|
23
|
+
# by calling #close as soons as this class instance is no longer
|
24
|
+
# needed.
|
25
|
+
def initialize(context, timeout = DEFAULT_TIMEOUT)
|
20
26
|
@store = {}
|
21
27
|
@mutex = Mutex.new
|
28
|
+
|
22
29
|
@timeout = timeout
|
30
|
+
@context = context
|
23
31
|
|
24
|
-
start_thread
|
32
|
+
start_thread
|
25
33
|
end
|
26
34
|
|
27
35
|
# Retrieve a session from the session manager.
|
@@ -31,21 +39,28 @@ module TinyDTLS
|
|
31
39
|
end
|
32
40
|
|
33
41
|
key = addrinfo.getnameinfo
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
42
|
+
@mutex.synchronize do
|
43
|
+
if @store.has_key? key
|
44
|
+
sess, _ = @store[key]
|
45
|
+
else
|
46
|
+
sess = Session.new(addrinfo)
|
47
|
+
@store[key] = [sess, true]
|
48
|
+
end
|
40
49
|
|
41
|
-
|
50
|
+
f.call(sess)
|
51
|
+
end
|
42
52
|
end
|
43
53
|
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
@
|
48
|
-
|
54
|
+
# Kills the background thread. All established sessions are closed
|
55
|
+
# as well, see Session#close.
|
56
|
+
def close
|
57
|
+
@mutex.synchronize do
|
58
|
+
@thread.kill
|
59
|
+
@store.each_value do |value|
|
60
|
+
sess, _ = value
|
61
|
+
sess.close(@context)
|
62
|
+
end
|
63
|
+
end
|
49
64
|
end
|
50
65
|
|
51
66
|
private
|
@@ -55,13 +70,9 @@ module TinyDTLS
|
|
55
70
|
# as described in Modern Operating Systems, p. 212.
|
56
71
|
#
|
57
72
|
# The thread is only created once.
|
58
|
-
def start_thread
|
73
|
+
def start_thread
|
59
74
|
@thread ||= Thread.new do
|
60
|
-
|
61
|
-
# XXX: How does concurrent access to variables work in ruby?
|
62
|
-
# as known as: Is this a concurrency problems since the value
|
63
|
-
# of @timeout might be changed by a different thread since an
|
64
|
-
# attr_accessor for it is declared.
|
75
|
+
loop do
|
65
76
|
sleep @timeout
|
66
77
|
|
67
78
|
@mutex.lock
|
@@ -70,7 +81,7 @@ module TinyDTLS
|
|
70
81
|
if used
|
71
82
|
[sess, !used]
|
72
83
|
else # Not used since we've been here last time → free resources
|
73
|
-
sess.
|
84
|
+
sess.close(@context)
|
74
85
|
nil
|
75
86
|
end
|
76
87
|
end
|
data/lib/tinydtls/udpsocket.rb
CHANGED
@@ -8,8 +8,11 @@ module TinyDTLS
|
|
8
8
|
#
|
9
9
|
# Basic send and receive methods are implemented and should work.
|
10
10
|
class UDPSocket < ::UDPSocket
|
11
|
+
# Maximum of times a dtls_send is retried.
|
12
|
+
MAX_RETRY = 5.freeze
|
13
|
+
|
11
14
|
Write = Proc.new do |ctx, sess, buf, len|
|
12
|
-
addrinfo = Session.
|
15
|
+
addrinfo = Session.addr_from_ptr(sess)
|
13
16
|
|
14
17
|
ctxobj = TinyDTLS::Context.from_ptr(ctx)
|
15
18
|
ctxobj.sendfn.call(buf.read_string(len),
|
@@ -18,14 +21,15 @@ module TinyDTLS
|
|
18
21
|
end
|
19
22
|
|
20
23
|
Read = Proc.new do |ctx, sess, buf, len|
|
21
|
-
addrinfo = Session.
|
24
|
+
addrinfo = Session.addr_from_ptr(sess)
|
22
25
|
|
23
26
|
# We need to perform a reverse lookup here because
|
24
27
|
# the #recvfrom function needs to return the DNS
|
25
28
|
# hostname.
|
26
29
|
sender = Socket.getaddrinfo(addrinfo.ip_address,
|
27
|
-
addrinfo.ip_port,
|
28
|
-
|
30
|
+
addrinfo.ip_port,
|
31
|
+
addrinfo.afamily,
|
32
|
+
:DGRAM, 0, 0, true).first
|
29
33
|
|
30
34
|
ctxobj = TinyDTLS::Context.from_ptr(ctx)
|
31
35
|
ctxobj.queue.push([buf.read_string(len), sender])
|
@@ -46,27 +50,33 @@ module TinyDTLS
|
|
46
50
|
@sendfn = method(:send).super_method
|
47
51
|
@secconf = SecurityConfig.new
|
48
52
|
|
49
|
-
|
50
|
-
CONTEXT_MAP[
|
51
|
-
|
52
|
-
cptr = Wrapper::dtls_new_context(FFI::Pointer.new(id))
|
53
|
-
@ctx = Wrapper::DTLSContextStruct.new(cptr)
|
53
|
+
@context = TinyDTLS::Context.new(@sendfn, @queue, @secconf)
|
54
|
+
CONTEXT_MAP[@context.key] = @context
|
54
55
|
|
55
56
|
if timeout.nil?
|
56
|
-
@sessions = SessionManager.new(@
|
57
|
+
@sessions = SessionManager.new(@context)
|
57
58
|
else
|
58
|
-
@sessions = SessionManager.new(@
|
59
|
+
@sessions = SessionManager.new(@context, timeout)
|
59
60
|
end
|
60
61
|
|
61
62
|
@handler = Wrapper::DTLSHandlerStruct.new
|
62
63
|
@handler[:write] = UDPSocket::Write
|
63
64
|
@handler[:read] = UDPSocket::Read
|
64
65
|
@handler[:get_psk_info] = SecurityConfig::GetPSKInfo
|
65
|
-
Wrapper::dtls_set_handler(@
|
66
|
+
Wrapper::dtls_set_handler(@context.to_ffi, @handler)
|
66
67
|
end
|
67
68
|
|
68
|
-
|
69
|
+
# Adds a new identity/key pair to the underlying
|
70
|
+
# TinyDTLS::SessionManager. By default the first pair added to the
|
71
|
+
# store will be used for establishing new handshakes, this behaviour
|
72
|
+
# can be changed using the optional default argument.
|
73
|
+
def add_client(id, key, default = false)
|
69
74
|
@secconf.add_client(id, key)
|
75
|
+
|
76
|
+
if default
|
77
|
+
@secconf.default_id = id
|
78
|
+
@secconf.default_key = key
|
79
|
+
end
|
70
80
|
end
|
71
81
|
|
72
82
|
def bind(host, port)
|
@@ -77,18 +87,28 @@ module TinyDTLS
|
|
77
87
|
# TODO: close_{read,write}
|
78
88
|
|
79
89
|
def close
|
80
|
-
|
81
|
-
|
90
|
+
# This method can't be called twice since we actually free memory
|
91
|
+
# allocated by ruby-ffi in this function. Since we can't free it
|
92
|
+
# twice we ensure that this function is only called once.
|
93
|
+
return unless CONTEXT_MAP.has_key? @context.key
|
94
|
+
|
95
|
+
@sessions.close
|
96
|
+
unless @thread.nil?
|
97
|
+
@thread.kill
|
98
|
+
while @thread.alive?
|
99
|
+
@thread.join
|
100
|
+
end
|
101
|
+
end
|
82
102
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
Wrapper::dtls_free_context(@
|
103
|
+
# dtls_free_context sends messages to peers so we need to
|
104
|
+
# explicitly free the dtls_context_t before closing the socket.
|
105
|
+
Wrapper::dtls_free_context(@context.to_ffi)
|
86
106
|
super
|
87
107
|
|
88
108
|
# Assuming the @thread is already stopped at this point
|
89
109
|
# we can safely access the CONTEXT_MAP without running
|
90
110
|
# into any kind of concurrency problems.
|
91
|
-
CONTEXT_MAP.delete(
|
111
|
+
CONTEXT_MAP.delete(@context.key)
|
92
112
|
end
|
93
113
|
|
94
114
|
def connect(host, port)
|
@@ -149,20 +169,25 @@ module TinyDTLS
|
|
149
169
|
port, host = Socket.unpack_sockaddr_in(host)
|
150
170
|
end
|
151
171
|
|
152
|
-
addr = Addrinfo.getaddrinfo(host, port,
|
172
|
+
addr = Addrinfo.getaddrinfo(host, port, @family, :DGRAM).first
|
153
173
|
|
154
174
|
# If a new thread has been started above a new handshake needs to
|
155
175
|
# be performed by it. We need to block here until the handshake
|
156
176
|
# was completed.
|
157
177
|
#
|
158
|
-
# The current approach is calling `Wrapper::dtls_write`
|
159
|
-
#
|
160
|
-
#
|
161
|
-
|
178
|
+
# The current approach is calling `Wrapper::dtls_write` up to
|
179
|
+
# MAX_RETRY times. If we didn't manage to send our data to the
|
180
|
+
# peer after MAX_RETRY times an exception is raised.
|
181
|
+
MAX_RETRY.times do
|
182
|
+
res = dtls_send(addr, mesg)
|
183
|
+
if res > 0
|
184
|
+
return res
|
185
|
+
end
|
186
|
+
|
162
187
|
sleep 1
|
163
188
|
end
|
164
189
|
|
165
|
-
|
190
|
+
raise Errno::ECONNREFUSED.new("DTLS handshake failed")
|
166
191
|
end
|
167
192
|
|
168
193
|
private
|
@@ -180,7 +205,8 @@ module TinyDTLS
|
|
180
205
|
# of locking the session manager and is thus thread-safe.
|
181
206
|
def dtls_send(addr, mesg)
|
182
207
|
@sessions[addr] do |sess|
|
183
|
-
res = Wrapper::dtls_write(@
|
208
|
+
res = Wrapper::dtls_write(@context.to_ffi, sess.to_ptr,
|
209
|
+
mesg, mesg.bytesize)
|
184
210
|
res == -1 ? raise(Errno::EIO) : res
|
185
211
|
end
|
186
212
|
end
|
@@ -191,13 +217,14 @@ module TinyDTLS
|
|
191
217
|
# The thread is only created once.
|
192
218
|
def start_thread
|
193
219
|
@thread ||= Thread.new do
|
194
|
-
|
220
|
+
loop do
|
195
221
|
data, addr = method(:recvfrom).super_method
|
196
222
|
.call(Wrapper::DTLS_MAX_BUF)
|
197
223
|
addrinfo = to_addrinfo(*addr)
|
198
224
|
|
199
225
|
@sessions[addrinfo] do |sess|
|
200
|
-
Wrapper::dtls_handle_message(@
|
226
|
+
Wrapper::dtls_handle_message(@context.to_ffi, sess.to_ptr,
|
227
|
+
data, data.bytesize)
|
201
228
|
end
|
202
229
|
end
|
203
230
|
end
|
data/lib/tinydtls/wrapper.rb
CHANGED
@@ -112,6 +112,7 @@ module TinyDTLS
|
|
112
112
|
# sockaddr_in{,6}`.
|
113
113
|
attach_function :dtls_new_session,
|
114
114
|
[:pointer, :socklen_t], :pointer
|
115
|
+
attach_function :dtls_free_session, [:pointer], :void
|
115
116
|
attach_function :dtls_session_addr, [:pointer, SocklenPtr], :pointer
|
116
117
|
|
117
118
|
def self.dtls_get_app_data(ctx)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tinydtls
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sören Tempel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -72,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
72
|
version: '0'
|
73
73
|
requirements: []
|
74
74
|
rubyforge_project:
|
75
|
-
rubygems_version: 2.7.
|
75
|
+
rubygems_version: 2.7.6
|
76
76
|
signing_key:
|
77
77
|
specification_version: 4
|
78
78
|
summary: It wraps the tinydtls library
|