tttls1.3 0.2.9 → 0.2.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +32 -0
- data/.rubocop.yml +9 -2
- data/Gemfile +1 -1
- data/README.md +5 -1
- data/Rakefile +66 -7
- data/example/helper.rb +6 -8
- data/example/https_client.rb +1 -1
- data/example/https_client_using_0rtt.rb +3 -3
- data/example/https_client_using_hrr.rb +1 -1
- data/example/https_client_using_hrr_and_ticket.rb +2 -2
- data/example/https_client_using_status_request.rb +31 -0
- data/example/https_client_using_ticket.rb +2 -2
- data/example/https_server.rb +6 -5
- data/interop/client_spec.rb +8 -8
- data/interop/helper.rb +10 -2
- data/interop/server_spec.rb +14 -10
- data/lib/tttls1.3.rb +1 -0
- data/lib/tttls1.3/client.rb +97 -12
- data/lib/tttls1.3/connection.rb +45 -12
- data/lib/tttls1.3/cryptograph.rb +1 -1
- data/lib/tttls1.3/cryptograph/aead.rb +20 -7
- data/lib/tttls1.3/message.rb +1 -1
- data/lib/tttls1.3/message/alert.rb +2 -2
- data/lib/tttls1.3/message/extension/status_request.rb +73 -17
- data/lib/tttls1.3/message/extensions.rb +35 -12
- data/lib/tttls1.3/server.rb +40 -13
- data/lib/tttls1.3/utils.rb +15 -0
- data/lib/tttls1.3/version.rb +1 -1
- data/spec/extensions_spec.rb +16 -0
- data/spec/fixtures/rsa_rsa.crt +15 -15
- data/spec/fixtures/rsa_rsa.key +25 -25
- data/spec/fixtures/rsa_rsa_ocsp.crt +18 -0
- data/spec/fixtures/rsa_rsa_ocsp.key +27 -0
- data/spec/server_hello_spec.rb +1 -1
- data/spec/spec_helper.rb +35 -1
- data/spec/status_request_spec.rb +77 -10
- data/tttls1.3.gemspec +1 -1
- metadata +14 -10
- data/.travis.yml +0 -18
- data/interop/Dockerfile +0 -28
data/interop/helper.rb
CHANGED
@@ -12,6 +12,14 @@ include TTTLS13::Message::Extension
|
|
12
12
|
include TTTLS13::Error
|
13
13
|
# rubocop: enable Style/MixinUsage
|
14
14
|
|
15
|
-
def wait_to_listen(port)
|
16
|
-
|
15
|
+
def wait_to_listen(host, port)
|
16
|
+
loop do
|
17
|
+
s = TCPSocket.open(host, port) # check by TCP handshake
|
18
|
+
rescue # rubocop: disable Style/RescueStandardError
|
19
|
+
sleep(0.2)
|
20
|
+
next
|
21
|
+
else
|
22
|
+
s.close
|
23
|
+
break
|
24
|
+
end
|
17
25
|
end
|
data/interop/server_spec.rb
CHANGED
@@ -9,14 +9,13 @@ PORT = 4433
|
|
9
9
|
tcpserver = TCPServer.open(PORT)
|
10
10
|
|
11
11
|
RSpec.describe Server do
|
12
|
-
# testcases
|
13
12
|
# normal [Boolean] Is this nominal scenarios?
|
14
13
|
# opt [String] openssl s_client options
|
15
14
|
# crt [String] server crt file path
|
16
15
|
# key [String] server key file path
|
17
16
|
# settings [Hash] TTTLS13::Client settins
|
18
|
-
|
19
|
-
|
17
|
+
# rubocop: disable Layout/LineLength
|
18
|
+
testcases = [
|
20
19
|
[
|
21
20
|
true,
|
22
21
|
'-groups P-256:P-384:P-521 -ciphersuites TLS_AES_256_GCM_SHA384',
|
@@ -172,20 +171,24 @@ RSpec.describe Server do
|
|
172
171
|
FIXTURES_DIR + '/rsa_rsa.key',
|
173
172
|
compatibility_mode: false
|
174
173
|
]
|
175
|
-
|
176
|
-
|
174
|
+
]
|
175
|
+
# rubocop: enable Layout/LineLength
|
176
|
+
testcases.each do |normal, opt, crt, key, settings|
|
177
177
|
context 'server interop' do
|
178
178
|
let(:server) do
|
179
|
-
|
179
|
+
loop do
|
180
|
+
@socket = tcpserver.accept
|
181
|
+
break unless @socket.eof?
|
182
|
+
end
|
180
183
|
settings[:crt_file] = crt
|
181
184
|
settings[:key_file] = key
|
182
|
-
Server.new(@socket, settings)
|
185
|
+
Server.new(@socket, **settings)
|
183
186
|
end
|
184
187
|
|
185
188
|
let(:client) do
|
186
|
-
wait_to_listen(PORT)
|
187
|
-
|
188
189
|
ip = Socket.ip_address_list.find(&:ipv4_private?).ip_address
|
190
|
+
wait_to_listen(ip, PORT)
|
191
|
+
|
189
192
|
cmd = 'echo -n ping | openssl s_client ' \
|
190
193
|
+ "-connect local:#{PORT} " \
|
191
194
|
+ '-tls1_3 ' \
|
@@ -195,7 +198,7 @@ RSpec.describe Server do
|
|
195
198
|
+ opt
|
196
199
|
'docker run ' \
|
197
200
|
+ "--volume #{FIXTURES_DIR}:/tmp " \
|
198
|
-
+ "--add-host=local:#{ip}
|
201
|
+
+ "--add-host=local:#{ip} thekuwayama/openssl " \
|
199
202
|
+ "sh -c \"#{cmd}\" 2>&1 >/dev/null"
|
200
203
|
end
|
201
204
|
|
@@ -216,6 +219,7 @@ RSpec.describe Server do
|
|
216
219
|
it "should NOT accept request from openssl s_client ...#{opt}" do
|
217
220
|
spawn(client)
|
218
221
|
expect { server.accept }.to raise_error ErrorAlerts
|
222
|
+
expect { server.close }.to_not raise_error
|
219
223
|
end
|
220
224
|
end
|
221
225
|
end
|
data/lib/tttls1.3.rb
CHANGED
data/lib/tttls1.3/client.rb
CHANGED
@@ -59,6 +59,8 @@ module TTTLS13
|
|
59
59
|
ticket_age_add: nil,
|
60
60
|
ticket_timestamp: nil,
|
61
61
|
record_size_limit: nil,
|
62
|
+
check_certificate_status: false,
|
63
|
+
process_certificate_status: nil,
|
62
64
|
compatibility_mode: true,
|
63
65
|
loglevel: Logger::WARN
|
64
66
|
}.freeze
|
@@ -300,7 +302,8 @@ module TTTLS13
|
|
300
302
|
message = recv_message(receivable_ccs: true, cipher: hs_rcipher)
|
301
303
|
if message.msg_type == Message::HandshakeType::CERTIFICATE
|
302
304
|
ct = transcript[CT] = message
|
303
|
-
|
305
|
+
alert = check_invalid_certificate(ct, transcript[CH])
|
306
|
+
terminate(alert) unless alert.nil?
|
304
307
|
|
305
308
|
@state = ClientState::WAIT_CV
|
306
309
|
elsif message.msg_type == Message::HandshakeType::CERTIFICATE_REQUEST
|
@@ -314,7 +317,8 @@ module TTTLS13
|
|
314
317
|
logger.debug('ClientState::WAIT_CERT')
|
315
318
|
|
316
319
|
ct = transcript[CT] = recv_certificate(hs_rcipher)
|
317
|
-
|
320
|
+
alert = check_invalid_certificate(ct, transcript[CH])
|
321
|
+
terminate(alert) unless alert.nil?
|
318
322
|
|
319
323
|
@state = ClientState::WAIT_CV
|
320
324
|
when ClientState::WAIT_CV
|
@@ -392,6 +396,53 @@ module TTTLS13
|
|
392
396
|
@succeed_early_data
|
393
397
|
end
|
394
398
|
|
399
|
+
# @param res [OpenSSL::OCSP::Response]
|
400
|
+
# @param cert [OpenSSL::X509::Certificate]
|
401
|
+
# @param chain [Array of OpenSSL::X509::Certificate, nil]
|
402
|
+
#
|
403
|
+
# @return [Boolean]
|
404
|
+
#
|
405
|
+
# @example
|
406
|
+
# m = Client.method(:softfail_check_certificate_status)
|
407
|
+
# Client.new(
|
408
|
+
# socket,
|
409
|
+
# hostname,
|
410
|
+
# check_certificate_status: true,
|
411
|
+
# process_certificate_status: m
|
412
|
+
# )
|
413
|
+
def self.softfail_check_certificate_status(res, cert, chain)
|
414
|
+
ocsp_response = res
|
415
|
+
cid = OpenSSL::OCSP::CertificateId.new(cert, chain.first)
|
416
|
+
|
417
|
+
# When NOT received OCSPResponse in TLS handshake, this method will
|
418
|
+
# send OCSPRequest. If ocsp_uri is NOT presented in Certificate, return
|
419
|
+
# true. Also, if it sends OCSPRequest and does NOT receive a HTTPresponse
|
420
|
+
# within 2 seconds, return true.
|
421
|
+
if ocsp_response.nil?
|
422
|
+
uri = cert.ocsp_uris&.find { |u| URI::DEFAULT_PARSER.make_regexp =~ u }
|
423
|
+
return true if uri.nil?
|
424
|
+
|
425
|
+
begin
|
426
|
+
# send OCSP::Request
|
427
|
+
ocsp_request = gen_ocsp_request(cid)
|
428
|
+
Timeout.timeout(2) do
|
429
|
+
ocsp_response = send_ocsp_request(ocsp_request, uri)
|
430
|
+
end
|
431
|
+
|
432
|
+
# check nonce of OCSP::Response
|
433
|
+
check_nonce = ocsp_request.check_nonce(ocsp_response.basic)
|
434
|
+
return true unless [-1, 1].include?(check_nonce)
|
435
|
+
rescue StandardError
|
436
|
+
return true
|
437
|
+
end
|
438
|
+
end
|
439
|
+
return true \
|
440
|
+
if ocsp_response.status != OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
|
441
|
+
|
442
|
+
status = ocsp_response.basic.status.find { |s| s.first.cmp(cid) }
|
443
|
+
status[1] != OpenSSL::OCSP::V_CERTSTATUS_REVOKED
|
444
|
+
end
|
445
|
+
|
395
446
|
private
|
396
447
|
|
397
448
|
# @return [Boolean]
|
@@ -423,6 +474,9 @@ module TTTLS13
|
|
423
474
|
rsl = @settings[:record_size_limit]
|
424
475
|
return false if !rsl.nil? && (rsl < 64 || rsl > 2**14 + 1)
|
425
476
|
|
477
|
+
return false if @settings[:check_certificate_status] &&
|
478
|
+
@settings[:process_certificate_status].nil?
|
479
|
+
|
426
480
|
true
|
427
481
|
end
|
428
482
|
# rubocop: enable Metrics/AbcSize
|
@@ -471,9 +525,10 @@ module TTTLS13
|
|
471
525
|
# @return [Hash of NamedGroup => OpenSSL::PKey::EC.$Object]
|
472
526
|
# rubocop: disable Metrics/AbcSize
|
473
527
|
# rubocop: disable Metrics/CyclomaticComplexity
|
528
|
+
# rubocop: disable Metrics/MethodLength
|
474
529
|
# rubocop: disable Metrics/PerceivedComplexity
|
475
530
|
def gen_ch_extensions
|
476
|
-
exs =
|
531
|
+
exs = Message::Extensions.new
|
477
532
|
# server_name
|
478
533
|
exs << Message::Extension::ServerName.new(@hostname)
|
479
534
|
|
@@ -519,10 +574,15 @@ module TTTLS13
|
|
519
574
|
exs << Message::Extension::Alpn.new(@settings[:alpn].reject(&:empty?)) \
|
520
575
|
if !@settings[:alpn].nil? && !@settings[:alpn].empty?
|
521
576
|
|
522
|
-
|
577
|
+
# status_request
|
578
|
+
exs << Message::Extension::OCSPStatusRequest.new \
|
579
|
+
if @settings[:check_certificate_status]
|
580
|
+
|
581
|
+
[exs, priv_keys]
|
523
582
|
end
|
524
583
|
# rubocop: enable Metrics/AbcSize
|
525
584
|
# rubocop: enable Metrics/CyclomaticComplexity
|
585
|
+
# rubocop: enable Metrics/MethodLength
|
526
586
|
# rubocop: enable Metrics/PerceivedComplexity
|
527
587
|
|
528
588
|
# @param extensions [TTTLS13::Message::Extensions]
|
@@ -611,7 +671,7 @@ module TTTLS13
|
|
611
671
|
# @return [TTTLS13::Message::Extensions]
|
612
672
|
# @return [Hash of NamedGroup => OpenSSL::PKey::EC.$Object]
|
613
673
|
def gen_newch_extensions(ch1, hrr)
|
614
|
-
exs =
|
674
|
+
exs = Message::Extensions.new
|
615
675
|
# key_share
|
616
676
|
if hrr.extensions.include?(Message::ExtensionType::KEY_SHARE)
|
617
677
|
group = hrr.extensions[Message::ExtensionType::KEY_SHARE]
|
@@ -633,7 +693,7 @@ module TTTLS13
|
|
633
693
|
if hrr.extensions.include?(Message::ExtensionType::COOKIE)
|
634
694
|
|
635
695
|
# early_data
|
636
|
-
new_exs = ch1.extensions.merge(
|
696
|
+
new_exs = ch1.extensions.merge(exs)
|
637
697
|
new_exs.delete(Message::ExtensionType::EARLY_DATA)
|
638
698
|
|
639
699
|
[new_exs, priv_keys]
|
@@ -753,16 +813,32 @@ module TTTLS13
|
|
753
813
|
|
754
814
|
# @param ct [TTTLS13::Message::Certificate]
|
755
815
|
# @param ch [TTTLS13::Message::ClientHello]
|
756
|
-
|
757
|
-
|
816
|
+
#
|
817
|
+
# @return [Symbol, nil] return key of ALERT_DESCRIPTION, if invalid
|
818
|
+
def check_invalid_certificate(ct, ch)
|
819
|
+
return :illegal_parameter unless ct.appearable_extensions?
|
758
820
|
|
759
|
-
|
821
|
+
return :unsupported_extension \
|
760
822
|
unless ct.certificate_list.map(&:extensions)
|
761
823
|
.all? { |e| (e.keys - ch.extensions.keys).empty? }
|
762
824
|
|
763
|
-
|
764
|
-
|
765
|
-
|
825
|
+
return :certificate_unknown unless trusted_certificate?(
|
826
|
+
ct.certificate_list,
|
827
|
+
@settings[:ca_file],
|
828
|
+
@hostname
|
829
|
+
)
|
830
|
+
|
831
|
+
if @settings[:check_certificate_status]
|
832
|
+
ee = ct.certificate_list.first
|
833
|
+
ocsp_response = ee.extensions[Message::ExtensionType::STATUS_REQUEST]
|
834
|
+
&.ocsp_response
|
835
|
+
cert = ee.cert_data
|
836
|
+
chain = ct.certificate_list[1..]&.map(&:cert_data)
|
837
|
+
return :bad_certificate_status_response \
|
838
|
+
unless satisfactory_certificate_status?(ocsp_response, cert, chain)
|
839
|
+
end
|
840
|
+
|
841
|
+
nil
|
766
842
|
end
|
767
843
|
|
768
844
|
# @param ct [TTTLS13::Message::Certificate]
|
@@ -784,6 +860,15 @@ module TTTLS13
|
|
784
860
|
)
|
785
861
|
end
|
786
862
|
|
863
|
+
# @param ocsp_response [OpenSSL::OCSP::Response]
|
864
|
+
# @param cert [OpenSSL::X509::Certificate]
|
865
|
+
# @param chain [Array of OpenSSL::X509::Certificate, nil]
|
866
|
+
#
|
867
|
+
# @return [Boolean]
|
868
|
+
def satisfactory_certificate_status?(ocsp_response, cert, chain)
|
869
|
+
@settings[:process_certificate_status]&.call(ocsp_response, cert, chain)
|
870
|
+
end
|
871
|
+
|
787
872
|
# @param nst [TTTLS13::Message::NewSessionTicket]
|
788
873
|
#
|
789
874
|
# @raise [TTTLS13::Error::ErrorAlerts]
|
data/lib/tttls1.3/connection.rb
CHANGED
@@ -254,7 +254,7 @@ module TTTLS13
|
|
254
254
|
end
|
255
255
|
# rubocop: enable Metrics/CyclomaticComplexity
|
256
256
|
|
257
|
-
# @param
|
257
|
+
# @param cipher [TTTLS13::Cryptograph::Aead, Passer]
|
258
258
|
#
|
259
259
|
# @return [TTTLS13::Message::Record]
|
260
260
|
def recv_record(cipher)
|
@@ -273,7 +273,7 @@ module TTTLS13
|
|
273
273
|
|
274
274
|
# Received a protected ccs, peer MUST abort the handshake.
|
275
275
|
if record.type == Message::ContentType::APPLICATION_DATA &&
|
276
|
-
record.messages.
|
276
|
+
record.messages.any? { |m| m.is_a?(Message::ChangeCipherSpec) }
|
277
277
|
terminate(:unexpected_message)
|
278
278
|
end
|
279
279
|
|
@@ -474,8 +474,10 @@ module TTTLS13
|
|
474
474
|
#
|
475
475
|
# @return [Boolean]
|
476
476
|
def trusted_certificate?(certificate_list, ca_file = nil, hostname = nil)
|
477
|
-
|
478
|
-
|
477
|
+
chain = certificate_list.map(&:cert_data).map do |c|
|
478
|
+
OpenSSL::X509::Certificate.new(c)
|
479
|
+
end
|
480
|
+
cert = chain.shift
|
479
481
|
|
480
482
|
# not support CN matching, only support SAN matching
|
481
483
|
return false if !hostname.nil? && !matching_san?(cert, hostname)
|
@@ -483,9 +485,6 @@ module TTTLS13
|
|
483
485
|
store = OpenSSL::X509::Store.new
|
484
486
|
store.set_default_paths
|
485
487
|
store.add_file(ca_file) unless ca_file.nil?
|
486
|
-
chain = certificate_list[1..].map(&:cert_data).map do |c|
|
487
|
-
OpenSSL::X509::Certificate.new(c)
|
488
|
-
end
|
489
488
|
# TODO: parse authorityInfoAccess::CA Issuers
|
490
489
|
ctx = OpenSSL::X509::StoreContext.new(store, cert, chain)
|
491
490
|
now = Time.now
|
@@ -515,22 +514,22 @@ module TTTLS13
|
|
515
514
|
def do_select_signature_algorithms(signature_algorithms, crt)
|
516
515
|
spki = OpenSSL::Netscape::SPKI.new
|
517
516
|
spki.public_key = crt.public_key
|
518
|
-
|
519
|
-
|
517
|
+
pka = OpenSSL::ASN1.decode(spki.to_der)
|
518
|
+
.value.first.value.first.value.first.value.first.value
|
520
519
|
signature_algorithms.select do |sa|
|
521
520
|
case sa
|
522
521
|
when SignatureScheme::ECDSA_SECP256R1_SHA256,
|
523
522
|
SignatureScheme::ECDSA_SECP384R1_SHA384,
|
524
523
|
SignatureScheme::ECDSA_SECP521R1_SHA512
|
525
|
-
|
524
|
+
pka == 'id-ecPublicKey'
|
526
525
|
when SignatureScheme::RSA_PSS_PSS_SHA256,
|
527
526
|
SignatureScheme::RSA_PSS_PSS_SHA384,
|
528
527
|
SignatureScheme::RSA_PSS_PSS_SHA512
|
529
|
-
|
528
|
+
pka == 'rsassaPss'
|
530
529
|
when SignatureScheme::RSA_PSS_RSAE_SHA256,
|
531
530
|
SignatureScheme::RSA_PSS_RSAE_SHA384,
|
532
531
|
SignatureScheme::RSA_PSS_RSAE_SHA512
|
533
|
-
|
532
|
+
pka == 'rsaEncryption'
|
534
533
|
else
|
535
534
|
# RSASSA-PKCS1-v1_5 algorithms refer solely to signatures which appear
|
536
535
|
# in certificates and are not defined for use in signed TLS handshake
|
@@ -539,6 +538,40 @@ module TTTLS13
|
|
539
538
|
end
|
540
539
|
end
|
541
540
|
end
|
541
|
+
|
542
|
+
class << self
|
543
|
+
# @param cid [OpenSSL::OCSP::CertificateId]
|
544
|
+
#
|
545
|
+
# @return [OpenSSL::OCSP::Request]
|
546
|
+
def gen_ocsp_request(cid)
|
547
|
+
ocsp_request = OpenSSL::OCSP::Request.new
|
548
|
+
ocsp_request.add_certid(cid)
|
549
|
+
ocsp_request.add_nonce
|
550
|
+
ocsp_request
|
551
|
+
end
|
552
|
+
|
553
|
+
# @param ocsp_request [OpenSSL::OCSP::Request]
|
554
|
+
# @param uri_string [String]
|
555
|
+
#
|
556
|
+
# @raise [Net::OpenTimeout, OpenSSL::OCSP::OCSPError, URI::$Exception]
|
557
|
+
#
|
558
|
+
# @return [OpenSSL::OCSP::Response, n
|
559
|
+
def send_ocsp_request(ocsp_request, uri_string)
|
560
|
+
# send HTTP POST
|
561
|
+
uri = URI.parse(uri_string)
|
562
|
+
path = uri.path
|
563
|
+
path = '/' if path.nil? || path.empty?
|
564
|
+
http_response = Net::HTTP.start(uri.host, uri.port) do |http|
|
565
|
+
http.post(
|
566
|
+
path,
|
567
|
+
ocsp_request.to_der,
|
568
|
+
'content-type' => 'application/ocsp-request'
|
569
|
+
)
|
570
|
+
end
|
571
|
+
|
572
|
+
OpenSSL::OCSP::Response.new(http_response.body)
|
573
|
+
end
|
574
|
+
end
|
542
575
|
end
|
543
576
|
# rubocop: enable Metrics/ClassLength
|
544
577
|
end
|
data/lib/tttls1.3/cryptograph.rb
CHANGED
@@ -44,8 +44,7 @@ module TTTLS13
|
|
44
44
|
#
|
45
45
|
# @return [String]
|
46
46
|
def encrypt(content, type)
|
47
|
-
reset_cipher
|
48
|
-
cipher = @cipher.encrypt
|
47
|
+
cipher = reset_cipher
|
49
48
|
plaintext = content + type + "\x00" * @length_of_padding
|
50
49
|
cipher.auth_data = additional_data(plaintext.length)
|
51
50
|
encrypted_data = cipher.update(plaintext) + cipher.final
|
@@ -66,8 +65,7 @@ module TTTLS13
|
|
66
65
|
# @return [String]
|
67
66
|
# @return [TTTLS13::Message::ContentType]
|
68
67
|
def decrypt(encrypted_record, auth_data)
|
69
|
-
|
70
|
-
decipher = @cipher.decrypt
|
68
|
+
decipher = reset_decipher
|
71
69
|
auth_tag = encrypted_record[-@auth_tag_len..-1]
|
72
70
|
decipher.auth_tag = auth_tag
|
73
71
|
decipher.auth_data = auth_data # record header of TLSCiphertext
|
@@ -105,11 +103,26 @@ module TTTLS13
|
|
105
103
|
+ ciphertext_len.to_uint16
|
106
104
|
end
|
107
105
|
|
106
|
+
# @return [OpenSSL::Cipher]
|
108
107
|
def reset_cipher
|
109
|
-
@cipher.
|
110
|
-
|
108
|
+
cipher = @cipher.encrypt
|
109
|
+
cipher.reset
|
110
|
+
cipher.key = @write_key
|
111
111
|
iv_len = CipherSuite.iv_len(@cipher_suite)
|
112
|
-
|
112
|
+
cipher.iv = @sequence_number.xor(@write_iv, iv_len)
|
113
|
+
|
114
|
+
cipher
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [OpenSSL::Cipher]
|
118
|
+
def reset_decipher
|
119
|
+
decipher = @cipher.decrypt
|
120
|
+
decipher.reset
|
121
|
+
decipher.key = @write_key
|
122
|
+
iv_len = CipherSuite.iv_len(@cipher_suite)
|
123
|
+
decipher.iv = @sequence_number.xor(@write_iv, iv_len)
|
124
|
+
|
125
|
+
decipher
|
113
126
|
end
|
114
127
|
|
115
128
|
# @param clear [String]
|
data/lib/tttls1.3/message.rb
CHANGED