simple_nts_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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