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.
- 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
|