enfcli 3.3.2.pre.alpha

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.
@@ -0,0 +1,562 @@
1
+ #
2
+ # Copyright 2018 Xaptum,Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ require 'enfthor'
17
+ require 'enfapi'
18
+ require 'base64'
19
+ require 'digest'
20
+ require 'openssl'
21
+ require 'ipaddr'
22
+
23
+ module EnfCli
24
+ module Cmd
25
+
26
+ class Xiam < EnfThor
27
+ no_commands {
28
+ def write_pem_file keyfile, data
29
+ # check if keyfile ends with .pem extension
30
+ keyfile = "#{keyfile}.pem" unless keyfile.end_with?(".pem")
31
+
32
+ # Write to file
33
+ if write_to_file keyfile, data
34
+ say "Created #{keyfile}", :green
35
+ end
36
+ end
37
+
38
+ def write_to_file filename, data
39
+ begin
40
+ # check if file exists
41
+ filename = EnfCli::expand_path(filename)
42
+ if File.exists?( filename )
43
+ proceed = ask("Overwrite #{filename}(type 'yes' to continue...)?")
44
+ return nil unless (proceed.downcase == 'yes')
45
+ end
46
+
47
+ # write to file
48
+ open filename, 'w' do |io| io.write data end
49
+
50
+ # return success
51
+ return true
52
+ rescue
53
+ raise EnfCli::ERROR, "Unable to write to file #{filename}! Please check file premissions."
54
+ end
55
+ end
56
+
57
+ def calculate_gid(basename, gpk)
58
+ Digest::SHA256.hexdigest([gpk].pack("H*") << [basename].pack("H*")).upcase
59
+ end
60
+
61
+ def scan_group_qr_code()
62
+ # Initalize values
63
+ gpk_header = ''
64
+ gpk_info = ''
65
+
66
+ # Scan QR Code
67
+ gpk_header = EnfCli::ask_password('Scan DAA group information:')
68
+ if gpk_header.length < 16
69
+ gpk_info = EnfCli::ask_password('')
70
+ else
71
+ tmp_header = gpk_header[0..12]
72
+ gpk_info = gpk_header[13..gpk_header.length]
73
+ gpk_header = tmp_header
74
+ end
75
+
76
+ gpk_info
77
+ end
78
+
79
+ def read_ec_key_file(keyfile)
80
+ # expand path
81
+ keyfile = EnfCli::expand_path(keyfile)
82
+
83
+ ## return if keyfile not found
84
+ raise EnfCli::ERROR, "#{keyfile} not found!" unless File.exists?(keyfile)
85
+
86
+ # read the private key
87
+ OpenSSL::PKey::EC.new(File.read(keyfile))
88
+ end
89
+
90
+ def display_groups groups
91
+ headings = ['DAA Group', 'Base Name', 'Provisioned', 'Onboarded', 'Domain' ]
92
+ rows = groups.map{ |hash|
93
+ [ Base64.decode64(hash[:gid]).unpack("H*")[0], Base64.decode64(hash[:basename]),
94
+ hash[:provisioned_count], hash[:onboarded_count],
95
+ "#{IPAddr::new_ntoh( Base64.decode64(hash[:domain][:prefix]) )}/#{hash[:domain][:prefix_length]}"
96
+ ]
97
+ }
98
+ render_table(headings, rows)
99
+ end
100
+
101
+ def provision_endpoint(ipv6_address, pub)
102
+ # encode public key
103
+ hex = pub.to_bn.to_s(16)
104
+ pubkey = Base64.strict_encode64([hex].pack("H*"))
105
+
106
+ # create NEW_CREDENTIAL_ECDSA
107
+ new_credential = {
108
+ :type => "ecdsa_p256",
109
+ :key => pubkey
110
+ }
111
+
112
+ # create NEW_ENDPOINT_AUTH
113
+ new_endpoint = {
114
+ :cred_oneof => {
115
+ :auth => {
116
+ :ecdsa_credential => new_credential,
117
+ :address_request => {
118
+ :address_request_oneof => {
119
+ :address => Base64.strict_encode64( ipv6_address.hton )
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ data = EnfApi::Iam.instance.provision_endpoint new_endpoint
127
+ provisioned_ip = data[:address]
128
+ provisioned_ip = EnfCli::IPV6::ntoh Base64.decode64(provisioned_ip)
129
+
130
+ # Make sure provisioned ip is same as the requested ip
131
+ raise EnfCli::ERROR, "Hello World" unless provisioned_ip.to_s.eql?( ipv6_address.to_s )
132
+
133
+ say "Created new ipv6 endpoint #{provisioned_ip}", :green
134
+ end
135
+
136
+ def update_endpoint_credentials(ipv6_address, pub)
137
+ # encode public key
138
+ hex = pub.to_bn.to_s(16)
139
+ pubkey = Base64.strict_encode64([hex].pack("H*"))
140
+
141
+ # create NEW_CREDENTIAL_ECDSA
142
+ new_credential = {
143
+ :type => "ecdsa_p256",
144
+ :key => pubkey
145
+ }
146
+
147
+ # create NEW_ECDSA_CREDENTIAL
148
+ new_cred = {
149
+ :cred_oneof => {
150
+ :ecdsa => new_credential
151
+ }
152
+ }
153
+
154
+ EnfApi::Iam.instance.update_endpoint_key ipv6_address, new_cred
155
+
156
+ say "Updated #{ipv6_address} credentials!", :green
157
+ end
158
+ }
159
+
160
+ desc "list-group", "List DAA Group Information"
161
+ def list_group
162
+ try_with_rescue_in_session do
163
+ qr_code_info = scan_group_qr_code()
164
+
165
+ # Extract gpk and basename
166
+ gpk_info = qr_code_info.split(",")
167
+ basename = gpk_info[0]
168
+ gpk = gpk_info[1]
169
+
170
+ # Calculate gid
171
+ gid = calculate_gid basename, gpk
172
+
173
+ # url safe encode gid
174
+ enc_gid = Base64.urlsafe_encode64([gid].pack("H*"))
175
+
176
+ data = EnfApi::Iam.instance.list_group enc_gid
177
+ groups = [data]
178
+
179
+ # display data
180
+ display_groups groups
181
+ end
182
+ end
183
+
184
+ desc "set-group-default-network", "Set /64 network as default for the DAA group"
185
+ method_option :'network', :type => :string, :required => true
186
+ method_option :'gid', :type => :string
187
+ def set_group_default_network
188
+ try_with_rescue_in_session do
189
+ network = EnfCli::IPV6Cidr.new options[:network]
190
+
191
+ # scan if gid not in options
192
+ if options[:gid]
193
+ gid = options[:gid]
194
+ else
195
+ qr_code_info = scan_group_qr_code()
196
+
197
+ # Extract gpk and basename
198
+ gpk_info = qr_code_info.split(",")
199
+ basename = gpk_info[0]
200
+ gpk = gpk_info[1]
201
+
202
+ # Calculate gid
203
+ gid = calculate_gid basename, gpk
204
+ end
205
+
206
+ # create IPV6_NETWORK
207
+ ipv6_network = {
208
+ :prefix => Base64.strict_encode64( network.prefix_bytes ),
209
+ :prefix_length => network.prefix_len
210
+ }
211
+
212
+ # create MODIFIED_GROUP
213
+ modified_group = {
214
+ :gid => Base64.strict_encode64([gid].pack("H*")),
215
+ :default_network => ipv6_network
216
+ }
217
+
218
+ # url safe encode gid
219
+ enc_gid = Base64.urlsafe_encode64([gid].pack("H*"))
220
+
221
+ # Post to server
222
+ EnfApi::Iam.instance.update_group enc_gid, modified_group
223
+
224
+ say "Set #{network} as default network for group #{gid}", :green
225
+ end
226
+ end
227
+
228
+ desc "set-group-domain", "Set /48 domain for the DAA group"
229
+ method_option :'domain', :type => :string, :required => true
230
+ method_option :'gid', :type => :string
231
+ def set_group_domain
232
+ try_with_rescue_in_session do
233
+ domain_network = EnfCli::IPV6Cidr.new options[:domain]
234
+
235
+ # scan if gid not in options
236
+ if options[:gid]
237
+ gid = options[:gid]
238
+ else
239
+ qr_code_info = scan_group_qr_code()
240
+
241
+ # Extract gpk and basename
242
+ gpk_info = qr_code_info.split(",")
243
+ basename = gpk_info[0]
244
+ gpk = gpk_info[1]
245
+
246
+ # Calculate gid
247
+ gid = calculate_gid basename, gpk
248
+ end
249
+
250
+ # create IPV6_NETWORK
251
+ ipv6_network = {
252
+ :prefix => Base64.strict_encode64( domain_network.prefix_bytes ),
253
+ :prefix_length => domain_network.prefix_len
254
+ }
255
+
256
+ # create MODIFIED_GROUP
257
+ modified_group = {
258
+ :gid => Base64.strict_encode64([gid].pack("H*")),
259
+ :domain => ipv6_network
260
+ }
261
+
262
+ # url safe encode gid
263
+ enc_gid = Base64.urlsafe_encode64([gid].pack("H*"))
264
+
265
+ # Post to server
266
+ EnfApi::Iam.instance.update_group enc_gid, modified_group
267
+
268
+ say "Associated group #{gid} with #{domain_network}", :green
269
+ end
270
+ end
271
+
272
+ desc "add-group-to-network", "Associate a DAA Group with /64 network"
273
+ method_option :'network', :type => :string, :required => true
274
+ method_option :'gid', :type => :string
275
+ def add_group_to_network
276
+ try_with_rescue_in_session do
277
+ network = EnfCli::IPV6Cidr.new options[:network]
278
+
279
+ # scan if gid not in options
280
+ if options[:gid]
281
+ gid = options[:gid]
282
+ else
283
+ qr_code_info = scan_group_qr_code()
284
+
285
+ # Extract gpk and basename
286
+ gpk_info = qr_code_info.split(",")
287
+ basename = gpk_info[0]
288
+ gpk = gpk_info[1]
289
+
290
+ # Calculate gid
291
+ gid = calculate_gid basename, gpk
292
+ end
293
+
294
+ # create IPV6_NETWORK
295
+ ipv6_network = {
296
+ :prefix => Base64.strict_encode64( network.prefix_bytes ),
297
+ :prefix_length => network.prefix_len
298
+ }
299
+
300
+ # url safe encode gid
301
+ enc_gid = Base64.urlsafe_encode64([gid].pack("H*"))
302
+
303
+ # Post to server
304
+ EnfApi::Iam.instance.add_group_to_network enc_gid, ipv6_network
305
+
306
+ say "Associated group #{gid} with #{network}", :green
307
+ end
308
+ end
309
+
310
+ desc "provision-group", "Provision a DAA Group"
311
+ method_option :'device-count', :type => :numeric, :required => true
312
+ method_option :'operator', :type => :array, :required => true, :banner => "OPERATOR"
313
+ def provision_group
314
+ try_with_rescue_in_session do
315
+ qr_code_info = scan_group_qr_code()
316
+
317
+ # Extract gpk and basename
318
+ gpk_info = qr_code_info.split(",")
319
+ basename = gpk_info[0]
320
+ gpk = gpk_info[1]
321
+
322
+ # Calculate gid
323
+ gid = calculate_gid basename, gpk
324
+
325
+ # Extract options
326
+ operator = options.operator.join(" ").gsub(/\A"+(.*?)"+\Z/m, '\1')
327
+ quantity = options[:'device-count']
328
+
329
+ # base64 encode all binary data: basename, gpk, gid
330
+ enc_basename = Base64.strict_encode64([basename].pack("H*"))
331
+ enc_gpk = Base64.strict_encode64([gpk].pack("H*"))
332
+ enc_gid = Base64.strict_encode64([gid].pack("H*"))
333
+
334
+ # create NEW_PROVISIONING
335
+ new_provisioning = {
336
+ :group_id => enc_gid,
337
+ :quantity => quantity,
338
+ :operator => operator
339
+ }
340
+
341
+ # create NEW_GROUP
342
+ new_group = {
343
+ :group_public_key => enc_gpk,
344
+ :basename => enc_basename,
345
+ :provisionings => [ new_provisioning ]
346
+ }
347
+
348
+ # Post to server
349
+ EnfApi::Iam.instance.provision_group new_group
350
+
351
+ # display success
352
+ say "Provisioned a new DAA group #{gid}", :green
353
+ end
354
+ end
355
+
356
+ desc "list-network-groups", "List all DAA groups associated with a /64 network"
357
+ method_option :'network', :type => :string, :required => true
358
+ def list_network_groups
359
+ try_with_rescue_in_session do
360
+ # validate network
361
+ network = EnfCli::IPV6Cidr.new options[:network]
362
+
363
+ # call the api
364
+ data = EnfApi::Iam.instance.list_network_groups network.to_s
365
+ groups = data[:body]
366
+
367
+ # display data
368
+ display_groups groups
369
+ end
370
+ end
371
+
372
+ desc "create-endpoint-key", "Generate a pem encoded EC key pair"
373
+ method_option :'key-out-file', :type => :string, :required => true, :banner => '<file>'
374
+ method_option :'public-key-out-file', :type => :string, :required => true, :banner => '<file>'
375
+ def create_endpoint_key
376
+ try_with_rescue_in_session do
377
+ # get options
378
+ keyfile = options['key-out-file']
379
+ pubfile = options['public-key-out-file']
380
+
381
+ # Generate a key pair
382
+ key = OpenSSL::PKey::EC.new('prime256v1')
383
+ key.generate_key
384
+
385
+ # write key pair to file
386
+ write_pem_file keyfile, key.to_pem
387
+
388
+ # write public key to file
389
+ key.private_key = nil
390
+ write_pem_file pubfile, key.to_pem
391
+ end
392
+ end
393
+
394
+ desc "create-endpoint-cert", "Creates and signs a certificate for a particular identity using the provided private key"
395
+ method_option :'identity', :type => :string, :required => true, :banner => '<ipv6>'
396
+ method_option :'key-in-file', :type => :string, :required => true, :banner => '<file>'
397
+ method_option :'cert-out-file', :type => :string, :required => true, :banner => '<file>'
398
+ def create_endpoint_cert
399
+ try_with_rescue_in_session do
400
+ # get options
401
+ ipv6 = EnfCli::IPV6.new(options['identity']).to_s
402
+ keyfile = EnfCli::expand_path(options['key-in-file'])
403
+ certfile = EnfCli::expand_path(options['cert-out-file'])
404
+
405
+ # read the private key
406
+ key = read_ec_key_file keyfile
407
+
408
+ # generate cert
409
+ cert = EnfCli::generate_ec_cert(key, ipv6)
410
+
411
+ # write to file
412
+ write_pem_file certfile, cert.to_pem
413
+ end
414
+ end
415
+
416
+ desc "create-endpoint-in-network", "Create an IPV6 endpoint in /64 network"
417
+ method_option :'network', :type => :string, :required => true, :banner => "<ipv6 network>"
418
+ method_option :'public-key-in-file', :type => :string, :required => true, :banner => "<file>"
419
+ def create_endpoint_in_network
420
+ try_with_rescue_in_session do
421
+ # Read options
422
+ network = EnfCli::IPV6Cidr.new options[:network]
423
+ keyfile = EnfCli::expand_path(options['public-key-in-file'])
424
+
425
+ # read keyfile
426
+ key = read_ec_key_file keyfile
427
+
428
+ # encode public key
429
+ pub = key.public_key
430
+ hex = pub.to_bn.to_s(16)
431
+ pubkey = Base64.strict_encode64([hex].pack("H*"))
432
+
433
+ # create NEW_CREDENTIAL_ECDSA
434
+ new_credential = {
435
+ :type => "ecdsa_p256",
436
+ :key => pubkey
437
+ }
438
+
439
+ # create IPV6_NETWORK
440
+ ipv6_network = {
441
+ :prefix => Base64.strict_encode64( network.prefix_bytes ),
442
+ :prefix_length => network.prefix_len
443
+ }
444
+
445
+ # create NEW_ENDPOINT_AUTH
446
+ new_endpoint = {
447
+ :cred_oneof => {
448
+ :auth => {
449
+ :ecdsa_credential => new_credential,
450
+ :address_request => {
451
+ :address_request_oneof => {
452
+ :network => ipv6_network
453
+ }
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ data = EnfApi::Iam.instance.provision_endpoint new_endpoint
460
+ provisioned_ip = data[:address]
461
+ provisioned_ip = EnfCli::IPV6::ntoh Base64.decode64(provisioned_ip)
462
+
463
+ say "Created new ipv6 endpoint #{provisioned_ip}", :green
464
+ end
465
+ end
466
+
467
+ desc "create-endpoint-with-address", "Create an IPV6 endpoint in /64 network with user specified address"
468
+ method_option :'address', :type => :string, :required => true, :banner => "<ipv6 address>"
469
+ method_option :'public-key-in-file', :type => :string, :required => true, :banner => "<file>"
470
+ def create_endpoint_with_address
471
+ try_with_rescue_in_session do
472
+ # Read address
473
+ ipv6_address = EnfCli::IPV6.new options[:address]
474
+
475
+ # read key
476
+ keyfile = EnfCli::expand_path(options['public-key-in-file'])
477
+ key = read_ec_key_file keyfile
478
+ pub = key.public_key
479
+
480
+ # create_endpoint
481
+ provision_endpoint ipv6_address, pub
482
+
483
+ end
484
+ end
485
+
486
+ desc "create-endpoint-from-cert", "Create an IPV6 endpoint in /64 network with address from cert"
487
+ method_option :'cert-in-file', :type => :string, :required => true, :banner => "<file>"
488
+ def create_endpoint_from_cert
489
+ try_with_rescue_in_session do
490
+ # read options
491
+ certfile = EnfCli::expand_path(options['cert-in-file'])
492
+ raise EnfCli::ERROR, "#{certfile} does not exist!" unless File.exists?(certfile)
493
+
494
+ # read cert file
495
+ rawcert = File.read certfile
496
+ cert = OpenSSL::X509::Certificate.new rawcert
497
+
498
+ # get subject
499
+ subject = cert.subject.to_a
500
+
501
+ # get ipv6 CN record
502
+ cn = subject.select { |d| "CN".eql?( d[0].upcase ) }
503
+ ipv6_address = EnfCli::IPV6.new cn[0][1]
504
+
505
+ # get the public key
506
+ pub = cert.public_key.public_key
507
+
508
+ # create_endpoint
509
+ provision_endpoint ipv6_address, pub
510
+
511
+ end
512
+ end
513
+
514
+ desc "update-endpoint-key", "Update an IPV6 endpoint's credentials"
515
+ method_option :'address', :type => :string, :required => true, :banner => "<ipv6 address>"
516
+ method_option :'public-key-in-file', :type => :string, :required => true, :banner => "<file>"
517
+ def update_endpoint_key
518
+ try_with_rescue_in_session do
519
+ # Read address
520
+ ipv6_address = EnfCli::IPV6.new options[:address]
521
+
522
+ # read key
523
+ keyfile = EnfCli::expand_path(options['public-key-in-file'])
524
+ key = read_ec_key_file keyfile
525
+ pub = key.public_key
526
+
527
+ # update endpoint
528
+ update_endpoint_credentials ipv6_address, pub
529
+ end
530
+ end
531
+
532
+ desc "update-endpoint-cert", "Update an IPV6 endpoint's credentials"
533
+ method_option :'cert-in-file', :type => :string, :required => true, :banner => "<file>"
534
+ def update_endpoint_cert
535
+ try_with_rescue_in_session do
536
+ # read options
537
+ certfile = EnfCli::expand_path(options['cert-in-file'])
538
+ raise EnfCli::ERROR, "#{certfile} does not exist!" unless File.exists?(certfile)
539
+
540
+ # read cert file
541
+ rawcert = File.read certfile
542
+ cert = OpenSSL::X509::Certificate.new rawcert
543
+
544
+ # get subject
545
+ subject = cert.subject.to_a
546
+
547
+ # get ipv6 CN record
548
+ cn = subject.select { |d| "CN".eql?( d[0].upcase ) }
549
+ ipv6_address = EnfCli::IPV6.new cn[0][1]
550
+
551
+ # get the public key
552
+ pub = cert.public_key.public_key
553
+
554
+ # update endpoint
555
+ update_endpoint_credentials ipv6_address, pub
556
+ end
557
+ end
558
+
559
+ end
560
+ end
561
+ end
562
+
@@ -0,0 +1,18 @@
1
+ #
2
+ # Copyright 2018 Xaptum,Inc
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module EnfCli
17
+ VERSION = '3.3.2-alpha'
18
+ end