simple_nts_client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rubocop.yml +24 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +14 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +57 -0
  8. data/Rakefile +10 -0
  9. data/exe/simple_nts_client +8 -0
  10. data/lib/nts/cli.rb +83 -0
  11. data/lib/nts/ntske/client.rb +100 -0
  12. data/lib/nts/ntske/message/aead_algorithm_negotiation.rb +47 -0
  13. data/lib/nts/ntske/message/cookie.rb +33 -0
  14. data/lib/nts/ntske/message/end_of_message.rb +30 -0
  15. data/lib/nts/ntske/message/error_record.rb +39 -0
  16. data/lib/nts/ntske/message/nts_next_protocol_negotiation.rb +35 -0
  17. data/lib/nts/ntske/message/ntsv4_port_negotiation.rb +36 -0
  18. data/lib/nts/ntske/message/ntsv4_server_negotiation.rb +36 -0
  19. data/lib/nts/ntske/message/warning_record.rb +34 -0
  20. data/lib/nts/ntske/message.rb +107 -0
  21. data/lib/nts/ntske.rb +4 -0
  22. data/lib/nts/sntp/client.rb +160 -0
  23. data/lib/nts/sntp/extension/nts_authenticator.rb +53 -0
  24. data/lib/nts/sntp/extension/nts_cookie.rb +34 -0
  25. data/lib/nts/sntp/extension/unique_identifier.rb +36 -0
  26. data/lib/nts/sntp/extension/unknown_extension.rb +36 -0
  27. data/lib/nts/sntp/extension.rb +38 -0
  28. data/lib/nts/sntp/message.rb +119 -0
  29. data/lib/nts/sntp.rb +4 -0
  30. data/lib/nts/version.rb +5 -0
  31. data/lib/nts.rb +12 -0
  32. data/simple_nts_client.gemspec +27 -0
  33. data/spec/aead_algorithm_negotiation_spec.rb +28 -0
  34. data/spec/cookie_spec.rb +36 -0
  35. data/spec/end_of_message_spec.rb +26 -0
  36. data/spec/error_record_spec.rb +28 -0
  37. data/spec/extension_spec.rb +28 -0
  38. data/spec/nts_authenticator_spec.rb +64 -0
  39. data/spec/nts_cookie_spec.rb +36 -0
  40. data/spec/nts_next_protocol_negotiation_spec.rb +28 -0
  41. data/spec/ntsv4_port_negotiation_spec.rb +28 -0
  42. data/spec/ntsv4_server_negotiation_spec.rb +28 -0
  43. data/spec/spec_helper.rb +11 -0
  44. data/spec/unique_identifier_spec.rb +36 -0
  45. data/spec/unknown_extension_spec.rb +46 -0
  46. data/spec/warning_record_spec.rb +28 -0
  47. metadata +145 -0
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nts
4
+ module Ntske
5
+ # https://tools.ietf.org/html/draft-ietf-ntp-using-nts-for-ntp-20#section-7.6
6
+ module RecordType
7
+ END_OF_MESSAGE = 0
8
+ NTS_NEXT_PROTOCOL_NEGOTIATION = 1
9
+ ERROR = 2
10
+ WARNING = 3
11
+ AEAD_ALGORITHM_NEGOTIATION = 4
12
+ NEW_COOKIE_FOR_NTPV4 = 5
13
+ NTPV4_SERVER_NEGOTIATION = 6
14
+ NTPV4_PORT_NEGOTIATION = 7
15
+ end
16
+
17
+ # https://tools.ietf.org/html/draft-ietf-ntp-using-nts-for-ntp-20#section-4
18
+ class Record
19
+ # @param c [Boolean]
20
+ # @param type [Integer] less than 32768(15 bits)
21
+ def initialize(c, type)
22
+ @c = c
23
+ @type = type
24
+ end
25
+
26
+ def serialize
27
+ # super class MUST override serialize_body.
28
+ sb = serialize_body
29
+ [(@c ? 32768 : 0) | @type, sb.length].pack('n2') + sb
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ Dir[File.dirname(__FILE__) + '/message/*.rb'].each { |f| require f }
36
+
37
+ module Nts
38
+ module Ntske
39
+ module_function
40
+
41
+ # @param s [String]
42
+ #
43
+ # @raise [Exception | RuntimeError]
44
+ #
45
+ # @return [Array of Nts::Ntske::$Object]
46
+ # @return [String] surplus binary
47
+ # rubocop: disable Metrics/AbcSize
48
+ # rubocop: disable Metrics/CyclomaticComplexity
49
+ # rubocop: disable Metrics/MethodLength
50
+ # rubocop: disable Metrics/PerceivedComplexity
51
+ def response_deserialize(s)
52
+ res = []
53
+ i = 0
54
+ while i < s.length
55
+ return [res, s[i..]] if i + 4 > s.length
56
+
57
+ c = !(s[i].unpack1('c') | 32768).zero?
58
+ type = s.slice(i, 2).unpack1('n') & 32767
59
+ body_len = s.slice(i + 2, 2).unpack1('n')
60
+ return [res, s[i..]] if i + 4 + body_len > s.length
61
+
62
+ sb = s.slice(i + 4, body_len)
63
+ case type
64
+ when RecordType::END_OF_MESSAGE
65
+ e = 'The Critical Bit contained in End Of Message MUST be set'
66
+ raise e unless c
67
+
68
+ res << EndOfMessage.deserialize(sb)
69
+ when RecordType::NTS_NEXT_PROTOCOL_NEGOTIATION
70
+ e = 'The Critical Bit contained in NTS Next Protocol Negotiation ' \
71
+ 'MUST be set'
72
+ raise e unless c
73
+
74
+ res << NtsNextProtocolNegotiation.deserialize(sb)
75
+ when RecordType::ERROR
76
+ e = 'The Critical Bit contained in Error MUST be set'
77
+ raise e unless c
78
+
79
+ res << ErrorRecord.deserialize(sb)
80
+ when RecordType::WARNING
81
+ e = 'The Critical Bit contained in Warning MUST be set'
82
+ raise e unless c
83
+
84
+ res << WarningRecord.deserialize(sb)
85
+ when RecordType::AEAD_ALGORITHM_NEGOTIATION
86
+ res << AeadAlgorithmNegotiation.deserialize(sb, c)
87
+ when RecordType::NEW_COOKIE_FOR_NTPV4
88
+ res << Cookie.deserialize(sb, c)
89
+ when RecordType::NTPV4_SERVER_NEGOTIATION
90
+ res << Ntsv4ServerNegotiation.deserialize(sb, c)
91
+ when RecordType::NTPV4_PORT_NEGOTIATION
92
+ res << Ntsv4PortNegotiation.deserialize(sb, c)
93
+ else
94
+ raise Exception if c
95
+ end
96
+ i += 4 + body_len
97
+ end
98
+ raise Exception unless i == s.length
99
+
100
+ [res, '']
101
+ end
102
+ # rubocop: enable Metrics/AbcSize
103
+ # rubocop: enable Metrics/CyclomaticComplexity
104
+ # rubocop: enable Metrics/MethodLength
105
+ # rubocop: enable Metrics/PerceivedComplexity
106
+ end
107
+ end
data/lib/nts/ntske.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require __dir__ + '/ntske/message.rb'
4
+ require __dir__ + '/ntske/client.rb'
@@ -0,0 +1,160 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ module Nts
5
+ module Sntp
6
+ class Client
7
+ # @param hostname [String]
8
+ # @param port [Integer]
9
+ # @param cookie [String]
10
+ # @param c2s_key [String]
11
+ # @param s2c_key [String]
12
+ def initialize(hostname, port, cookie, c2s_key, s2c_key)
13
+ @hostname = hostname
14
+ @port = port
15
+ @cookie = cookie
16
+ @c2s_key = c2s_key
17
+ @s2c_key = s2c_key
18
+ end
19
+
20
+ # @return [Time]
21
+ # rubocop: disable Metrics/AbcSize
22
+ # rubocop: disable Metrics/MethodLength
23
+ def what_time
24
+ sock = UDPSocket.new
25
+ localtime = Time.now
26
+ origin_timestamp = t2timestamp(localtime)
27
+
28
+ # make NTS-protected NTP packet
29
+ ntp_header = sntp_request_header(origin_timestamp)
30
+ unique_identifier = Extension::UniqueIdentifier.new
31
+ extensions = [
32
+ unique_identifier,
33
+ Extension::NtsCookie.new(@cookie)
34
+ ]
35
+ nonce = OpenSSL::Random.random_bytes(16)
36
+ cipher = Miscreant::AEAD.new('AES-CMAC-SIV', @c2s_key)
37
+ plaintext = extensions.map(&:serialize).join
38
+ ad = ntp_header + plaintext
39
+ ciphertext = cipher.seal(plaintext, nonce: nonce, ad: ad)
40
+ extensions << Extension::NtsAuthenticator.new(nonce, ciphertext)
41
+
42
+ # send NTS-protected NTP packet
43
+ req = Sntp::Message.new(ntp_header, extensions)
44
+ sock.send(req.serialize, 0, @hostname, @port)
45
+
46
+ # recv NTS-protected NTP packet
47
+ s = nil
48
+ destination_timestamp = nil
49
+ read, = IO.select([sock], nil, nil, 1)
50
+ if read.nil?
51
+ warn 'Timeout: receiving for NTP packet'
52
+ exit 1
53
+ else
54
+ s, = sock.recvfrom(65536)
55
+ destination_timestamp = Time.now
56
+ end
57
+ res = Sntp::Message.deserialize(s)
58
+
59
+ # validate response timestamp
60
+ if res.origin_timestamp != origin_timestamp
61
+ warn 'NTP Response Origin Timestamp != NTP Request Transmit Timestamp'
62
+ exit 1
63
+ end
64
+
65
+ # validate Unique Identifier
66
+ if res.unique_identifier.id != unique_identifier.id
67
+ warn 'NTP Response Unique Identifier != NTP Request Unique Identifier'
68
+ exit 1
69
+ end
70
+
71
+ # validate NTS Authenticator and Encrypted Extension Fields
72
+ decipher = Miscreant::AEAD.new('AES-CMAC-SIV', @s2c_key)
73
+ ciphertext = res.nts_authenticator.ciphertext
74
+ nonce = res.nts_authenticator.nonce
75
+ ad = res.ntp_header + res.extensions.reject { |ex|
76
+ ex.is_a?(Extension::NtsAuthenticator)
77
+ }.map(&:serialize).join
78
+ plaintext = decipher.open(ciphertext, nonce: nonce, ad: ad)
79
+ Message.extensions_deserialize(plaintext)
80
+ # not handle decrypt any NTP Extensions
81
+
82
+ # calculate system clock offset
83
+ offset = offset(
84
+ localtime,
85
+ timestamp2t(res.receive_timestamp),
86
+ timestamp2t(res.transmit_timestamp),
87
+ destination_timestamp
88
+ )
89
+ destination_timestamp + offset
90
+ end
91
+ # rubocop: enable Metrics/AbcSize
92
+ # rubocop: enable Metrics/MethodLength
93
+
94
+ private
95
+
96
+ # https://tools.ietf.org/html/rfc5905#section-14
97
+ # https://tools.ietf.org/html/rfc4330#section-5
98
+ #
99
+ # @param xmt [String] transmit timestamp(8 bytes)
100
+ #
101
+ # @return [String]
102
+ def sntp_request_header(xmt)
103
+ # LI(0b00) | VN(0b100) | Mode(0b011) | Stratum(0x00)
104
+ # Poll(0x00) | Precision(0x00)
105
+ # Root Delay(0x00000000)
106
+ # Root Dispersion(0x00000000)
107
+ # Reference ID(0x00000000)
108
+ # Reference Timestamp(0x0000000000000000)
109
+ # Origin Timestamp(0x0000000000000000)
110
+ # Receive Timestamp(0x0000000000000000)
111
+ # Transmit Timestamp(xmt)
112
+ ['00' + '100' + '011'].pack('B8') + "\x00" * 39 + xmt
113
+ end
114
+
115
+ TIMESTAMP_BASE_DATE = Time.parse('1900-01-01 00:00:00+00:00').to_i
116
+ private_constant :TIMESTAMP_BASE_DATE
117
+
118
+ # https://tools.ietf.org/html/rfc5905#section-6
119
+ #
120
+ # @param t [Time]
121
+ #
122
+ # @return [String] NTP Timestamp Format(8 bytes)
123
+ def t2timestamp(t)
124
+ # In order to minimize bias and help make timestamps unpredictable to
125
+ # an intruder, Fraction should be set to an unbiased random bit string.
126
+ [t.to_i - TIMESTAMP_BASE_DATE].pack('N') \
127
+ + OpenSSL::Random.random_bytes(4)
128
+ end
129
+
130
+ # @param s [String] NTP Timestamp Format(8 bytes)
131
+ #
132
+ # @return [Time]
133
+ def timestamp2t(s)
134
+ Time.at(s[0...4].unpack1('N') + TIMESTAMP_BASE_DATE \
135
+ + s[4..].unpack1('N') / (2.0**32))
136
+ end
137
+
138
+ # @param t1 [Time]
139
+ # @param t2 [Time]
140
+ # @param t3 [Time]
141
+ # @param t4 [Time]
142
+ #
143
+ # @return [Float]
144
+ def offset(t1, t2, t3, t4)
145
+ # Timestamp Name ID When Generated
146
+ # ------------------------------------------------------------
147
+ # Originate Timestamp T1 time request sent by client
148
+ # Receive Timestamp T2 time request received by server
149
+ # Transmit Timestamp T3 time reply sent by server
150
+ # Destination Timestamp T4 time reply received by client
151
+ #
152
+ # The roundtrip delay d and system clock offset t are defined as:
153
+ #
154
+ # d = (T4 - T1) - (T3 - T2) t = ((T2 - T1) + (T3 - T4)) / 2.
155
+ # https://tools.ietf.org/html/rfc4330#section-5
156
+ ((t2 - t1) + (t3 - t4)) / 2
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nts
4
+ module Sntp
5
+ module Extension
6
+ # https://tools.ietf.org/html/draft-ietf-ntp-using-nts-for-ntp-20#section-5.6
7
+ class NtsAuthenticator
8
+ include Extension
9
+ attr_reader :nonce, :ciphertext, :padding_length
10
+
11
+ # @param nonce [String]
12
+ # @param ciphertext [String]
13
+ # @param padding_length [Integer]
14
+ def initialize(nonce, ciphertext, padding_length = 0)
15
+ @field_type = ExtensionFieldType::NTS_AUTHENTICATOR
16
+ @nonce = nonce
17
+ @ciphertext = ciphertext
18
+ @padding_length = padding_length
19
+ end
20
+
21
+ # @return [String]
22
+ def serialize
23
+ pn = pad_zero(@nonce)
24
+ pc = pad_zero(@ciphertext)
25
+ value = [pn.length, pc.length].pack('n2') \
26
+ + pn + pc + ("\x00" * @padding_length)
27
+ pv = pad_zero(value)
28
+ length = pv.length + 4
29
+
30
+ [@field_type, length].pack('n2') + pv
31
+ end
32
+
33
+ # @param s [String]
34
+ #
35
+ # @raise [Exception]
36
+ #
37
+ # @return [Nts::Sntp::Extension::NtsAuthenticator]
38
+ def self.deserialize(s)
39
+ raise Exception if s.length < 4
40
+
41
+ nl = s.slice(0, 2).unpack1('n')
42
+ cl = s.slice(2, 2).unpack1('n')
43
+ raise Exception if s.length < 4 + nl + cl
44
+
45
+ nonce = Extension.truncate_zero_padding(s.slice(4, nl))
46
+ ciphertext = Extension.truncate_zero_padding(s.slice(4 + nl, cl))
47
+ padding_length = s.length - 4 - nl - cl
48
+ NtsAuthenticator.new(nonce, ciphertext, padding_length)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nts
4
+ module Sntp
5
+ module Extension
6
+ # https://tools.ietf.org/html/draft-ietf-ntp-using-nts-for-ntp-20#section-5.4
7
+ class NtsCookie
8
+ include Extension
9
+ attr_reader :cookie
10
+
11
+ # @param cookie [String]
12
+ def initialize(cookie)
13
+ @field_type = ExtensionFieldType::NTS_COOKIE
14
+ @cookie = cookie
15
+ end
16
+
17
+ # @return [String]
18
+ def serialize
19
+ pc = pad_zero(@cookie)
20
+ length = pc.length + 4
21
+
22
+ [@field_type, length].pack('n2') + pc
23
+ end
24
+
25
+ # @param s [String]
26
+ #
27
+ # @return [Nts::Sntp::Extension::NtsCookie]
28
+ def self.deserialize(s)
29
+ NtsCookie.new(Extension.truncate_zero_padding(s))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nts
4
+ module Sntp
5
+ module Extension
6
+ # https://tools.ietf.org/html/draft-ietf-ntp-using-nts-for-ntp-20#section-5.3
7
+ class UniqueIdentifier
8
+ include Extension
9
+ attr_reader :id
10
+
11
+ # @param id [String]
12
+ def initialize(id = OpenSSL::Random.random_bytes(32))
13
+ raise Exception if id.length < 32
14
+
15
+ @field_type = ExtensionFieldType::UNIQUE_IDENTIFIER
16
+ @id = id
17
+ end
18
+
19
+ # @return [String]
20
+ def serialize
21
+ pi = pad_zero(@id)
22
+ length = pi.length + 4
23
+
24
+ [@field_type, length].pack('n2') + pi
25
+ end
26
+
27
+ # @param s [String]
28
+ #
29
+ # @return [Nts::Sntp::Extension::UniqueIdentifier]
30
+ def self.deserialize(s)
31
+ UniqueIdentifier.new(Extension.truncate_zero_padding(s))
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nts
4
+ module Sntp
5
+ module Extension
6
+ class UnknownExtension
7
+ include Extension
8
+ attr_reader :field_type
9
+ attr_reader :value
10
+
11
+ # @param value [String]
12
+ # @param field_type [Integer]
13
+ def initialize(value, field_type)
14
+ @field_type = field_type
15
+ @value = value
16
+ end
17
+
18
+ # @return [String]
19
+ def serialize
20
+ pv = pad_zero(@value)
21
+ length = pv.length + 4
22
+
23
+ [@field_type, length].pack('n2') + pv
24
+ end
25
+
26
+ # @param s [String]
27
+ # @param field_type [Integer]
28
+ #
29
+ # @return [Nts::Sntp::Extension::UnknownExtension]
30
+ def self.deserialize(s, field_type)
31
+ UnknownExtension.new(Extension.truncate_zero_padding(s), field_type)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nts
4
+ module Sntp
5
+ # https://tools.ietf.org/html/draft-ietf-ntp-using-nts-for-ntp-20#section-7.5
6
+ module ExtensionFieldType
7
+ UNIQUE_IDENTIFIER = 260
8
+ NTS_COOKIE = 516
9
+ NTS_COOKIE_PLACEHOLDER = 772
10
+ NTS_AUTHENTICATOR = 1028
11
+ end
12
+
13
+ # https://tools.ietf.org/html/rfc7822#section-3
14
+ module Extension
15
+ ZERO_PADDING = ['', "\x00\x00\x00", "\x00\x00", "\x00"].freeze
16
+
17
+ module_function
18
+
19
+ # @param s [String]
20
+ #
21
+ # @return [String]
22
+ def pad_zero(s)
23
+ s + ZERO_PADDING[s.length % 4]
24
+ end
25
+
26
+ # @param s [String]
27
+ #
28
+ # @return [String]
29
+ def truncate_zero_padding(s)
30
+ i = 1
31
+ i += 1 while s[-i] == "\x00"
32
+ s[0..-i]
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ Dir[File.dirname(__FILE__) + '/extension/*.rb'].each { |f| require f }
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require __dir__ + '/extension.rb'
4
+
5
+ module Nts
6
+ module Sntp
7
+ # https://tools.ietf.org/html/rfc5905#section-7.3
8
+ class Message
9
+ attr_reader :ntp_header
10
+ attr_reader :extensions
11
+
12
+ # @param ntp_header [String] 48-octet NTP header(leap ~ xmt)
13
+ # @param extensions [Array of Nts::Sntp::Extension::$Object]
14
+ #
15
+ # @raise [Exception]
16
+ def initialize(ntp_header, extensions = [])
17
+ @ntp_header = ntp_header
18
+ @extensions = extensions
19
+
20
+ raise 'extensions include only Sntp::Extension::$Object' \
21
+ unless @extensions.all? do |ex|
22
+ ex.class.ancestors.include?(Sntp::Extension)
23
+ end
24
+ end
25
+
26
+ # @return [Nts::Sntp::Extension::UniqueIdentifier]
27
+ def unique_identifier
28
+ @extensions.find { |ex| ex.is_a?(Extension::UniqueIdentifier) }
29
+ end
30
+
31
+ # @return [Nts::Sntp::Extension::NtsCookie]
32
+ def nts_cookie
33
+ @extensions.find { |ex| ex.is_a?(Extension::NtsCookie) }
34
+ end
35
+
36
+ # @return [Nts::Sntp::Extension::NtsAuthenticator]
37
+ def nts_authenticator
38
+ @extensions.find { |ex| ex.is_a?(Extension::NtsAuthenticator) }
39
+ end
40
+
41
+ # @return [String]
42
+ def serialize
43
+ @ntp_header + @extensions.map(&:serialize).join
44
+ end
45
+
46
+ # @param s [String]
47
+ #
48
+ # @raise [Exception]
49
+ #
50
+ # @return [Nts::Sntp::Message]
51
+ def self.deserialize(s)
52
+ raise Exception if s.length < 48
53
+
54
+ ntp_header = s.slice(0, 48)
55
+ extensions = extensions_deserialize(s[48..])
56
+
57
+ Message.new(ntp_header, extensions)
58
+ end
59
+
60
+ # @param s [String]
61
+ #
62
+ # @raise [Exception]
63
+ #
64
+ # @return [Array of Nts::Sntp::Extension::$Object]
65
+ # rubocop: disable Metrics/AbcSize
66
+ # rubocop: disable Metrics/CyclomaticComplexity
67
+ def self.extensions_deserialize(s)
68
+ i = 0
69
+ extensions = []
70
+ while i < s.length
71
+ raise Exception if i + 4 > s.length
72
+
73
+ field_type = s.slice(i, 2).unpack1('n')
74
+ value_len = s.slice(i + 2, 2).unpack1('n')
75
+ raise Exception if i + value_len > s.length
76
+
77
+ # `value_len` indicates the length of the entire extension field
78
+ # in octets.
79
+ sv = s.slice(i + 4, value_len - 4)
80
+ case field_type
81
+ when ExtensionFieldType::UNIQUE_IDENTIFIER
82
+ extensions << Extension::UniqueIdentifier.deserialize(sv)
83
+ when ExtensionFieldType::NTS_COOKIE
84
+ extensions << Extension::NtsCookie.deserialize(sv)
85
+ when ExtensionFieldType::NTS_COOKIE_PLACEHOLDER
86
+ # unsupported NtsCookiePlaceholder
87
+ warn sv.bytes.map { |x| x.to_s(16).rjust(2, '0') }.join(' ')
88
+ when ExtensionFieldType::NTS_AUTHENTICATOR
89
+ extensions << Extension::NtsAuthenticator.deserialize(sv)
90
+ else
91
+ extensions \
92
+ << Extension::UnknownExtension.deserialize(sv, field_type)
93
+ end
94
+ i += value_len
95
+ end
96
+ raise Exception unless i == s.length
97
+
98
+ extensions
99
+ end
100
+ # rubocop: enable Metrics/AbcSize
101
+ # rubocop: enable Metrics/CyclomaticComplexity
102
+
103
+ # @return [String]
104
+ def origin_timestamp
105
+ @ntp_header.slice(24, 8)
106
+ end
107
+
108
+ # @return [String]
109
+ def receive_timestamp
110
+ @ntp_header.slice(32, 8)
111
+ end
112
+
113
+ # @return [String]
114
+ def transmit_timestamp
115
+ @ntp_header.slice(40, 8)
116
+ end
117
+ end
118
+ end
119
+ end
data/lib/nts/sntp.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require __dir__ + '/sntp/message.rb'
4
+ require __dir__ + '/sntp/client.rb'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nts
4
+ VERSION = '0.0.1'
5
+ end
data/lib/nts.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'socket'
5
+ require 'time'
6
+ require 'tttls1.3'
7
+ require 'miscreant'
8
+
9
+ require 'nts/ntske'
10
+ require 'nts/sntp'
11
+ require 'nts/cli'
12
+ require 'nts/version'
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'nts/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'simple_nts_client'
9
+ spec.version = Nts::VERSION
10
+ spec.authors = ['thekuwayama']
11
+ spec.email = ['thekuwayama@gmail.com']
12
+ spec.summary = 'Simple NTS(Network Time Security) Client'
13
+ spec.description = spec.summary
14
+ spec.homepage = 'https://github.com/thekuwayama/simple_nts_client'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>=2.6.1'
17
+
18
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+ spec.bindir = 'exe'
22
+ spec.executables = ['simple_nts_client']
23
+
24
+ spec.add_development_dependency 'bundler'
25
+ spec.add_dependency 'miscreant'
26
+ spec.add_dependency 'tttls1.3', '>= 0.2.7'
27
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'spec_helper'
5
+
6
+ RSpec.describe AeadAlgorithmNegotiation do
7
+ context 'initialized with AEAD_AES_SIV_CMAC_256' do
8
+ let(:message) do
9
+ AeadAlgorithmNegotiation.new([AeadAlgorithm::AEAD_AES_SIV_CMAC_256])
10
+ end
11
+
12
+ it 'should be serialized' do
13
+ expect(message.algorithms).to eq [AeadAlgorithm::AEAD_AES_SIV_CMAC_256]
14
+ expect(message.serialize).to eq "\x00\x04\x00\x02\x00\x0F"
15
+ end
16
+ end
17
+
18
+ context 'received "\x00\x0f" means AEAD_AES_SIV_CMAC_256' do
19
+ let(:message) do
20
+ AeadAlgorithmNegotiation.deserialize("\x00\x0F", false)
21
+ end
22
+
23
+ it 'should be deserialized' do
24
+ expect(message.algorithms).to eq [AeadAlgorithm::AEAD_AES_SIV_CMAC_256]
25
+ expect(message.serialize).to eq "\x00\x04\x00\x02\x00\x0F"
26
+ end
27
+ end
28
+ end