simple_nts_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rubocop.yml +24 -0
- data/.travis.yml +17 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +57 -0
- data/Rakefile +10 -0
- data/exe/simple_nts_client +8 -0
- data/lib/nts/cli.rb +83 -0
- data/lib/nts/ntske/client.rb +100 -0
- data/lib/nts/ntske/message/aead_algorithm_negotiation.rb +47 -0
- data/lib/nts/ntske/message/cookie.rb +33 -0
- data/lib/nts/ntske/message/end_of_message.rb +30 -0
- data/lib/nts/ntske/message/error_record.rb +39 -0
- data/lib/nts/ntske/message/nts_next_protocol_negotiation.rb +35 -0
- data/lib/nts/ntske/message/ntsv4_port_negotiation.rb +36 -0
- data/lib/nts/ntske/message/ntsv4_server_negotiation.rb +36 -0
- data/lib/nts/ntske/message/warning_record.rb +34 -0
- data/lib/nts/ntske/message.rb +107 -0
- data/lib/nts/ntske.rb +4 -0
- data/lib/nts/sntp/client.rb +160 -0
- data/lib/nts/sntp/extension/nts_authenticator.rb +53 -0
- data/lib/nts/sntp/extension/nts_cookie.rb +34 -0
- data/lib/nts/sntp/extension/unique_identifier.rb +36 -0
- data/lib/nts/sntp/extension/unknown_extension.rb +36 -0
- data/lib/nts/sntp/extension.rb +38 -0
- data/lib/nts/sntp/message.rb +119 -0
- data/lib/nts/sntp.rb +4 -0
- data/lib/nts/version.rb +5 -0
- data/lib/nts.rb +12 -0
- data/simple_nts_client.gemspec +27 -0
- data/spec/aead_algorithm_negotiation_spec.rb +28 -0
- data/spec/cookie_spec.rb +36 -0
- data/spec/end_of_message_spec.rb +26 -0
- data/spec/error_record_spec.rb +28 -0
- data/spec/extension_spec.rb +28 -0
- data/spec/nts_authenticator_spec.rb +64 -0
- data/spec/nts_cookie_spec.rb +36 -0
- data/spec/nts_next_protocol_negotiation_spec.rb +28 -0
- data/spec/ntsv4_port_negotiation_spec.rb +28 -0
- data/spec/ntsv4_server_negotiation_spec.rb +28 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/unique_identifier_spec.rb +36 -0
- data/spec/unknown_extension_spec.rb +46 -0
- data/spec/warning_record_spec.rb +28 -0
- 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,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
data/lib/nts/version.rb
ADDED
data/lib/nts.rb
ADDED
@@ -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
|