1#!/usr/bin/perl
2
3use strict;
4use 5.8.0;
5
6use Getopt::Long qw(:config gnu_getopt no_ignore_case pass_through);
7use Cwd qw(getcwd realpath);
8use File::Basename;
9use File::Copy;
10use File::Path;
11use Sys::Hostname;
12use Carp;
13
14our @CC = (
15["AF","AFGHANISTAN"],
16["AX","ÅLAND ISLANDS"],
17["AL","ALBANIA"],
18["DZ","ALGERIA"],
19["AS","AMERICAN SAMOA"],
20["AD","ANDORRA"],
21["AO","ANGOLA"],
22["AI","ANGUILLA"],
23["AQ","ANTARCTICA"],
24["AG","ANTIGUA AND BARBUDA"],
25["AR","ARGENTINA"],
26["AM","ARMENIA"],
27["AW","ARUBA"],
28["AU","AUSTRALIA"],
29["AT","AUSTRIA"],
30["AZ","AZERBAIJAN"],
31["BS","BAHAMAS"],
32["BH","BAHRAIN"],
33["BD","BANGLADESH"],
34["BB","BARBADOS"],
35["BY","BELARUS"],
36["BE","BELGIUM"],
37["BZ","BELIZE"],
38["BJ","BENIN"],
39["BM","BERMUDA"],
40["BT","BHUTAN"],
41["BO","BOLIVIA, PLURINATIONAL STATE OF"],
42["BA","BOSNIA AND HERZEGOVINA"],
43["BW","BOTSWANA"],
44["BV","BOUVET ISLAND"],
45["BR","BRAZIL"],
46["IO","BRITISH INDIAN OCEAN TERRITORY"],
47["BN","BRUNEI DARUSSALAM"],
48["BG","BULGARIA"],
49["BF","BURKINA FASO"],
50["BI","BURUNDI"],
51["KH","CAMBODIA"],
52["CM","CAMEROON"],
53["CA","CANADA"],
54["CV","CAPE VERDE"],
55["KY","CAYMAN ISLANDS"],
56["CF","CENTRAL AFRICAN REPUBLIC"],
57["TD","CHAD"],
58["CL","CHILE"],
59["CN","CHINA"],
60["CX","CHRISTMAS ISLAND"],
61["CC","COCOS (KEELING) ISLANDS"],
62["CO","COLOMBIA"],
63["KM","COMOROS"],
64["CG","CONGO"],
65["CD","CONGO, THE DEMOCRATIC REPUBLIC OF THE"],
66["CK","COOK ISLANDS"],
67["CR","COSTA RICA"],
68["CI","CÔTE D'IVOIRE"],
69["HR","CROATIA"],
70["CU","CUBA"],
71["CY","CYPRUS"],
72["CZ","CZECH REPUBLIC"],
73["DK","DENMARK"],
74["DJ","DJIBOUTI"],
75["DM","DOMINICA"],
76["DO","DOMINICAN REPUBLIC"],
77["EC","ECUADOR"],
78["EG","EGYPT"],
79["SV","EL SALVADOR"],
80["GQ","EQUATORIAL GUINEA"],
81["ER","ERITREA"],
82["EE","ESTONIA"],
83["ET","ETHIOPIA"],
84["FK","FALKLAND ISLANDS (MALVINAS)"],
85["FO","FAROE ISLANDS"],
86["FJ","FIJI"],
87["FI","FINLAND"],
88["FR","FRANCE"],
89["GF","FRENCH GUIANA"],
90["PF","FRENCH POLYNESIA"],
91["TF","FRENCH SOUTHERN TERRITORIES"],
92["GA","GABON"],
93["GM","GAMBIA"],
94["GE","GEORGIA"],
95["DE","GERMANY"],
96["GH","GHANA"],
97["GI","GIBRALTAR"],
98["GR","GREECE"],
99["GL","GREENLAND"],
100["GD","GRENADA"],
101["GP","GUADELOUPE"],
102["GU","GUAM"],
103["GT","GUATEMALA"],
104["GG","GUERNSEY"],
105["GN","GUINEA"],
106["GW","GUINEA-BISSAU"],
107["GY","GUYANA"],
108["HT","HAITI"],
109["HM","HEARD ISLAND AND MCDONALD ISLANDS"],
110["VA","HOLY SEE (VATICAN CITY STATE)"],
111["HN","HONDURAS"],
112["HK","HONG KONG"],
113["HU","HUNGARY"],
114["IS","ICELAND"],
115["IN","INDIA"],
116["ID","INDONESIA"],
117["IR","IRAN, ISLAMIC REPUBLIC OF"],
118["IQ","IRAQ"],
119["IE","IRELAND"],
120["IM","ISLE OF MAN"],
121["IL","ISRAEL"],
122["IT","ITALY"],
123["JM","JAMAICA"],
124["JP","JAPAN"],
125["JE","JERSEY"],
126["JO","JORDAN"],
127["KZ","KAZAKHSTAN"],
128["KE","KENYA"],
129["KI","KIRIBATI"],
130["KP","KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF"],
131["KR","KOREA, REPUBLIC OF"],
132["KW","KUWAIT"],
133["KG","KYRGYZSTAN"],
134["LA","LAO PEOPLE'S DEMOCRATIC REPUBLIC"],
135["LV","LATVIA"],
136["LB","LEBANON"],
137["LS","LESOTHO"],
138["LR","LIBERIA"],
139["LY","LIBYAN ARAB JAMAHIRIYA"],
140["LI","LIECHTENSTEIN"],
141["LT","LITHUANIA"],
142["LU","LUXEMBOURG"],
143["MO","MACAO"],
144["MK","MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF"],
145["MG","MADAGASCAR"],
146["MW","MALAWI"],
147["MY","MALAYSIA"],
148["MV","MALDIVES"],
149["ML","MALI"],
150["MT","MALTA"],
151["MH","MARSHALL ISLANDS"],
152["MQ","MARTINIQUE"],
153["MR","MAURITANIA"],
154["MU","MAURITIUS"],
155["YT","MAYOTTE"],
156["MX","MEXICO"],
157["FM","MICRONESIA, FEDERATED STATES OF"],
158["MD","MOLDOVA, REPUBLIC OF"],
159["MC","MONACO"],
160["MN","MONGOLIA"],
161["ME","MONTENEGRO"],
162["MS","MONTSERRAT"],
163["MA","MOROCCO"],
164["MZ","MOZAMBIQUE"],
165["MM","MYANMAR"],
166["NA","NAMIBIA"],
167["NR","NAURU"],
168["NP","NEPAL"],
169["NL","NETHERLANDS"],
170["AN","NETHERLANDS ANTILLES"],
171["NC","NEW CALEDONIA"],
172["NZ","NEW ZEALAND"],
173["NI","NICARAGUA"],
174["NE","NIGER"],
175["NG","NIGERIA"],
176["NU","NIUE"],
177["NF","NORFOLK ISLAND"],
178["MP","NORTHERN MARIANA ISLANDS"],
179["NO","NORWAY"],
180["OM","OMAN"],
181["PK","PAKISTAN"],
182["PW","PALAU"],
183["PS","PALESTINIAN TERRITORY, OCCUPIED"],
184["PA","PANAMA"],
185["PG","PAPUA NEW GUINEA"],
186["PY","PARAGUAY"],
187["PE","PERU"],
188["PH","PHILIPPINES"],
189["PN","PITCAIRN"],
190["PL","POLAND"],
191["PT","PORTUGAL"],
192["PR","PUERTO RICO"],
193["QA","QATAR"],
194["RE","RÉUNION"],
195["RO","ROMANIA"],
196["RU","RUSSIAN FEDERATION"],
197["RW","RWANDA"],
198["BL","SAINT BARTHÉLEMY"],
199["SH","SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA"],
200["KN","SAINT KITTS AND NEVIS"],
201["LC","SAINT LUCIA"],
202["MF","SAINT MARTIN"],
203["PM","SAINT PIERRE AND MIQUELON"],
204["VC","SAINT VINCENT AND THE GRENADINES"],
205["WS","SAMOA"],
206["SM","SAN MARINO"],
207["ST","SAO TOME AND PRINCIPE"],
208["SA","SAUDI ARABIA"],
209["SN","SENEGAL"],
210["RS","SERBIA"],
211["SC","SEYCHELLES"],
212["SL","SIERRA LEONE"],
213["SG","SINGAPORE"],
214["SK","SLOVAKIA"],
215["SI","SLOVENIA"],
216["SB","SOLOMON ISLANDS"],
217["SO","SOMALIA"],
218["ZA","SOUTH AFRICA"],
219["GS","SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS"],
220["ES","SPAIN"],
221["LK","SRI LANKA"],
222["SD","SUDAN"],
223["SR","SURINAME"],
224["SJ","SVALBARD AND JAN MAYEN"],
225["SZ","SWAZILAND"],
226["SE","SWEDEN"],
227["CH","SWITZERLAND"],
228["SY","SYRIAN ARAB REPUBLIC"],
229["TW","TAIWAN, PROVINCE OF CHINA"],
230["TJ","TAJIKISTAN"],
231["TZ","TANZANIA, UNITED REPUBLIC OF"],
232["TH","THAILAND"],
233["TL","TIMOR-LESTE"],
234["TG","TOGO"],
235["TK","TOKELAU"],
236["TO","TONGA"],
237["TT","TRINIDAD AND TOBAGO"],
238["TN","TUNISIA"],
239["TR","TURKEY"],
240["TM","TURKMENISTAN"],
241["TC","TURKS AND CAICOS ISLANDS"],
242["TV","TUVALU"],
243["UG","UGANDA"],
244["UA","UKRAINE"],
245["AE","UNITED ARAB EMIRATES"],
246["GB","UNITED KINGDOM"],
247["US","UNITED STATES"],
248["UM","UNITED STATES MINOR OUTLYING ISLANDS"],
249["UY","URUGUAY"],
250["UZ","UZBEKISTAN"],
251["VU","VANUATU"],
252["VE","VENEZUELA, BOLIVARIAN REPUBLIC OF"],
253["VN","VIET NAM"],
254["VG","VIRGIN ISLANDS, BRITISH"],
255["VI","VIRGIN ISLANDS, U.S."],
256["WF","WALLIS AND FUTUNA"],
257["EH","WESTERN SAHARA"],
258["YE","YEMEN"],
259["ZM","ZAMBIA"],
260["ZW","ZIMBABWE"],
261);
262
263package NetSNMP::Term;
264# gratefully taken from Wayne Thompson's Term::Complete
265# if newer CORE modules could be used this could be removed
266our($complete, $exit, $done, $kill, $erase1, $erase2, $tty_raw_noecho, $tty_restore, $stty, $tty_safe_restore);
267our($tty_saved_state) = '';
268CONFIG: {
269  $exit   = "\003";
270  $done   = "\004";
271  $kill   = "\025";
272  $erase1 =   "\177";
273  $erase2 =   "\010";
274  foreach my $s (qw(/bin/stty /usr/bin/stty)) {
275    if (-x $s) {
276      $tty_raw_noecho = "$s raw -echo";
277      $tty_restore    = "$s -raw echo";
278      $tty_safe_restore = $tty_restore;
279      $stty = $s;
280      last;
281    }
282  }
283}
284
285sub Complete {
286  my($prompt, $dflt, $help, $match, @cmp_lst);
287  my (%help, $cmp, $test, $l, @match);
288  my ($return, $r, $exitting) = ("", 0, 0);
289  my $tab;
290
291  $prompt = shift;
292  $dflt = shift;
293  $help = shift;
294  $match = shift;
295
296  if (ref $_[0] and ref($_[0][0])) {
297    @cmp_lst = @{$_[0]};
298  } else {
299    @cmp_lst = @_;
300  }
301  @cmp_lst = map {if (ref($_)) { $help{$_->[0]}=$_->[1]; $_->[0]} else {$_;}} @cmp_lst;
302
303  # Attempt to save the current stty state, to be restored later
304  if (defined $stty && defined $tty_saved_state && $tty_saved_state eq '') {
305    $tty_saved_state = qx($stty -g 2>/dev/null);
306    if ($?) {
307      # stty -g not supported
308      $tty_saved_state = undef;
309    } else {
310      $tty_saved_state =~ s/\s+$//g;
311      $tty_restore = qq($stty "$tty_saved_state" 2>/dev/null);
312    }
313  }
314  system $tty_raw_noecho if defined $tty_raw_noecho;
315 LOOP: {
316    local $_;
317    print($prompt, $return);
318    while (($_ = getc(STDIN)) ne "\r") {
319    CASE: {
320	# (TAB) attempt completion
321	$_ eq "\t" && do {
322	  if ($tab) {
323	    print(join("\r\n", '', map {exists $help{$_} ? sprintf("\t%-10.10s - %s", $_, $help{$_}) :
324					  "\t$_"} grep(/^\Q$return/, @cmp_lst)), "\r\n");
325	    $tab--;
326	    redo LOOP;
327	  }
328	  @match = grep(/^\Q$return/, @cmp_lst);
329	  unless ($#match < 0) {
330	    $l = length($test = shift(@match));
331	    foreach $cmp (@match) {
332	      until (substr($cmp, 0, $l) eq substr($test, 0, $l)) {
333		$l--;
334	      }
335	    }
336	    print("\a");
337	    print($test = substr($test, $r, $l - $r));
338	    $r = length($return .= $test);
339	  }
340	  $tab++;
341	  last CASE;
342	};
343
344	# (^C) exit
345	$_ eq $exit && do {
346	  print("\r\naborting application...\r\n");
347	  $exitting++;
348	  undef $return;
349	  last LOOP;
350	};
351
352	# (^D) done
353	$_ eq $done && do {
354	  undef $return;
355	  last LOOP;
356	};
357
358	# (?) show help if available
359	$_ eq '?' && do {
360	  print("\r\n$help\r\n");
361	  if (exists $help{$return}) {
362	    print("\t$return - $help{$return}\r\n");
363	  } else {
364	    print(join("\r\n", map {exists $help{$_} ? "\t$_ - $help{$_}" :
365				      "\t$_"} grep(/^\Q$return/, @cmp_lst)), "\r\n");
366	  }
367	  redo LOOP;
368	};
369
370	# (^U) kill
371	$_ eq $kill && do {
372	  if ($r) {
373	    $r	= 0;
374	    $return	= "";
375	    print("\r\n");
376	    redo LOOP;
377	  }
378	  last CASE;
379	};
380
381	# (DEL) || (BS) erase
382	($_ eq $erase1 || $_ eq $erase2) && do {
383	  if ($r) {
384	    print("\b \b");
385	    chop($return);
386	    $r--;
387	  }
388	  last CASE;
389	};
390
391	# printable char
392	ord >= 32 && do {
393	  $return .= $_;
394	  $r++;
395	  print;
396	  last CASE;
397	};
398      }
399    }
400
401    if (defined $return) {
402      if (length($return) == 0 or $return eq $dflt) {
403	$return = $dflt;
404      } elsif ($match == $NetSNMP::Cert::MATCH and scalar(grep {/^$return$/} @cmp_lst) != 1 or
405	       $match == $NetSNMP::Cert::PREMATCH and scalar(grep {$return=~/$_/} @cmp_lst) != 1) {
406	$r	= 0;
407	$return	= "";
408	print("\r\nChoose a valid option, or ^D to exit\r\n");
409	redo LOOP;
410      }
411    }
412  }
413 DONE:
414  # system $tty_restore if defined $tty_restore;
415  if (defined $tty_saved_state && defined $tty_restore && defined $tty_safe_restore) {
416    system $tty_restore;
417    if ($?) {
418      # tty_restore caused error
419      system $tty_safe_restore;
420    }
421  }
422
423  exit(1) if $exitting;
424  print("\n");
425  return $return;
426}
427
428package NetSNMP::Cert;
429
430our $VERSION      = '0.2.9';
431
432our $PROG         = ::basename($0);
433our $DEBUG        = 0;
434our $OK           = 0;
435our $ERR          = -1;
436
437# user input param
438our $MATCH        = 1;
439our $PREMATCH     = 2;
440
441# Load LWP if possible to import cert from URL
442eval('use LWP::UserAgent;');
443our $haveUserAgent = ($@ ? 0 : 1);
444
445# required executables
446our $OPENSSL     = $ENV{NET_SNMP_CRT_OPENSSL}  || 'openssl';
447our $CFGTOOL     = $ENV{NET_SNMP_CRT_CFGTOOL}  || 'net-snmp-config';
448
449# default app config file
450our $CFGFILE     = $ENV{NET_SNMP_CRT_CFGFILE}  || 'net-snmp-cert.conf';
451
452# default OpenSSL config files
453our $SSLCFGIN    = $ENV{NET_SNMP_CRT_SSLCFGIN} || 'openssl.in';
454our $SSLCFGOUT   = $ENV{NET_SNMP_CRT_SSLCFGIN} || '.openssl.conf';
455our $SSLCFG      = $ENV{NET_SNMP_CRT_SSLCFG};
456
457# default cmd logs
458our $OUTLOG      = $ENV{NET_SNMP_CRT_OUTLOG}   || '.cmd.out.log';
459our $ERRLOG      = $ENV{NET_SNMP_CRT_ERRLOG}   || '.cmd.err.log';
460
461# default cert dirs
462our $TLSDIR      = $ENV{NET_SNMP_CRT_TLSDIR}   || 'tls';
463our $CRTDIR      = $ENV{NET_SNMP_CRT_CRTDIR}   || 'certs';
464our $NEWCRTDIR   = $ENV{NET_SNMP_CRT_NEWCRTDIR}|| 'newcerts';
465our $CADIR       = $ENV{NET_SNMP_CRT_CADIR}    || 'ca-certs';
466our $PRIVDIR     = $ENV{NET_SNMP_CRT_PRIVDIR}  || 'private';
467
468our $SERIAL      = $ENV{NET_SNMP_CRT_SERIAL}   || '.serial';
469our $INDEX       = $ENV{NET_SNMP_CRT_INDEX}    || '.index';
470
471our $DEFCADAYS   = $ENV{NET_SNMP_CRT_DEFCADAYS} || 1825;
472our $DEFCRTDAYS  = $ENV{NET_SNMP_CRT_DEFCRTDAYS}|| 365;
473
474our @TLSDIRS     = ($CRTDIR, $NEWCRTDIR, $CADIR, $PRIVDIR);
475
476our @CRTSUFFIXES = qw(.pem .csr .der .crt);
477
478our @X509FMTS = qw(text modulus serial subject_hash issuer_hash hash subject
479		    purpose issuer startdate enddate dates fingerprint C);
480
481sub dprint {
482  my $str = shift;
483  my $opts = shift;
484  my $dlevel = shift || 1;
485  my $flag = (defined $opts ? int($opts->{'debug'} >= $dlevel) : 1);
486  my ($pkg, $file, $line, $sub) = caller(1);
487  my ($pkg0, $file0, $line0) = caller(0);
488  print("${sub}():$line0: $str") if $flag;
489}
490
491sub dwarn {
492  my $str = shift;
493  my $opts = shift;
494  my $dlevel = shift || 1;
495  my $flag = (defined $opts ? $opts->{'debug'} >= $dlevel : 1);
496  my ($pkg, $file, $line, $sub) = caller(1);
497  my ($pkg0, $file0, $line0) = caller(0);
498  warn("${sub}():$line0: $str") if $flag;
499}
500
501sub vprint {
502  my $str = shift;
503  my $opts = shift;
504  my $flag = (defined $opts ? $opts->{'verbose'} : 1);
505  my $debug = (defined $opts ? $opts->{'debug'} : 1);
506  my ($pkg, $file, $line, $sub) = caller(1);
507  my ($pkg0, $file0, $line0) = caller(0);
508  $str = ($debug ? "${sub}():$line0: $str" : "$str");
509  print("$str") if $flag;
510}
511
512sub vwarn {
513  my $str = shift;
514  my $opts = shift;
515  my $flag = (defined $opts ? $opts->{'verbose'} : 1);
516  my ($pkg, $file, $line, $sub) = caller(1);
517  my ($pkg0, $file0, $line0) = caller(0);
518  warn("${sub}():$line0: $str") if $flag;
519}
520
521sub rsystem {
522  my $cmd = shift;
523  my $flag = shift;
524
525  # if not running as root try to use sudo
526  if ($>) {
527    $cmd = "sudo $flag $cmd";
528  } else {
529    if ($flag =~ /-b/) {
530      $cmd = "$cmd \&";
531    }
532  }
533  die("cmd failed($!): $cmd\n") if system("$cmd");
534}
535
536sub usystem {
537  my $cmd = shift;
538  my $opts = shift;
539  my $ret;
540
541  unlink $NetSNMP::Cert::OUTLOG if -e $NetSNMP::Cert::OUTLOG;
542  unlink $NetSNMP::Cert::ERRLOG if -e $NetSNMP::Cert::ERRLOG;
543
544  $ret = system("$cmd");
545
546  if ($ret) {
547    if ($opts->{'verbose'}) {
548      system("cat $NetSNMP::Cert::OUTLOG") if -r $NetSNMP::Cert::OUTLOG;
549      system("cat $NetSNMP::Cert::ERRLOG") if -r $NetSNMP::Cert::ERRLOG;
550    }
551    die("cmd failed($!): $cmd\n");
552  }
553}
554
555sub home_dir {
556  my $cdir = ::getcwd();
557
558  chdir("~");
559  my $dir = ::getcwd();
560  chdir($cdir);
561
562  return $dir;
563}
564
565sub find_bin_dir {
566  # This code finds the path to the currently invoked program and
567  my $dir;
568
569  $0 =~ m%^(([^/]*)(.*)/)([^/]+)$%;
570  if ($1) {
571    if ($2) {
572      # Invoked using a relative path.  CD there and use pwd.
573      $dir = `cd $1 && pwd`;
574      chomp $dir;
575    } else {
576      # Invoked using an absolute path; that's it!
577      $dir = $3;
578    }
579  } else {
580    # No path.  Look in PATH for the first instance.
581    foreach my $p (split /:/, $ENV{PATH}) {
582      $p ||= '.';
583      -x "$p/$4" or next;
584      $dir = $p;
585    }
586  }
587  $dir or die "Cannot locate program '$0'!";
588}
589
590sub fq_rel_path {
591  my $path = shift;
592  my $cwd = ::getcwd();
593  my $rdir = shift || $cwd;
594
595  chdir($rdir) or die("can't change directory: $rdir");
596  # get fully qualified path
597  if ($path !~ m|^/|) {
598    my $pwd = `pwd`; chomp $pwd;
599    $path = "$pwd/$path";
600    $path = ::realpath($path) if -e $path;
601  }
602  chdir($cwd) or die("can't change directory: $cwd");
603
604  return $path;
605}
606
607sub dir_empty
608  {
609    my $path = shift;
610    opendir(DIR, $path);
611    foreach (readdir(DIR)) {
612      next if /^\.\.?$/;
613      closedir(DIR);
614      return 0;
615    }
616    closedir(DIR);
617    return 1;
618  }
619
620sub make_dirs {
621  my $opts = shift;
622  my $dir = shift;
623  my $mode = shift;
624  my @dirs = @_;
625
626  my $wd = ::getcwd();
627
628  NetSNMP::Cert::dprint("make dirs [$dir:@dirs] from $wd\n", $opts);
629
630  ::mkpath($dir, $opts->{'debug'}, $mode) or die("error - can't make $dir")
631    if defined $dir and not -d $dir;
632
633  foreach my $subdir (@dirs) {
634    my $d = "$subdir";
635    $d = "$dir/$d" if defined $dir;
636    NetSNMP::Cert::dprint("making directory: $d\n", $opts) unless -d $d;
637    mkdir($d, $mode) or die("error - can't make $d") unless -d $d;
638  }
639}
640
641sub is_url {
642  my $url = shift;
643
644  return $url =~ /^(?#Protocol)(?:(?:ht|f)tp(?:s?)\:\/\/|~\/|\/)?(?#Username:Password)(?:\w+:\w+@)?(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:\/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|\/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)?$/;
645}
646
647sub in_set {
648  my $elem = shift;
649  my $set = shift;
650  for my $e (eval($set)) {
651    return 1 if $e == $elem;
652  }
653  return 0;
654}
655
656sub in_arr {
657  my $elem = shift;
658  my $arr = shift;
659  for my $e (@{$arr}) {
660    return 1 if $e eq $elem;
661  }
662  return 0;
663}
664
665sub map_bool {
666  my $val = shift;
667
668  return 1 if $val =~ /^true$/i;
669  return 0 if $val =~ /^false$/i;
670  return $val;
671}
672
673sub version {
674  my $ret = (@_ ? shift : 1);
675  print "$NetSNMP::Cert::PROG: $NetSNMP::Cert::VERSION\n";
676  exit($ret);
677}
678
679sub GetOptsFromArray {
680  my $args = shift;
681  local *ARGV;
682  @ARGV = @$args;
683  my $ret = ::GetOptions(@_);
684  @$args = @ARGV; # GetOptions strips out the ones it handles and leaves others
685  return $ret;
686}
687sub pull_cmd {
688  my $args = shift;
689  my $cmd;
690
691  foreach (@{$args}) {
692    if (/^(gence?rt|genca|gencsr|signcsr|showcas?|showce?rts?|import)$/) {
693      $cmd = $1;
694    }
695  }
696
697  @{$args} = grep {!/^$cmd$/} @{$args};
698
699  return $cmd;
700}
701
702
703sub usage {
704  my $ret = (@_ ? shift : 1);
705  my $arg = shift;
706
707  print "Command not implmeneted yet: $arg\n" if $ret == 2;
708  print "Unknown: $arg\n" if $ret == 3;
709  print "\n   NAME:\n";
710  print "     $NetSNMP::Cert::PROG: [$NetSNMP::Cert::VERSION] - ";
711  print "Net-SNMP Certificate Management Tool\n";
712  print "\n   DESCRIPTION:\n";
713  print "     net-snmp-cert creates, signs, installs and displays X.509\n";
714  print "     certificates used in the operation of Net-SNMP/(D)TLS.\n";
715  print "\n   SYNOPSIS:\n";
716  print "     net-snmp-cert [--help|-?]\n";
717  print "     net-snmp-cert [--version|-V]\n";
718  print "     net-snmp-cert genca    [<flags>] [<dn-opts>] [--with-ca <ca>]\n";
719  print "     net-snmp-cert gencert  [<flags>] [<dn-opts>] [--with-ca <ca>]\n";
720  print "     net-snmp-cert gencsr   [<flags>] [<dn-opts>] [--from-crt <crt>]\n";
721  print "     net-snmp-cert signcsr  [<flags>] [--install] --csr <csr> --with-ca <ca>\n";
722  print "     net-snmp-cert showca   [<flags>] [<format-opts>] [<file>|<search-tag>]\n";
723  print "     net-snmp-cert showcert [<flags>] [<format-opts>] [<file>|<search-tag>]\n";
724  print "     net-snmp-cert import   [<flags>] <file|url> [<key>]\n";
725  print "\n   COMMANDS:\n";
726  print "     genca    -- generate a signed CA certificate suitable for signing other\n";
727  print "                 certificates. default: self-signed unless --with-ca <ca> supplied\n\n";
728  print "     gencert  -- generate a signed certificate suitable for identification, or\n";
729  print "                 validation. default: self-signed unless --with-ca <ca> supplied\n\n";
730  print "     gencsr   -- generate a certificate signing request. will create a new\n";
731  print "                 key and certificate unless --from-crt <crt> supplied\n\n";
732  print "     signcsr  -- sign a certificate signing request specified by --csr <csr>\n";
733  print "                 with the CA certificate specified by --with-ca <ca>\n";
734  print "     import   -- import an identity or CA certificate, optionally import <key>\n";
735  print "                 if an URL is passed, will attempt to import certificate from site\n";
736  print "     showca,\n";
737  print "     showcert -- show CA or identity certificate(s). may pass fully qualified\n";
738  print "                 file or directory name, or a search-tag which prefix matches\n";
739  print "                 installed CA or identity certificate file name(s)\n";
740  print "                 see FORMAT OPTIONS to specify output format\n\n";
741  print "\n   FLAGS:\n";
742  print "     -?, --help            -- show this text and exit\n";
743  print "     -V, --version         -- show version string and exit\n";
744  print "     -D, --debug           -- raise debug level (-D -D for higher level)\n";
745  print "     -F, --force           -- force overwrite of existing output files\n";
746  print "     -I, --nointeractive   -- non-interactive run (default interactive)\n";
747  print "     -Q, --noverbose       -- non-verbose output (default verbose)\n";
748  print "     -C, --cfgdir   <dir>  -- configuration dir (see man(5) snmp_config)\n";
749  print "     -T, --tlsdir   <dir>  -- root for cert storage (default <cfgdir>/tls)\n";
750  print "     -f, --cfgfile  <file> -- config (default <cfgdir>/net-snmp-cert.conf)\n";
751  print "     -i, --identity <id>   -- identity to use from config\n";
752  print "     -t, --tag      <tag>  -- application tag (default 'snmp')\n";
753  print "     --<cfg-param>[=<val>] -- additional config params\n";
754  print "\n   CERTIFICATE OPTIONS (<cert-opts>):\n";
755  print "     -a, --with-ca <ca>    -- CA certificate used to sign request\n";
756  print "     -A, --ca <ca>         -- explicit output CA certificate\n";
757  print "     -r, --csr <csr>       -- certificate signing request\n";
758  print "     -x, --from-crt <crt>  -- input certificate for current operation\n";
759  print "     -X, --crt <crt>       -- explicit output certificate\n";
760  print "     -y, --install         -- install result in local repository\n";
761  print "\n   DISTINGUISHED NAME OPTIONS (<dn-opts>):\n";
762  print "     -e, --email <email>       -- email name\n";
763  print "     -h, --host <host>         -- DNS host name, or IP address\n";
764  print "     -d, --days <days>         -- number of days certificate is valid\n";
765  print "     -n, --cn <cn>             -- common name (CN)\n";
766  print "     -o, --org <org>           -- organiztion name\n";
767  print "     -u, --unit <unit>         -- organiztion unit name\n";
768  print "     -c, --country <country>   -- country code (e.g., US)\n";
769  print "     -p, --province <province> -- province name (synomynous w/ state)\n";
770  print "     -p, --state <state>       -- state name (synomynous w/ province)\n";
771  print "     -l, --locality <locality> -- locality name (e.g, town)\n";
772  print "     -s, --san <san>           -- subjectAltName, repeat for each <san>\n";
773  print "                               -- <san> value format (<FMT>:<VAL>):\n";
774  print "                               --   dirName:/usr/share/snmp/tls\n";
775  print "                               --   DNS:net-snmp.org\n";
776  print "                               --   email:admin\@net-snmp.org\n";
777  print "                               --   IP:192.168.1.1\n";
778  print "                               --   RID:1.1.3.6\n";
779  print "                               --   URI:http://net-snmp.org\n";
780  print "\n   FORMAT OPTIONS (<format-opts>): \n";
781  print "     --brief        -- minimized output (values only where applicable)\n";
782  print "     --text         -- full text description\n";
783  print "     --subject      -- subject description\n";
784  print "     --subject_hash -- hash of subject for indexing\n";
785  print "     --issuer       -- issuer description\n";
786  print "     --issuer_hash  -- hash of issuer for indexing\n";
787  print "     --fingerprint  -- SHA1 digest of DER\n";
788  print "     --serial       -- serial number\n";
789  print "     --modulus      -- modulus of the public key\n";
790  print "     --dates        -- start and end dates\n";
791  print "     --purpose      -- displays allowed uses\n";
792  print "     --C            -- C code description\n";
793  print "\n   EXAMPLES: \n";
794  print "     net-snmp-cert genca --cn ca-net-snmp.org --days 1000\n";
795  print "     net-snmp-cert genca -f .snmp/net-snmp-cert.conf -i nocadm -I\n";
796  print "     net-snmp-cert gencert -t snmpd --cn host.net-snmp.org\n";
797  print "     net-snmp-cert gencsr -t snmpapp\n";
798  print "     net-snmp-cert signcsr --csr snmpapp --with-ca ca-net-snmp.org\n";
799  print "     net-snmp-cert showcerts --subject --issuer --dates snmpapp\n";
800  print "     net-snmp-cert showcas --fingerprint ca-net-snmp.org --brief\n";
801  print "     net-snmp-cert import ca-third-party.crt\n";
802  print "     net-snmp-cert import signed-request.crt signed-request.key\n\n";
803
804  exit $ret;
805}
806
807sub set_default {
808  my $opts = shift;
809  my $config = shift;
810  my $field = shift;
811  my $val = shift;
812
813  if (not defined $opts->{$field}) {
814    my $cval = $config->inherit($field);
815    $opts->{$field} = (defined $cval ? $cval : $val);
816  }
817}
818
819sub cfg_path {
820  my $path;
821
822  $path = `$NetSNMP::Cert::CFGTOOL --snmpconfpath`;
823  chomp $path;
824  return (wantarray ? split(':', $path) : $path);
825}
826
827sub find_cfgfile {
828  my $dir = shift;
829  my $file = shift;
830
831  if (defined $dir and -d $dir and
832      defined $file and $file !~ /^[\.\/]/) {
833    return fq_rel_path("$dir/$file") if -f "$dir/$file";
834  }
835
836  if (defined $file) {
837    if (-f $file) {
838      return fq_rel_path($file);
839    } else {
840      return $file; # file is not found, complain later
841    }
842  }
843
844  my @path = cfg_path();
845  unshift(@path, $dir) if defined $dir;
846  while (@path) {
847    my $p = pop(@path);
848    next if $p eq '/var/lib/snmp';
849    if (-r "$p/$NetSNMP::Cert::CFGFILE") {
850      return fq_rel_path("$p/$NetSNMP::Cert::CFGFILE");
851    }
852  }
853
854  return $file;
855}
856
857sub find_cfgdir {
858  my $dir = shift;
859  my $file = shift;
860
861  if (defined $dir) {
862    $dir = NetSNMP::Cert::fq_rel_path($dir);
863    return $dir;
864  }
865
866  if (defined $file and -f $file) {
867    $dir = ::dirname($file);
868    return NetSNMP::Cert::fq_rel_path($dir);
869  } else {
870    my @path = cfg_path();
871    # search first for writeable tls dir
872    # for root search top down, for user bottom up
873    while (@path) {
874      $dir = ($> ? pop(@path): shift(@path));
875      next if $dir eq '/var/lib/snmp';
876      return $dir if -d "$dir/$NetSNMP::Cert::TLSDIR" and -w "$dir/$NetSNMP::Cert::TLSDIR";
877    }
878    @path = cfg_path();
879    # if no tls dir found, pick first writable config dir
880    # for root search top down, for user bottom up
881    while (@path) {
882      $dir = ($> ? pop(@path): shift(@path));
883      next if $dir eq '/var/lib/snmp';
884      return $dir if -d $dir and -w $dir;
885    }
886    my $home = $ENV{HOME} || die "Unable to determine home directory: set \$HOME\n";
887    return ($> ? "$home/.snmp" : "/usr/share/snmp"); # XXX hack - no dirs existed or were writable
888  }
889  # this should never happen
890  return undef;
891}
892
893sub find_certs {
894  my $opts = shift;
895  my $dir = shift || 'certs';
896  my $targ = shift;
897  my $suffix_regex;
898  my $cwd = ::getcwd();
899
900  if ($dir eq 'csrs') {
901    $dir = $NetSNMP::Cert::NEWCRTDIR;
902    $suffix_regex = '\.csr|\.pem';
903  } else {
904    $suffix_regex = '\.crt|\.pem';
905  }
906
907  NetSNMP::Cert::dprint("find_certs(1:$cwd):$dir:$targ:$suffix_regex\n", $opts);
908
909  if ($targ =~ /\.?\//) {
910    # see if targ could be file - calc orig. rel. path
911    my $arg = NetSNMP::Cert::fq_rel_path($targ, $opts->{'rdir'});
912    NetSNMP::Cert::dprint("find_certs(2):$dir:$targ:$arg\n", $opts);
913    # targ is a file name - use it
914    return (wantarray ? ($arg) : $arg) if -f $arg;
915    # else mark as dir if it is
916    $targ = "$arg" if -d $arg;
917  }
918
919  $targ =~ s/\/*$/\// if -d $targ;
920
921  NetSNMP::Cert::dprint("find_certs(3):$dir:$targ\n", $opts);
922
923  # find certs in targ if a dir (in tlsdir, or relative)
924  $dir = $1 if $targ =~ s/^(\S+)\/([^\/\s]*)$/$2/ and -d $1;
925
926  NetSNMP::Cert::dprint("find_certs(4):${dir}:$targ:$cwd\n", $opts);
927
928  my @certs;
929  my $glob = "$dir/$targ";
930  foreach (<$dir/*>) {
931    NetSNMP::Cert::dprint("checking($dir:$targ): $_\n", $opts);
932    next unless /^$dir\/$targ(.*$suffix_regex)?$/;
933    # return exact match if not wantarray()
934    return $_ if (not wantarray()) and /^$dir\/$targ($suffix_regex)?$/;
935    NetSNMP::Cert::dprint("pushing: $_\n", $opts);
936    push(@certs, $_);
937  }
938
939  return (wantarray ? @certs : $certs[0]);
940}
941
942sub check_output_file {
943  my $opts = shift;
944  my $file = shift;
945  my $interactive = shift;
946  my $force = shift;
947  my $continue = 1;
948
949  if (-w $file) {
950    if ($interactive and not $force) {
951      print "Output file ($file) exists; will be overwritten\nContinue [y/N]: ";
952      $continue = <STDIN>; chomp $continue;
953      $continue = 0 if $continue =~ /^\s*$|^no?\s*$/i;
954    } else {
955      if ($force) {
956	NetSNMP::Cert::vprint("Output file ($file) exists; overwriting...\n", $opts);
957      } else {
958	NetSNMP::Cert::vprint("Output file ($file) exists; exiting...\n", $opts);
959	$continue = 0;
960      }
961    }
962  } elsif (-e $file) {
963    NetSNMP::Cert::vprint("Output file ($file) not writable; exiting...\n", $opts);
964    $continue = 0;
965  }
966  exit(1) unless $continue;
967}
968
969sub is_server {
970  my $tag = shift;
971  return 1 if $tag eq 'snmpd' or $tag eq 'snmptrapd';
972  return 0;
973}
974
975sub is_ca_cert {
976  my $crt = shift;
977  my $output = `openssl x509 -in '$crt' -text -noout`;
978  return ($output =~ /^\s*CA:TRUE\s*$/m ? 1 : 0);
979}
980
981sub x509_format {
982  my $opts = shift;
983  my $fmt;
984
985  foreach my $f (@NetSNMP::Cert::X509FMTS) {
986    $fmt .= " -$f" if defined $opts->{$f};
987  }
988
989  return $fmt;
990}
991
992sub make_openssl_conf {
993  my $file = shift;
994  return if -r $file;
995
996  open(F, ">$file") or die("could not open $file");
997
998  print F <<'END';
999#
1000# Net-SNMP (D)TLS OpenSSL configuration file.
1001#
1002
1003rdir            = .
1004dir		= $ENV::DIR
1005RANDFILE	= $rdir/.rand
1006MD		= sha1
1007KSIZE		= 2048
1008CN		= net-snmp.org
1009EMAIL		= admin@net-snmp.org
1010COUNTRY	        = US
1011STATE		= CA
1012LOCALITY	= Davis
1013ORG		= Net-SNMP
1014ORG_UNIT	= Development
1015SAN             = email:copy
1016DAYS            = 365
1017CRLDAYS         = 30
1018
1019default_days	= $ENV::DAYS		# how long to certify for
1020default_crl_days= $ENV::CRLDAYS		# how long before next CRL
1021default_md	= $ENV::MD		# which md to use.
1022
1023database	= $dir/.index		# database index file.
1024serial		= $dir/.serial 		# The current serial number
1025certs		= $rdir/certs		# identity certs
1026new_certs_dir	= $dir/newcerts	# default place for new certs.
1027ca_certs_dir	= $rdir/ca-certs	# default place for new certs.
1028key_dir		= $rdir/private
1029
1030crl_dir		= $dir/crl		# crl's
1031crlnumber	= $dir/.crlnumber	# the current crl number
1032					# must be commented out to leave V1 CRL
1033crl		= $crl_dir/crl.pem	# The current CRL
1034
1035preserve	= no			# keep passed DN ordering
1036unique_subject	= yes			# Set to 'no' to allow creation of
1037					# certificates with same subject.
1038# Extra OBJECT IDENTIFIER info:
1039oid_section		= new_oids
1040
1041[ new_oids ]
1042
1043# Add new OIDs in here for use by 'ca' and 'req'.
1044# Add a simple OID like this:
1045# testoid1=1.2.3.4
1046# Use config file substitution like this:
1047# testoid2=${testoid1}.5.6
1048
1049####################################################################
1050[ ca ]
1051default_ca	= CA_default		# The default ca section
1052
1053####################################################################
1054[ CA_default ]
1055# certificate	= $ca_certs_dir/$ENV::TAG.crt 	# CA certificate so sign with
1056name_opt 	= ca_default		# Subject Name options
1057cert_opt 	= ca_default		# Certificate field options
1058policy		= policy_match
1059copy_extensions = copy                  # copy v3 extensions (subjectAltName)
1060subjectAltName  = copy
1061
1062# For the CA policy
1063[ policy_match ]
1064countryName		= match
1065stateOrProvinceName	= match
1066organizationName	= match
1067organizationalUnitName	= optional
1068commonName		= supplied
1069emailAddress		= optional
1070
1071# For the 'anything' policy
1072# At this point in time, you must list all acceptable 'object'
1073# types.
1074[ policy_anything ]
1075countryName		= optional
1076stateOrProvinceName	= optional
1077localityName		= optional
1078organizationName	= optional
1079organizationalUnitName	= optional
1080commonName		= supplied
1081emailAddress		= optional
1082
1083####################################################################
1084[ req ]
1085default_bits		= $ENV::KSIZE
1086default_md		= $ENV::MD
1087distinguished_name	= req_distinguished_name
1088string_mask 		= MASK:0x2002
1089req_extensions          = v3_req
1090x509_extensions         = v3_user_create
1091
1092[ req_distinguished_name ]
1093countryName			= Country Name (2 letter code)
1094countryName_default		= $ENV::COUNTRY
1095countryName_min			= 2
1096countryName_max			= 2
1097
1098stateOrProvinceName		= State or Province Name (full name)
1099stateOrProvinceName_default	= $ENV::STATE
1100
1101localityName			= Locality Name (eg, city)
1102localityName_default		= $ENV::LOCALITY
1103
11040.organizationName		= Organization Name (eg, company)
11050.organizationName_default	= $ENV::ORG
1106
1107organizationalUnitName		= Organizational Unit Name (eg, section)
1108organizationalUnitName_default	= $ENV::ORG_UNIT
1109
1110commonName			= Common Name (eg, your name or your server\'s hostname)
1111commonName_max			= 64
1112commonName_default              = $ENV::CN
1113
1114emailAddress			= Email Address
1115emailAddress_max		= 64
1116emailAddress_default            = $ENV::EMAIL
1117
1118[ v3_req ]
1119
1120# Extensions to add to a certificate request
1121basicConstraints = CA:FALSE
1122
1123# Import the email address and/or specified SANs.
1124%[subjectAltName = $ENV::SAN]
1125
1126[ v3_user_create ]
1127
1128# Extensions to add to a certificate request
1129basicConstraints = CA:FALSE
1130#keyUsage = nonRepudiation, digitalSignature, keyEncipherment
1131
1132# PKIX recommendation.
1133subjectKeyIdentifier = hash
1134authorityKeyIdentifier = keyid:always,issuer:always
1135
1136# Import the email address and/or specified SANs.
1137%[subjectAltName = $ENV::SAN]
1138
1139[ v3_ca_create ]
1140# Extensions to add to a CA certificate
1141basicConstraints = CA:TRUE
1142# This will be displayed in Netscape's comment listbox.
1143nsComment = "OpenSSL Generated Certificate (net-snmp)"
1144
1145# PKIX recommendation.
1146subjectKeyIdentifier = hash
1147authorityKeyIdentifier = keyid:always,issuer:always
1148
1149# Import the email address and/or specified SANs.
1150%[subjectAltName = $ENV::SAN]
1151
1152[ v3_ca_sign ]
1153# Extensions to add when 'ca' signs a request.
1154basicConstraints = CA:FALSE
1155# This will be displayed in Netscape's comment listbox.
1156nsComment = "OpenSSL Generated Certificate (net-snmp)"
1157
1158keyUsage = nonRepudiation, digitalSignature, keyEncipherment
1159
1160# PKIX recommendations harmless if included in all certificates.
1161subjectKeyIdentifier = hash
1162authorityKeyIdentifier = keyid:always,issuer:always
1163
1164[ v3_ca_sign_ca ]
1165# Extensions to add when 'ca' signs a ca request.
1166basicConstraints = CA:TRUE
1167
1168# This will be displayed in Netscape's comment listbox.
1169nsComment = "OpenSSL Generated Certificate (net-snmp)"
1170
1171# PKIX recommendations harmless if included in all certificates.
1172subjectKeyIdentifier = hash
1173authorityKeyIdentifier = keyid:always,issuer:always
1174
1175END
1176
1177}
1178
1179
1180package NetSNMP::Cert::Obj;
1181
1182sub new {
1183  my $class = shift;
1184  my $cfield = shift;
1185  my $parent = shift;
1186  my $ind = shift;
1187  my $this = {};
1188  bless($this, $class);
1189
1190  # initialize hash of keys which are dynamically created or internal
1191  $this->{'AUTOKEYS'}{'AUTOKEYS'}++;
1192
1193  # store a reference to ourselves so our children can find us
1194  $this->autoSet('CFIELD', $cfield);
1195  $this->autoSet($cfield , $this);
1196  $this->autoSet('CHILDREN', []);
1197  $this->autoSet('INDEX', $ind) if defined $ind;
1198
1199  if (defined $parent) {
1200    # cache 'APP' in all objs for easy reference
1201    $this->autoSet('APP', $parent->inherit('APP'));
1202    my $app = $this->{'APP'};
1203
1204    die("net-snmp-cert: error: no NetSNMP::Cert::App context provided")
1205      if not defined $app or not ref $app eq 'NetSNMP::Cert::App';
1206    # save children for list traversal
1207    push(@{$parent->{'CHILDREN'}}, $this) unless $parent eq $this;
1208  } else {
1209    # we are the top of the list
1210    $parent = $this;
1211  }
1212  $this->autoSet('PARENT' , $parent);
1213
1214  return $this;
1215}
1216
1217sub autoSet {
1218  my $this = shift;
1219  my $key = shift;
1220  if (@_) {
1221    my $val = shift;
1222    $this->{'AUTOKEYS'}{$key}++;
1223    $this->{$key} = $val;
1224  }
1225  return exists $this->{'AUTOKEYS'}{$key};
1226}
1227
1228sub inherit {
1229  my $this = shift;
1230  my $field = shift;
1231  my $id;
1232
1233  # cmd opts override config settings
1234  if (exists $this->{'APP'} and exists $this->{'APP'}{'OPTS'}) {
1235    my $opts = $this->{'APP'}{'OPTS'};
1236    $id = $opts->{'identity'};
1237    return $opts->{$field} if defined $opts->{$field};
1238  }
1239
1240  if (defined $id and exists $this->{'identity'} and
1241      exists $this->{'identity'}{$id} and
1242      exists $this->{'identity'}{$id}{$field}) {
1243    return $this->{'identity'}{$id}{$field};
1244  }
1245
1246  # return our field if we have it
1247  return $this->{$field} if defined $this->{$field};
1248
1249  # terminate recursion at top and return undef if not found
1250  return undef if not defined $this->{'PARENT'} or $this->{'PARENT'} eq $this;
1251
1252  # recurse to parent
1253  $this->{'PARENT'}->inherit($field);
1254}
1255
1256sub resolve {
1257  my $this = shift;
1258  my $opts = $this->inherit('OPTS');
1259  my $val = shift;
1260
1261  NetSNMP::Cert::dprint("resolving: $val\n", $opts);
1262
1263  $val =~ s/(\$(\w+))/$this->inherit($2) or die("unresolved reference in config: $1")/ge;
1264  $val =~ s/(\&\{(.*?)\})/$2/gee;
1265
1266  NetSNMP::Cert::dprint("resolved: $val\n", $opts);
1267
1268  return $val
1269}
1270
1271sub delete {
1272  my $this = shift;
1273  my $opts = $this->inherit('OPTS');
1274
1275  NetSNMP::Cert::dprint("Obj::delete: ($this) [$this->{CFIELD}]\n", $opts);
1276
1277  my $parent = $this->{'PARENT'};
1278  my @children = @{$this->{'CHILDREN'}};
1279
1280  foreach my $child (@children) {
1281    $child->delete();
1282  }
1283
1284  NetSNMP::Cert::dwarn("Obj: children not freed\n", $opts) if @{$this->{'CHILDREN'}};
1285
1286  # delete all our self-references
1287  delete($this->{$this->{'CFIELD'}}) if exists $this->{'CFIELD'};
1288  delete($this->{'PARENT'});
1289  delete($this->{'CHILDREN'});
1290
1291  $parent->disown($this) if defined $parent and ref($parent) =~ /NetSNMP::Cert/;
1292}
1293
1294sub disown {
1295  my $this = shift;
1296  my $thechild = shift;
1297  my $opts = $this->inherit('OPTS');
1298  my $ind = 0;
1299
1300  NetSNMP::Cert::dprint("Obj::disown: ($this) [$this->{CFIELD}] disowning ($thechild) [$thechild->{CFIELD}]\n", $opts);
1301
1302  foreach my $child (@{$this->{'CHILDREN'}}) {
1303    last if $child eq $thechild;
1304    $ind++;
1305  }
1306  if ($ind < @{$this->{'CHILDREN'}}) {
1307    splice(@{$this->{'CHILDREN'}}, $ind, 1);
1308  } else {
1309    NetSNMP::Cert::dwarn("Child ($thechild) not found in object ($this)\n", $opts);
1310  }
1311}
1312
1313sub disabled {
1314  my $this = shift;
1315  return (exists $this->{'disable'} ? $this->{'disable'} : 0);
1316}
1317
1318my %cfg = (
1319	   'name' => 1,
1320	   'type' => 2,
1321	   'id' => 6,
1322	  );
1323
1324sub cfgsort {
1325  my $self = shift;
1326  my $a = shift;
1327  my $b = shift;
1328  return -1 if exists $cfg{$a} and not exists $cfg{$b};
1329  return 1 if exists $cfg{$b} and not exists $cfg{$a};
1330  return $cfg{$a} <=> $cfg{$b} if exists $cfg{$a} and exists $cfg{$b};
1331  return -1 if !ref($self->{$a}) and ref($self->{$b});
1332  return 1 if !ref($self->{$b}) and ref($self->{$a});
1333  return -1 if ref($self->{$a}) =~ /NetSNMP::Cert/ and ref($self->{$b}) !~ /NetSNMP::Cert/;
1334  return 1 if ref($self->{$b}) =~ /NetSNMP::Cert/ and ref($self->{$a}) !~ /NetSNMP::Cert/;
1335  return 0;
1336}
1337
1338sub dump {
1339  my $self = shift;
1340  my $indent = shift;
1341  my $self_str = $self->{'CFIELD'};
1342  my @data_keys =  grep {!$self->autoSet($_)} keys %$self;
1343  my @lines;
1344
1345  push(@lines, "\n" . ' ' x $indent . "$self_str = {\n") if defined $indent;
1346
1347  {
1348    my $indent = (defined $indent ? $indent + 3 : 0);
1349    foreach my $key (sort {cfgsort($self,$NetSNMP::Cert::Obj::a,
1350				   $NetSNMP::Cert::Obj::b)} (sort @data_keys)) {
1351      if (ref($self->{$key}) =~ /NetSNMP::Cert/) {
1352	push(@lines, $self->{$key}->dump($indent));
1353      } elsif (ref($self->{$key}) =~ /ARRAY/) {
1354	push(@lines, "\n") if ref(${$self->{$key}}[0]) !~ /NetSNMP::Cert/;
1355	foreach my $elem (@{$self->{$key}}) {
1356	  if (ref($elem) =~ /NetSNMP::Cert/) {
1357	    push(@lines, $elem->dump($indent));
1358	  } else {
1359	    push(@lines, ' ' x $indent . "$key = $elem\n");
1360	  }
1361	}
1362      } else {
1363	my $str = $key . (defined $self->{$key} ? " = $self->{$key}\n" : "\n");
1364	push(@lines, ' ' x $indent . $str);
1365      }
1366    }
1367  }
1368
1369  push(@lines, ' ' x $indent . "}; # end $self_str\n") if defined $indent;
1370  return @lines;
1371}
1372
1373sub DESTROY {
1374  my $this = shift;
1375
1376  print("Obj::DESTROY $this [", ref $this, "]\n") if $NetSNMP::Cert::DEBUG >= 3;
1377}
1378
1379package NetSNMP::Cert::App;
1380use vars qw(@ISA);
1381
1382@ISA = qw(NetSNMP::Cert::Obj);
1383
1384sub new {
1385  my $class = shift;
1386
1387  # the app is god, it is its own parent
1388  my $this = $class->SUPER::new('APP');
1389
1390  bless($this, $class);
1391
1392  # verify required tools or die
1393  $this->checkReqs();
1394
1395  # internal intitialization
1396  $this->initOpts();
1397
1398  # make a new empty config and init (not parsed)
1399  $this->{'config'} = new NetSNMP::Cert::Config($this);
1400
1401  return $this;
1402}
1403
1404sub checkReqs {
1405  my $app = shift;
1406
1407  die("$NetSNMP::Cert::OPENSSL does not exist or is not executable")
1408    if system("$NetSNMP::Cert::OPENSSL version > /dev/null 2>&1");
1409
1410  my $ossl_ver = `$NetSNMP::Cert::OPENSSL version`; chomp $ossl_ver;
1411  $ossl_ver =~ s/^OpenSSL\s+([\d\.]+).*$/$1/;
1412  my $ossl_min_ver = $NetSNMP::Cert::OPENSSL_MIN_VER;
1413
1414  die("$NetSNMP::Cert::OPENSSL (v$ossl_ver): must be $ossl_min_ver or later")
1415    if ($ossl_ver cmp $ossl_min_ver) < 0;
1416
1417  die("$NetSNMP::Cert::CFGTOOL not found: please install")
1418    if system("$NetSNMP::Cert::CFGTOOL > /dev/null 2>&1");
1419}
1420
1421sub initOpts {
1422  my $app = shift;
1423  my $opts = {};
1424  $app->autoSet('OPTS', $opts);
1425
1426  # Define directories we need.
1427  $opts->{'bindir'} = NetSNMP::Cert::find_bin_dir();
1428
1429  $opts->{'out'} = "> $NetSNMP::Cert::OUTLOG";
1430  $opts->{'err'} = "2> $NetSNMP::Cert::ERRLOG";
1431
1432  # set up paths for app install and runtime env
1433  $ENV{PATH} = "/sbin:/usr/sbin:$ENV{PATH}";
1434  $ENV{PATH} = "$opts->{'bindir'}:$ENV{PATH}";
1435
1436  # default all privs to -rw-------
1437  umask(077);
1438}
1439
1440sub init {
1441  my $app = shift;
1442  my $opts = $app->{'OPTS'};
1443  my $config = $app->{'config'};
1444  my @args = @_;   # pass external args (typically ARGV)
1445
1446  # parse command line
1447  $app->parseArgs(@args);
1448
1449  # lazy config parsing postponed until here, will not reparse
1450  $config->parse();
1451
1452  # set defaults here if not already set in cmdline or config
1453  # guided interactive mode by default
1454  NetSNMP::Cert::set_default($opts, $config, 'interactive', 1);
1455  # verbose output
1456  NetSNMP::Cert::set_default($opts, $config, 'verbose', 1);
1457
1458  # find tlsdir/subdirs or make it
1459  $opts->{'tlsdir'} = $app->createTlsDir();
1460}
1461
1462sub parseArgs {
1463  my $app = shift;
1464  my $opts = $app->{'OPTS'};
1465  my @args = @_;
1466
1467  NetSNMP::Cert::GetOptsFromArray(\@args, $opts, 'help|?', 'version|V',
1468		      'interactive!', 'I', 'verbose!', 'Q', 'force|F',
1469		      'cfgfile|f=s', 'cfgdir|C=s', 'tlsdir|tlsDir|T=s',
1470		      'tag|t=s', 'identity|i=s', 'debug|D+',);
1471
1472  NetSNMP::Cert::version(0) if $opts->{'version'};
1473
1474  NetSNMP::Cert::usage(0) if $opts->{'help'};
1475
1476  # pull out the cmd - getOpts should leave it untouched for us
1477  $opts->{'cmd'} = NetSNMP::Cert::pull_cmd(\@args);
1478
1479  # save extra args for command specific processing
1480  $opts->{'cmdargs'} = [@args];
1481
1482  # Check debug option first
1483  $NetSNMP::Cert::DEBUG = $opts->{'debug'};
1484  $opts->{'err'} = $opts->{'out'} = "" if $opts->{'debug'};
1485  $opts->{'interactive'} = not $opts->{'I'} if defined $opts->{'I'};
1486  $opts->{'verbose'} = not $opts->{'Q'} if defined $opts->{'Q'};
1487
1488  # search for cfgdir and cfgfile based on opts and confpath
1489  $opts->{'cfgdir'} = NetSNMP::Cert::find_cfgdir($opts->{'cfgdir'},
1490						 $opts->{'cfgfile'});
1491  $opts->{'cfgfile'} = NetSNMP::Cert::find_cfgfile($opts->{'cfgdir'},
1492						   $opts->{'cfgfile'});
1493}
1494
1495sub createTlsDir {
1496  my $app = shift;
1497  my $opts = $app->{'OPTS'};
1498  my $config = $app->{'config'};
1499  my $dir;
1500  my $file;
1501  my $cmd;
1502
1503  $dir = $config->inherit('tlsDir');
1504
1505  if (not defined $dir) {
1506    my $cfgdir = $opts->{'cfgdir'};
1507    die("undefined cfgdir: unable to creat tlsdir: exiting...\n") unless defined $cfgdir;
1508    $dir = "$cfgdir/$NetSNMP::Cert::TLSDIR";
1509  }
1510
1511  NetSNMP::Cert::dprint("tlsDir is: $dir\n", $opts);
1512  $dir = NetSNMP::Cert::fq_rel_path($dir);
1513  NetSNMP::Cert::dprint("tlsDir is: $dir\n", $opts);
1514
1515  NetSNMP::Cert::dprint("tlsDir not found, creating\n", $opts) unless -d $dir;
1516  NetSNMP::Cert::make_dirs($opts, $dir, 0700, @NetSNMP::Cert::TLSDIRS);
1517
1518  my $ssl_cfg_in = NetSNMP::Cert::fq_rel_path($NetSNMP::Cert::SSLCFGIN,$dir);
1519
1520  # Existing openssl.conf tmpl will be preserved
1521  if (-f $ssl_cfg_in) {
1522    NetSNMP::Cert::dwarn("OpenSSL template exists ($ssl_cfg_in): preserving...", $opts);
1523  }
1524
1525  if (not -f $ssl_cfg_in) {
1526    NetSNMP::Cert::dprint("make_openssl_conf($ssl_cfg_in)", $opts);
1527    NetSNMP::Cert::make_openssl_conf($ssl_cfg_in);
1528    chmod(0600, $ssl_cfg_in);
1529  }
1530
1531  NetSNMP::Cert::dprint("createTlsDir: done\n", $opts);
1532  return $dir;
1533}
1534
1535my @interactive_ops = (['gencert', "Generate a signed certificate"],
1536	   ['genca', "Generate a CA certificate"],
1537	   ['gencsr', "Generate a Certificate Signing Request"],
1538	   ['signcsr', "Sign a Certificate Signing Request"],
1539	  );
1540
1541sub run {
1542  my $app = shift;
1543  my $opts = $app->{'OPTS'};
1544  my $cmd = $opts->{'cmd'};
1545
1546  # must have a command in non-Interactive mode
1547  NetSNMP::Cert::usage(3) if !$opts->{'interactive'} and !$opts->{'cmd'};
1548
1549  #  change dir tls dir - the cwd for all commands - save cwd first
1550  $opts->{'rdir'} = ::getcwd();
1551  chdir($opts->{'tlsdir'}) or die("could'nt change directory: $opts->{tlsdir}");
1552  # display context
1553  NetSNMP::Cert::dprint("PATH: $ENV{PATH}\n\n", $opts);
1554  NetSNMP::Cert::dprint("config file: $opts->{cfgfile}\n", $opts);
1555  NetSNMP::Cert::dprint("config dir:  $opts->{cfgdir}\n", $opts);
1556  NetSNMP::Cert::dprint("tls dir:     $opts->{tlsdir}\n", $opts);
1557
1558  my $cmdstr = join(' ', $cmd, @{$opts->{'cmdargs'}});
1559  NetSNMP::Cert::dprint("command:     $cmdstr\n", $opts);
1560
1561  NetSNMP::Cert::GetOptsFromArray(\@{$opts->{'cmdargs'}}, $opts,
1562				  'with-ca|a=s', 'ca|A=s','csr|r=s',
1563				  'from-crt|x=s', 'crt|X=s', 'days|d=s',
1564				  'cn|n=s', 'email|e=s', 'host|h=s',
1565				  'san|s=s@', 'org|o=s', 'unit|u=s',
1566				  'country|c=s', 'state|province|p=s',
1567				  'locality|l=s', 'brief|b',
1568				  @NetSNMP::Cert::X509FMTS);
1569
1570  # process extra args; --<cfg-name>[=<val>]
1571  $app->processExtraArgs();
1572
1573  # If in interactive mode - fill missing info by interviewing user
1574  if (not defined $cmd or grep {/$cmd/} map {$_->[0]} @interactive_ops) {
1575    $app->userInput() if $opts->{interactive};
1576    $cmd = $opts->{'cmd'}; # may have changed
1577  }
1578
1579  # resolve input args to filenames
1580  $app->resolveCrtArgs();
1581
1582  # use env. or merge template for OpenSSL config
1583  $NetSNMP::Cert::SSLCFG ||= $app->opensslCfg();
1584
1585  if ($cmd =~ /^genca$/) {
1586    NetSNMP::Cert::usage(1) if defined $opts->{'from-crt'} or defined $opts->{'csr'};
1587    $app->genCa($opts->{'with-ca'});
1588  } elsif ($cmd =~ /^gence?rt$/) {
1589    NetSNMP::Cert::usage(1) if defined $opts->{'from-crt'} or defined $opts->{'csr'};
1590    $app->genCert($opts->{'with-ca'});
1591  } elsif ($cmd =~ /^gencsr$/) {
1592    NetSNMP::Cert::usage(1) if defined $opts->{'with-ca'} or defined $opts->{'crt'};
1593    $app->genCsr();
1594  } elsif ($cmd =~ /^signcsr$/) {
1595    NetSNMP::Cert::usage(1) unless defined $opts->{'with-ca'} and defined $opts->{'csr'};
1596    $app->signCsr();
1597  } elsif ($cmd =~ /^show(ce?rts?)?$/) {
1598    $app->show('certs');
1599  } elsif ($cmd =~ /^showcas?$/) {
1600    $app->show('ca-certs');
1601  } elsif ($cmd =~ /^import$/) {
1602    $app->import();
1603  } else {
1604    NetSNMP::Cert::usage();
1605  }
1606}
1607
1608sub processExtraArgs {
1609  my $app = shift;
1610  my $opts = $app->{'OPTS'};
1611  my @args;
1612
1613  NetSNMP::Cert::dprint("processing extra args...\n", $opts);
1614
1615  while (@{$opts->{'cmdargs'}}) {
1616    my $arg = shift(@{$opts->{'cmdargs'}});
1617    NetSNMP::Cert::dprint("found: arg --> $arg\n", $opts);
1618    if ($arg =~ /^-+(\w+)(?:=(.*?))?\s*$/) {
1619      NetSNMP::Cert::dprint("found: arg($1) val($2)\n", $opts);
1620      if (exists $opts->{$1}) {
1621   	NetSNMP::Cert::vwarn("option ($1) already set: overwriting\n", $opts);
1622      }
1623      $opts->{$1} = (defined $2 ? $2 : 1);
1624    } else {
1625      push(@args, $arg);
1626    }
1627  }
1628  @{$opts->{'cmdargs'}} = @args;
1629}
1630
1631sub resolveCrtArgs {
1632  my $app = shift;
1633  my $opts = $app->{'OPTS'};
1634
1635  # find ca, crt, csr args if present and return fully qualified path
1636  if (defined $opts->{'with-ca'}) {
1637    my $ca;
1638    $ca = NetSNMP::Cert::find_certs($opts, 'ca-certs', $opts->{'with-ca'});
1639    die("unable to locate CA certificate ($opts->{'with-ca'})\n") unless -e $ca;
1640    die("unable read CA certificate ($opts->{'with-ca'})\n") unless -r $ca;
1641    $opts->{'with-ca'} = $ca;
1642  }
1643
1644  if (defined $opts->{'from-crt'}) {
1645    my $crt;
1646    $crt = NetSNMP::Cert::find_certs($opts, 'certs', $opts->{'from-crt'});
1647    die("unable to locate certificate ($opts->{'from-crt'})\n") unless -e $crt;
1648    die("unable read certificate ($opts->{'from-crt'})\n") unless -r $crt;
1649    $opts->{'from-crt'} = $crt;
1650  }
1651
1652  if (defined $opts->{'csr'}) {
1653    my $csr;
1654    $csr = NetSNMP::Cert::find_certs($opts, 'csrs', $opts->{'csr'});
1655    die("unable to locate CSR certificate ($opts->{csr})\n") unless -e $csr;
1656    die("unable read CSR certificate ($opts->{csr})\n") unless -r $csr;
1657    $opts->{'csr'} = $csr;
1658  }
1659}
1660
1661sub opensslCfg {
1662  my $app = shift;
1663  my $config = $app->{'config'};
1664  my $opts = $app->{'OPTS'};
1665  my $san = $config->inherit('san') || $config->inherit('subjectAltName');
1666  my $ssl_cfg_in = NetSNMP::Cert::fq_rel_path($NetSNMP::Cert::SSLCFGIN);
1667  my $ssl_cfg_out = NetSNMP::Cert::fq_rel_path($NetSNMP::Cert::SSLCFGOUT);
1668
1669  if (not -f $ssl_cfg_in) {
1670    NetSNMP::Cert::vwarn("OpenSSL template not found: $ssl_cfg_in\n", $opts);
1671    die("no OpenSSL template");
1672  }
1673
1674  open(IN, $ssl_cfg_in) or die("unable to open OpenSSL template: $ssl_cfg_in\n");
1675  open(OUT, ">$ssl_cfg_out") or die("unable to open OpenSSL config: $ssl_cfg_out\n");
1676
1677  print OUT "#######################################################\n";
1678  print OUT "#####    Warning: Do Not Edit - Generated File    #####\n";
1679  print OUT "#######################################################\n";
1680  while (<IN>) {
1681    if ($san) {
1682      s/\%\[([^\]]*?)\]/$1/;
1683    } else {
1684      s/\%\[([^\]]*?)\]//;
1685    }
1686    print OUT $_;
1687  }
1688  close(IN);
1689  close(OUT);
1690
1691  return $ssl_cfg_out;
1692}
1693
1694sub opensslEnv {
1695  my $app = shift;
1696  my $config = $app->{'config'};
1697  my $opts = $app->{'OPTS'};
1698
1699  my $cn = shift;
1700  my $days = shift;
1701  my $dir = shift || ".";
1702  # XXX - need to handle config'd identity here
1703  my $name = $config->inherit("name");
1704  my $host = $config->inherit("host");
1705  my $email = $config->inherit("email");
1706  my $country = $config->inherit("country");
1707  my $state = $config->inherit("state");
1708  my $locality = $config->inherit("locality");
1709  my $org = $config->inherit("org");
1710  my $org_unit = $config->inherit("unit") || $config->inherit("orgUnit");
1711  my $san;
1712  my $san_arr_ref;
1713  my $md = $config->inherit("msgDigest");
1714  my $ksize = $config->inherit("keySize");
1715
1716  my $env;
1717
1718  $env .= " KSIZE=$ksize" if defined $ksize;
1719  $env .= " DAYS=$days" if defined $days;
1720  $env .= " MD=$md" if defined $md;
1721
1722  $env .= " DIR='$dir'" if defined $dir;
1723
1724  $env .= " EMAIL=$email" if defined $email;
1725  $env .= " CN='$cn'" if defined $cn;
1726  $env .= " ORG='$org'" if defined $org;
1727  $env .= " ORG_UNIT='$org_unit'" if defined $org_unit;
1728  $env .= " COUNTRY=$country" if defined $country;
1729  $env .= " STATE=$state" if defined $state;
1730  $env .= " LOCALITY=$locality" if defined $locality;
1731
1732  $san_arr_ref = $config->inherit('subjectAltName');
1733  $san = join('\,\ ', @{$san_arr_ref}) if ref $san_arr_ref;
1734  $san_arr_ref = $config->inherit('san');
1735  $san .= join('\,\ ', @{$san_arr_ref}) if ref $san_arr_ref;
1736  $san =~ s/EMAIL:/email:/g;
1737  $env .= " SAN=$san" if defined $san;
1738
1739  NetSNMP::Cert::dprint("opensslEnv: $env\n", $opts);
1740
1741  return $env;
1742}
1743our @san_prefix = (['dirName:', "e.g., dirName:/usr/share/snmp/tls"],
1744		   ['DNS:', "e.g., DNS:test.net-snmp.org)"],
1745		   ['email:', "e.g., email:admin\@net-snmp.org"],
1746		   ['IP:', "e.g., IP:192.168.1.1"],
1747		   ['RID:', "e.g., RID:1.1.3.6.20"],
1748		   ['URI:', "e.g., URI:http://www.net-snmp.org"]);
1749
1750sub userInputDN {
1751  my $app = shift;
1752  my $config = $app->{'config'};
1753  my $opts = $app->{'OPTS'};
1754  my $prompt;
1755  my $ret;
1756  my $host = $config->inherit("host") || ::hostname();
1757  my $email = $config->inherit('email') || getlogin() . "\@$host";
1758
1759 # get EMAIL
1760  $prompt = "Enter Email";
1761  $ret = $email;
1762  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1763  $ret = NetSNMP::Term::Complete($prompt, $ret,
1764				 "\tEmail Address - (e.g., <name>@<domain>)");
1765  $email = $opts->{'email'} = $ret if defined $ret;
1766  # get CN
1767  $prompt = "Enter Common Name";
1768  $ret = ($opts->{'cmd'} eq 'genca' ? "ca-$host" : $email);
1769  $ret = $config->inherit('cn') || $config->inherit('commonName') || $ret;
1770  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1771  $ret = NetSNMP::Term::Complete($prompt, $ret,
1772				 "\tCommon Name - (e.g., net-snmp.org)");
1773  $opts->{'cn'} = $ret if defined $ret;
1774  # get ORG
1775  $prompt = "Enter Organization";
1776  $ret = $config->inherit('org') || 'Net-SNMP';
1777  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1778  $ret = NetSNMP::Term::Complete($prompt, $ret,
1779				 "\tOrganization - (e.g., Net-SNMP)");
1780  $opts->{'org'} = $ret if defined $ret;
1781  # get ORG_UNIT
1782  $prompt = "Enter Organizational Unit";
1783  $ret = $config->inherit('unit') || 'Development';
1784  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1785  $ret = NetSNMP::Term::Complete($prompt, $ret,
1786				 "\tOrganizational Unit - (e.g., Development)");
1787  $opts->{'unit'} = $ret if defined $ret;
1788  # get COUNTRY
1789  $prompt = "Enter Country Code";
1790  $ret = $config->inherit('country') || 'US';
1791  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1792  $ret = NetSNMP::Term::Complete($prompt, $ret,
1793				 "Country Code, 2 letters (<tab> for options)",
1794				 $NetSNMP::Cert::MATCH, \@CC);
1795  $opts->{'country'} = $ret if defined $ret;
1796  # get STATE(Province)
1797  $prompt = "Enter State or Province";
1798  $ret = $config->inherit('state') || 'CA';
1799  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1800  $ret = NetSNMP::Term::Complete($prompt, $ret,
1801				 "\tState or Province - (e.g., CA)");
1802  $opts->{'state'} = $ret if defined $ret;
1803  # get LOCALITY
1804  $prompt = "Enter Locality";
1805  $ret = $config->inherit('locality') || 'Davis';
1806  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1807  $ret = NetSNMP::Term::Complete($prompt, $ret, "\tLocality - (e.g., Davis)");
1808  $opts->{'locality'} = $ret if defined $ret;
1809  # get SAN (loop)
1810  if (!$config->{'brief'}) {
1811      print "Enter Subject Alt Names.  Examples:\n";
1812      foreach my $pair (@san_prefix) {
1813	  printf("\t%-10.10s %s\n", $pair->[0], $pair->[1]);
1814      }
1815  }
1816  do {
1817    $ret = 'done';
1818    $prompt = "Enter Subject Alt Name (enter 'done' when finished) [$ret]: ";
1819    $ret = NetSNMP::Term::Complete ($prompt, $ret,
1820			    "\tSubject Alt Name - (<type>:<val>)",
1821			     $NetSNMP::Cert::PREMATCH, \@san_prefix);
1822    push(@{$opts->{'san'}}, $ret) if defined $ret and $ret ne 'done';
1823  } while (defined $ret and $ret ne 'done');
1824}
1825
1826our @snmp_apps = (['snmp', 'Generic Certificate'],
1827		  ['snmpapp','Client Certificate'],
1828		  ['snmpd','Agent Certificate'],
1829		  ['snmptrapd','Trap-agent Certificate']);
1830
1831sub userInputTag {
1832  my $app = shift;
1833  my $config = $app->{'config'};
1834  my $opts = $app->{'OPTS'};
1835  my $ret;
1836  my $prompt;
1837
1838  print "Application Tag:\n\tused to name the certificate and dictate its filename\n";
1839  print "\tIt may also associate it with a particular application (eg \"snmpd\")\n";
1840  print "\tif 'none', a name will be generated from other parameters\n";
1841  print "\tenter <tab><tab> for typical options, or enter new name\n";
1842  $prompt = "Enter Application Tag";
1843  $ret = $config->inherit('tag') || 'none';
1844  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1845  $ret = NetSNMP::Term::Complete($prompt, $ret,
1846				 "Application Tag assocaiated with certificate",
1847				 (not $NetSNMP::Cert::MATCH), \@snmp_apps);
1848  $opts->{'tag'} = $ret if defined $ret and $ret ne 'none';
1849}
1850
1851sub userInput {
1852  my $app = shift;
1853  my $config = $app->{'config'};
1854  my $opts = $app->{'OPTS'};
1855  my $prompt;
1856  my $ret;
1857
1858  print "Choose an operation:\n";
1859  foreach my $op (@interactive_ops) {
1860    print "\t$op->[0]\t-  $op->[1]\n";
1861  }
1862
1863  $prompt = "Operation";
1864  $ret = $config->inherit('cmd') || $interactive_ops[0][0];
1865  $prompt .= (defined $ret ? " [$ret]: " : ": ");
1866  $ret = NetSNMP::Term::Complete($prompt, $ret,
1867				 "Certifciate Operation to perform",
1868				 $NetSNMP::Cert::MATCH, \@interactive_ops);
1869
1870  $opts->{'cmd'} = $ret;
1871
1872  if ($ret =~ /^gencert$/) {
1873    # get tag
1874    $app->userInputTag();
1875    # get DN
1876    $app->userInputDN();
1877    # self-signed/CA-signed(ca-cert)
1878  } elsif ($ret =~ /^genca$/) {
1879    # get DN
1880    $app->userInputDN();
1881  } elsif ($ret =~ /^gencsr$/) {
1882    # get tag
1883    $app->userInputTag();
1884    # get DN
1885    $app->userInputDN();
1886  } elsif ($ret =~ /^signcsr$/) {
1887    # get csr
1888    $prompt = "Choose Certificate Signing Request";
1889    $ret = $config->inherit('csr');
1890    $prompt .= (defined $ret ? " [$ret]: " : ": ");
1891    $ret = NetSNMP::Term::Complete($prompt, $ret);
1892    $opts->{'csr'} = $ret if defined $ret;
1893    # get ca
1894    $prompt = "Choose CA Certificate";
1895    $ret = $config->inherit('with-ca');
1896    $prompt .= (defined $ret ? " [$ret]: " : ": ");
1897    $ret = NetSNMP::Term::Complete($prompt, $ret);
1898    $opts->{'with-ca'} = $ret if defined $ret;
1899  } else {
1900    NetSNMP::Cert::vwarn("aborting operation: exiting...\n", $opts);
1901    exit(1);
1902  }
1903}
1904
1905sub createCaDir {
1906  my $app = shift;
1907  my $dir = shift;
1908  my $config = $app->{'config'};
1909  my $opts = $app->{'OPTS'};
1910  my $file;
1911  my $cmd;
1912
1913  NetSNMP::Cert::make_dirs($opts, $dir, 0700,'newcerts','private');
1914
1915  $file = "$dir/$NetSNMP::Cert::SERIAL";
1916  if (not -f $file) {
1917    $cmd = "echo '01' > '$file'";
1918    NetSNMP::Cert::dprint("$cmd\n", $opts);
1919    NetSNMP::Cert::usystem($cmd, $opts);
1920    chmod(0600, $file);
1921  }
1922
1923  $file = "$dir/$NetSNMP::Cert::INDEX";
1924  if (not -f $file) {
1925    $cmd = "touch '$file'";
1926    NetSNMP::Cert::dprint("$cmd\n", $opts);
1927    NetSNMP::Cert::usystem($cmd, $opts);
1928    chmod(0600, $file);
1929  }
1930}
1931
1932sub genCa {
1933  my $app = shift;
1934  my $config = $app->{'config'};
1935  my $opts = $app->{'OPTS'};
1936  my $host = $config->inherit('host') || ::hostname();
1937  my $days = $config->inherit('days') || $config->inherit('caDays') ||
1938             $NetSNMP::Cert::DEFCADAYS;
1939  my $cn = $config->inherit('cn') || $config->inherit('commonName') ||
1940           "ca-$host";
1941  my $ca = $config->inherit('with-ca');
1942  my $tag = $config->inherit('tag') || $cn;
1943
1944  # create CA dir
1945  my $dir = ".ca/$tag";
1946  $app->createCaDir($dir);
1947
1948  my $outCrt = "$NetSNMP::Cert::CADIR/$tag.crt";
1949  my $outKey = "$NetSNMP::Cert::PRIVDIR/$tag.key";
1950
1951  # set command env
1952  my $env = $app->opensslEnv($cn, $days);
1953
1954  NetSNMP::Cert::check_output_file($opts, $outCrt,
1955				   $config->inherit('interactive'),
1956				   $config->inherit('force'));
1957  NetSNMP::Cert::check_output_file($opts, $outKey,
1958				   $config->inherit('interactive'),
1959				   $config->inherit('force'));
1960
1961  my $cmd = "$env openssl req -extensions v3_ca_create -new -days $days -batch -config $NetSNMP::Cert::SSLCFG -keyout '$outKey'";
1962  $cmd .= " -nodes";
1963
1964  if (defined $ca) {
1965    # we have to gen a csr and then sign it, must preserve CA:TRUE
1966    my $outCsr = "$NetSNMP::Cert::NEWCRTDIR/$tag.csr";
1967    NetSNMP::Cert::check_output_file($opts, $outCsr,
1968				   $config->inherit('interactive'),
1969				   $config->inherit('force'));
1970    $cmd .= " -out '$outCsr'";
1971
1972    NetSNMP::Cert::dprint("genCa (gencsr): $cmd\n", $opts);
1973    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
1974
1975    my $ca_base = ::basename($ca, @NetSNMP::Cert::CRTSUFFIXES);
1976    NetSNMP::Cert::dprint("ca_base: $ca_base\n", $opts);
1977
1978    # set command env
1979    $env = $app->opensslEnv($cn, $days, ".ca/$ca_base");
1980
1981    $cmd = "$env openssl ca -extensions v3_ca_sign_ca -days $days -cert '$ca' -keyfile '$NetSNMP::Cert::PRIVDIR/$ca_base.key' -in '$outCsr' -batch -config $NetSNMP::Cert::SSLCFG -out '$outCrt'";
1982
1983    NetSNMP::Cert::dprint("genCa (signcsr): $cmd\n", $opts);
1984    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
1985  } else {
1986    $cmd .= " -x509 -out '$outCrt'";
1987
1988    NetSNMP::Cert::dprint("genCa: $cmd\n", $opts);
1989    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
1990  }
1991
1992  NetSNMP::Cert::vprint("CA Generated:\n", $opts);
1993  NetSNMP::Cert::vprint("  $outCrt\n", $opts);
1994  NetSNMP::Cert::vprint("  $outKey\n", $opts);
1995}
1996
1997sub genCert {
1998  my $app = shift;
1999  my $config = $app->{'config'};
2000  my $opts = $app->{'OPTS'};
2001  my $host = $config->inherit("host") || ::hostname();
2002  my $email = $config->inherit("email") || getlogin() . "\@$host";
2003  my $days = $config->inherit('days') || $config->inherit('crtDays') || $NetSNMP::Cert::DEFCRTDAYS;
2004  my $cn = $config->inherit('cn') || $config->inherit('commonName');
2005  my $ca = $config->inherit('with-ca');
2006  my $cmd;
2007
2008  my $tag = $opts->{'tag'} || 'snmp';
2009  $cn ||= (NetSNMP::Cert::is_server($tag) ? $host : $email);
2010
2011  my $env = $app->opensslEnv($cn, $days);
2012
2013  my $outCrt = "$NetSNMP::Cert::CRTDIR/$tag.crt";
2014  my $outKey = "$NetSNMP::Cert::PRIVDIR/$tag.key";
2015
2016  NetSNMP::Cert::check_output_file($opts, $outCrt,
2017				   $config->inherit('interactive'),
2018				   $config->inherit('force'));
2019  NetSNMP::Cert::check_output_file($opts, $outKey,
2020				   $config->inherit('interactive'),
2021				   $config->inherit('force'));
2022
2023  $cmd = "$env openssl req -extensions v3_user_create -new -days $days -keyout '$outKey' -batch -config $NetSNMP::Cert::SSLCFG";
2024  $cmd .= " -nodes";
2025
2026  if (defined $ca) {
2027    my $outCsr = "$NetSNMP::Cert::NEWCRTDIR/$tag.csr";
2028    NetSNMP::Cert::check_output_file($opts, $outCsr,
2029				   $config->inherit('interactive'),
2030				   $config->inherit('force'));
2031    $cmd .= " -out '$outCsr'";
2032
2033    NetSNMP::Cert::dprint("genCert (gencsr): $cmd\n", $opts);
2034    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
2035
2036    # XXX cleanup this temp CSR
2037    my $ca_base = ::basename($ca, @NetSNMP::Cert::CRTSUFFIXES);
2038    NetSNMP::Cert::dprint("ca_base: $ca_base\n", $opts);
2039
2040    # set command env
2041    $env = $app->opensslEnv($cn, $days, ".ca/$ca_base");
2042
2043    $cmd = "$env openssl ca -extensions v3_ca_sign -days $days -cert '$ca' -keyfile '$NetSNMP::Cert::PRIVDIR/$ca_base.key' -in '$outCsr' -batch -config $NetSNMP::Cert::SSLCFG -out '$outCrt'";
2044
2045    NetSNMP::Cert::dprint("gencert (signcsr): $cmd\n", $opts);
2046    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
2047  } else {
2048    $cmd .= " -x509 -out '$outCrt'";
2049
2050    NetSNMP::Cert::dprint("genCert: $cmd\n", $opts);
2051    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
2052  }
2053
2054  NetSNMP::Cert::vprint("Certificate Generated:\n", $opts);
2055  NetSNMP::Cert::vprint("  $outCrt\n", $opts);
2056  NetSNMP::Cert::vprint("  $outKey\n", $opts);
2057}
2058
2059sub genCsr {
2060  my $app = shift;
2061  my $isCa = shift; # XXX - not implemented yet
2062  my $config = $app->{'config'};
2063  my $opts = $app->{'OPTS'};
2064  my $host = $config->inherit("host") || ::hostname();
2065  my $email = $config->inherit("email") || getlogin() . "\@$host";
2066  my $days = $config->inherit('days') || $config->inherit('crtDays') || $NetSNMP::Cert::DEFCRTDAYS;
2067  my $cn = $config->inherit('cn') || $config->inherit('commonName');
2068  my $tag = $config->inherit('tag');
2069  my $inCrt = $config->inherit('from-crt') || $config->inherit('fromCert');
2070  my $outCsr;
2071  my $csrKey;
2072
2073  if (defined $inCrt) {
2074    $inCrt = NetSNMP::Cert::find_certs($opts, 'certs', $inCrt);
2075    my $crt = ::basename($inCrt, @NetSNMP::Cert::CRTSUFFIXES);
2076    $csrKey = "$NetSNMP::Cert::PRIVDIR/$crt.key";
2077    $tag ||= $crt;
2078  } else {
2079    $tag ||= 'snmp';
2080    $csrKey ||= "$NetSNMP::Cert::PRIVDIR/$tag.key";
2081  }
2082
2083  $outCsr = "$NetSNMP::Cert::NEWCRTDIR/$tag.csr";
2084
2085  $cn ||= (NetSNMP::Cert::is_server($tag) ? $host : $email);
2086
2087  my $env = $app->opensslEnv($cn, $days);
2088
2089  NetSNMP::Cert::check_output_file($opts, $outCsr,
2090				   $config->inherit('interactive'),
2091				   $config->inherit('force'));
2092  NetSNMP::Cert::check_output_file($opts, $csrKey,
2093				   $config->inherit('interactive'),
2094				   $config->inherit('force'));
2095
2096  my $cmd = (defined $inCrt ?
2097  "$env openssl x509 -x509toreq -in $inCrt -out '$outCsr' -signkey '$csrKey' -nodes -days $days -batch -config $NetSNMP::Cert::SSLCFG" :
2098  "$env openssl req -new -nodes -days $days -batch -keyout '$csrKey' -out '$outCsr' -config $NetSNMP::Cert::SSLCFG");
2099
2100  $cmd .= ($isCa ? " -extensions v3_ca_create" : " -extensions v3_user_create");
2101
2102  NetSNMP::Cert::dprint("genCsr: $cmd\n", $opts);
2103
2104  NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
2105
2106  NetSNMP::Cert::vprint("Certificate Signing Request Generated:\n", $opts);
2107  NetSNMP::Cert::vprint("  $outCsr\n", $opts);
2108  NetSNMP::Cert::vprint("  $csrKey\n", $opts);
2109}
2110
2111sub signCsr {
2112  my $app = shift;
2113  my $isCa = shift;
2114  my $config = $app->{'config'};
2115  my $opts = $app->{'OPTS'};
2116  my $host = $config->inherit("host") || ::hostname();
2117  my $email = $config->inherit("email") || getlogin() . "\@$host";
2118  my $days = $config->inherit('days') || $config->inherit('crtDays') || $NetSNMP::Cert::DEFCRTDAYS;
2119  my $cn = $config->inherit('cn') || $config->inherit('commonName');
2120  my $tag = $config->inherit('tag') || 'snmp';
2121  my $install = $config->inherit('install');
2122
2123  $cn = (NetSNMP::Cert::is_server($tag) ? $host : $email);
2124
2125  my $ca = $opts->{'with-ca'};
2126  NetSNMP::Cert::dprint("ca: $ca\n", $opts);
2127  my $ca_base = ::basename($ca, @NetSNMP::Cert::CRTSUFFIXES);
2128  NetSNMP::Cert::dprint("ca_base: $ca_base\n", $opts);
2129  my $ca_key = "$NetSNMP::Cert::PRIVDIR/$ca_base.key";
2130
2131  my $csr = $opts->{'csr'};
2132  NetSNMP::Cert::dprint("csr: $csr\n", $opts);
2133  my $csr_base = ::basename($csr, @NetSNMP::Cert::CRTSUFFIXES);
2134  NetSNMP::Cert::dprint("csr_base: $csr_base\n", $opts);
2135  my $outdir = ($install ? $NetSNMP::Cert::CRTDIR : $NetSNMP::Cert::NEWCRTDIR);
2136  my $outCrt = "$outdir/$csr_base.crt";
2137
2138  my $env = $app->opensslEnv($cn, $days, ".ca/$ca_base");
2139
2140  NetSNMP::Cert::check_output_file($opts, $outCrt,
2141				   $config->inherit('interactive'),
2142				   $config->inherit('force'));
2143
2144  # XXX - handle keyfile search??
2145  my $cmd = "$env openssl ca -batch -days $days -extensions v3_ca_sign -cert '$ca' -keyfile '$ca_key' -in '$csr' -out '$outCrt' -config $NetSNMP::Cert::SSLCFG";
2146
2147  # $cmd .= ($isCa ? " -extensions v3_ca_sign_ca" : " -extensions v3_ca_sign");
2148
2149  NetSNMP::Cert::dprint("signCsr: $cmd\n", $opts);
2150
2151  NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
2152
2153  NetSNMP::Cert::vprint("Signed Certificate Signing Request:\n", $opts);
2154  NetSNMP::Cert::vprint("  $csr\n", $opts);
2155  NetSNMP::Cert::vprint("with CA:\n", $opts);
2156  NetSNMP::Cert::vprint("  $ca\n", $opts);
2157  NetSNMP::Cert::vprint("  $ca_key\n", $opts);
2158  NetSNMP::Cert::vprint("Generated Certificate:\n", $opts);
2159  NetSNMP::Cert::vprint("  $outCrt\n", $opts);
2160}
2161
2162sub show {
2163  my $app = shift;
2164  my $type = shift || 'certs';
2165  my $config = $app->{'config'};
2166  my $opts = $app->{'OPTS'};
2167  my $stag = shift(@{$opts->{'cmdargs'}});
2168  my $fmt = NetSNMP::Cert::x509_format($opts) || '-subject';
2169  my $brief = $config->inherit('brief');
2170  my $output;
2171  my $cmd;
2172
2173  my $cwd = ::getcwd();
2174  NetSNMP::Cert::dprint("show ($cwd):$type:$stag:$fmt\n", $opts);
2175  NetSNMP::Cert::vprint("$opts->{'tlsdir'}:\n", $opts) unless $brief;
2176
2177  foreach my $c (NetSNMP::Cert::find_certs($opts, $type, $stag)) {
2178    print "\n$c:\n" unless $brief;
2179    $cmd = "openssl x509 -in '$c' -noout $fmt";
2180    NetSNMP::Cert::dprint("show: $cmd\n", $opts);
2181
2182    $output = `$cmd`; chomp $output;
2183    NetSNMP::Cert::vwarn("show-$type failed ($?): $output\n", $opts) if $?;
2184
2185    $output =~ s/^[^\n=]+=// if $brief;
2186
2187    print "$output\n";
2188    print "\n" unless $brief;
2189  }
2190}
2191
2192sub import_file {
2193  my ($file, $suffix, $targ, $rdir, $tag) = @_;
2194
2195  if (NetSNMP::Cert::is_url($file)) {
2196    if ($NetSNMP::Cert::haveUserAgent) {
2197
2198      require File::Temp;
2199      import File::Temp qw(tempfile);
2200
2201      my ($fh, $newfilename) = tempfile(SUFFIX => $suffix);
2202      return if (!$fh || !$newfilename);
2203      my $ua = LWP::UserAgent->new;
2204      my $response = $ua->get($file);
2205      if ($response->is_success) {
2206	print $fh $response->decoded_content();
2207      } else {
2208	NetSNMP::Cert::vwarn("failed to download a certificate from $file");
2209	return;
2210      }
2211      $fh->close;
2212      $file = $newfilename;
2213    } else {
2214      NetSNMP::Cert::vwarn("LWP::UserAgent not installed: unable to import certificate");
2215      return;
2216    }
2217  }
2218
2219  $file = NetSNMP::Cert::fq_rel_path($file, $rdir);
2220  die("file unreadable: $file\n") unless -r $file;
2221
2222
2223  if (! $targ) {
2224    $targ = (NetSNMP::Cert::is_ca_cert($file) ? $NetSNMP::Cert::CADIR : $NetSNMP::Cert::CRTDIR);
2225  }
2226
2227  $targ .= "/" . $tag . $suffix if ($tag);
2228  ::copy($file, $targ);
2229}
2230
2231
2232sub import {
2233  my $app = shift;
2234  my $config = $app->{'config'};
2235  my $opts = $app->{'OPTS'};
2236  my $carg = shift(@{$opts->{'cmdargs'}});
2237  my $karg = shift(@{$opts->{'cmdargs'}});
2238  my $targ;
2239
2240  if (not defined $carg) {
2241    NetSNMP::Cert::vwarn("import: no certificate supplied\n", $opts);
2242    NetSNMP::Cert::usage(1);
2243  }
2244
2245  import_file($carg, '.crt', '',,
2246	      $opts->{'rdir'}, $opts->{'tag'});
2247
2248  return unless defined $karg;
2249
2250  import_file($karg, '.key', 'private',,
2251	      $opts->{'rdir'}, $opts->{'tag'});
2252}
2253
2254
2255package NetSNMP::Cert::Config;
2256use vars qw(@ISA);
2257@ISA = qw(NetSNMP::Cert::Obj);
2258
2259sub new {
2260  my $class = shift;
2261  my $parent = shift;
2262  my $this = $class->SUPER::new('config', $parent);
2263
2264  bless($this, $class);
2265}
2266
2267sub parse {
2268  my $config = shift;
2269  my $app = $config->{'APP'};
2270  my $opts = $app->{'OPTS'};
2271  my $cfgfile = shift;
2272  $cfgfile ||= $opts->{'cfgfile'};
2273
2274  return '0 but true' if $config->{'PARSED'};
2275  return '0 but true' unless defined $cfgfile;
2276
2277  open( CONFIG, "<$cfgfile" )
2278    or die "error - could not open configuration file: $cfgfile";
2279
2280  while (<CONFIG>) {
2281    next if /^\s*#/ or /^\s*$/;
2282
2283    if (/^\s*(\w+)(?:\(([\)\(\d\,\.]+)\))?\s*=\s*{/) {
2284      my $obj = $1;
2285
2286      NetSNMP::Cert::dprint("object: $obj ($2) = {\n", $opts);
2287      die "error - identity: indices not supported: $2" if defined $2;
2288
2289      # Found an object.
2290      if ( $obj eq 'identity' ) {
2291	my $identity = NetSNMP::Cert::Identity::parse(*CONFIG, $config);
2292	my $id = $identity->{'id'};
2293	die "error - identity: 'id' not defined" unless defined $id;
2294	die "error - identity: duplicate '$id'" if exists $config->{$obj}{$id};
2295	$config->{$obj}{$id} = $identity;
2296      } else {
2297        die "error - unrecognized object ($1) at scope (config)";
2298      }
2299    } elsif (/^\s*(\w+)\s*=?\s*(.*?)\s*$/) {
2300      my $key = $1;
2301      my $val = $2;
2302
2303      $val = $config->resolve($val) if $val =~ /\$\w+|\&\{.*?\}/;
2304      # Found a symbol.
2305      NetSNMP::Cert::dprint("  $key = $val\n", $opts);
2306      if ($key eq 'subjectAltName' or $key eq 'san') {
2307	push(@{$config->{$key}}, $val);
2308      } elsif ( defined $config->{$key} ) {
2309        die "error - duplicate symbol $key";
2310      } else {
2311        $config->{$key} = (defined $val ? NetSNMP::Cert::map_bool($val) : 1 );
2312      }
2313    } elsif (/^\s*env\s*(\w+=\S+)\s*$/) {
2314      # Found an environment variable.
2315      NetSNMP::Cert::dprint("$&\n", $opts);
2316      push(@{$config->{'env'}}, $1);
2317    } else {
2318      die("error in config file [$cfgfile:line $.]");
2319    }
2320  }
2321
2322  NetSNMP::Cert::dprint("end parse config\n", $opts);
2323  close(CONFIG);
2324
2325  # augment with any config directives supplied in opts
2326  foreach my $cfg (@{$opts->{'config'}}) {
2327    NetSNMP::Cert::dprint("augmenting config: $cfg\n", $opts);
2328
2329    $config->autoSet($1, (defined($2) ? $2 : 1 ))
2330      if $cfg =~ /^\s*(\w+)\s*=?\s*(.*?)\s*$/;
2331  }
2332  $config->autoSet('PARSED', 1);
2333  return $config->{'PARSED'};
2334}
2335
2336package NetSNMP::Cert::Identity;
2337use vars qw(@ISA);
2338@ISA = qw(NetSNMP::Cert::Obj);
2339
2340sub new {
2341  my $class = shift;
2342  my $this = shift || $class->SUPER::new('identity', @_);
2343  my $ind = $this->{'INDEX'} || 1;
2344
2345  $this->autoSet('name', "$this->{type}.$ind") unless exists $this->{'name'};
2346
2347  $this->autoSet('LOG','') unless exists $this->{'LOG'};
2348  $this->autoSet('TTY_LOG','') unless exists $this->{TTY_LOG};
2349
2350  bless($this, $class);
2351}
2352
2353
2354sub parse {
2355  my $FILE = shift;
2356  my $parent = shift;
2357  my $opts = $parent->inherit('OPTS');
2358  my $identity = new NetSNMP::Cert::Obj('identity', $parent);
2359
2360  NetSNMP::Cert::dprint("parse identity\n", $opts);
2361
2362  while (<$FILE>) {
2363    next if /^\s*#/ or /^\s*$/;
2364
2365    if (/^\s*(\w+)\s*=\s*{/) {
2366      # Found an object.
2367      die "error - can't have nested $1";
2368    } elsif (/^\s*(\w+)\s*=?\s*(.*?)\s*$/) {
2369      my $key = $1;
2370      my $val = $2;
2371
2372      # Found a symbol.
2373      NetSNMP::Cert::dprint("  $key = $val\n", $opts);
2374
2375      $val = $identity->resolve($val) if $val =~ /\$\w+|\&\{.*?\}/;
2376
2377      if ( $key eq 'subjectAltName' or $key eq 'san') {
2378	push(@{$identity->{$key}}, $val);
2379      } elsif ( defined $identity->{$key} ) {
2380        die "error - duplicate symbol $key";
2381      } else {
2382        $identity->{$key} = (defined $val ? NetSNMP::Cert::map_bool($val) : 1 );
2383      }
2384    } elsif (/\s*\}\s*\;/) {
2385      # End of definition.
2386      NetSNMP::Cert::dprint("end parse identity\n", $opts);
2387      return new NetSNMP::Cert::Identity($identity);
2388    } else {
2389      die("error in config file [$opts->{cfgfile}:line $.]");
2390    }
2391  }
2392  die "error - unexpected end of conf file";
2393}
2394
2395package main;
2396
2397my $app = new NetSNMP::Cert::App();
2398$app->init(@ARGV);
2399$app->run();
2400
2401__END__
2402=pod
2403
2404=head1   NAME
2405
2406net-snmp-cert - Net-SNMP Certificate Management Tool
2407
2408=head1   SYNOPSIS
2409
2410=over
2411
2412=item $ net-snmp-cert genca -I --cn ca-Net-SNMP
2413
2414=item $ net-snmp-cert gencert -I -t snmpapp --with-ca ca-Net-SNMP
2415
2416=item $ net-snmp-cert gencert -I -t snmpd --cn net-snmp.org
2417
2418=item $ net-snmp-cert showcas
2419
2420=item $ net-snmp-cert showcerts
2421
2422=back
2423
2424=head1   DESCRIPTION
2425
2426net-snmp-cert creates, signs, installs and displays X.509
2427certificates used in the operation of Net-SNMP/(D)TLS.
2428
2429=head1   SYNTAX
2430
2431=over
2432
2433=item net-snmp-cert [--help|-?]
2434
2435=item net-snmp-cert [--version|-V]
2436
2437=item net-snmp-cert genca    [<flags>] [<dn-opts>] [--with-ca <ca>]
2438
2439=item net-snmp-cert gencert  [<flags>] [<dn-opts>] [--with-ca <ca>]
2440
2441=item net-snmp-cert gencsr   [<flags>] [<dn-opts>] [--from-crt <crt>]
2442
2443=item net-snmp-cert signcsr  [<flags>] [--install] --csr <csr> --with-ca <ca>
2444
2445=item net-snmp-cert showca   [<flags>] [<format-opts>] [<file>|<search-tag>]
2446
2447=item net-snmp-cert showcert [<flags>] [<format-opts>] [<file>|<search-tag>]
2448
2449=item net-snmp-cert import   [<flags>] <file|url> [<key>]
2450
2451=back
2452
2453=head1   COMMANDS
2454
2455=over
2456
2457=item genca
2458
2459generate a signed CA certificate suitable for signing other
2460certificates. default: self-signed unless --with-ca <ca> supplied
2461
2462=item gencert
2463
2464generate a signed certificate suitable for identification, or
2465validation. default: self-signed unless --with-ca <ca> supplied
2466
2467=item gencsr
2468
2469generate a certificate signing request. will create a new
2470key and certificate unless --from-crt <crt> supplied
2471
2472=item signcsr
2473
2474sign a certificate signing request specified by --csr <csr>
2475with the CA certificate specified by --with-ca <ca>
2476
2477=item import
2478
2479import an identity or CA certificate, optionally import <key>
2480if an URL is passed, will attempt to import certificate from site
2481
2482=item showca, showcert
2483
2484show CA or identity certificate(s). may pass fully qualified
2485file or directory name, or a search-tag which prefix matches
2486installed CA or identity certificate file name(s)
2487see FORMAT OPTIONS to specify output format
2488
2489
2490=back
2491
2492=head1   FLAGS
2493
2494=over
2495
2496=item -?, --help            -- show this text and exit
2497
2498=item -V, --version         -- show version string and exit
2499
2500=item -D, --debug           -- raise debug level (-D -D for higher level)
2501
2502=item -F, --force           -- force overwrite of existing output files
2503
2504=item -I, --nointeractive   -- non-interactive run (default interactive)
2505
2506=item -Q, --noverbose       -- non-verbose output (default verbose)
2507
2508=item -C, --cfgdir   <dir>  -- configuration dir (see man(5) snmp_config)
2509
2510=item -T, --tlsdir   <dir>  -- root for cert storage (default <cfgdir>/tls)
2511
2512=item -f, --cfgfile  <file> -- config (default <cfgdir>/net-snmp-cert.conf)
2513
2514=item -i, --identity <id>   -- identity to use from config
2515
2516=item -t, --tag      <tag>  -- application tag (default 'snmp')
2517
2518=item --<cfg-param>[=<val>] -- additional config params
2519
2520=back
2521
2522=head1   CERTIFICATE OPTIONS (<cert-opts>)
2523
2524=over
2525
2526=item -a, --with-ca <ca>    -- CA certificate used to sign request
2527
2528=item -A, --ca <ca>         -- explicit output CA certificate
2529
2530=item -r, --csr <csr>       -- certificate signing request
2531
2532=item -x, --from-crt <crt>  -- input certificate for current operation
2533
2534=item -X, --crt <crt>       -- explicit output certificate
2535
2536=item -y, --install         -- install result in local repository
2537
2538=back
2539
2540=head1   DISTINGUISHED NAME OPTIONS (<dn-opts>)
2541
2542=over
2543
2544=item -e, --email <email>       -- email name
2545
2546=item -h, --host <host>         -- DNS host name, or IP address
2547
2548=item -d, --days <days>         -- number of days certificate is valid
2549
2550=item -n, --cn <cn>             -- common name (CN)
2551
2552=item -o, --org <org>           -- organiztion name
2553
2554=item -u, --unit <unit>         -- organiztion unit name
2555
2556=item -c, --country <country>   -- country code (e.g., US)
2557
2558=item -p, --province <province> -- province name (synomynous w/ state)
2559
2560=item -p, --state <state>       -- state name (synomynous w/ province)
2561
2562=item -l, --locality <locality> -- locality name (e.g, town)
2563
2564=item -s, --san <san>           -- subjectAltName, repeat for each <san>
2565
2566=over 2
2567
2568=item    <san> value format (<FMT>:<VAL>):
2569
2570=over 3
2571
2572=item       dirName:/usr/share/snmp/tls
2573
2574=item       DNS:net-snmp.org
2575
2576=item       email:admin@net-snmp.org
2577
2578=item       IP:192.168.1.1
2579
2580=item       RID:1.1.3.6
2581
2582=item       URI:http://net-snmp.org
2583
2584=back
2585
2586=back
2587
2588=back
2589
2590=head1   FORMAT OPTIONS (<format-opts>)
2591
2592=over
2593
2594=item --brief        -- minimized output (values only where applicable)
2595
2596=item --text         -- full text description
2597
2598=item --subject      -- subject description
2599
2600=item --subject_hash -- hash of subject for indexing
2601
2602=item --issuer       -- issuer description
2603
2604=item --issuer_hash  -- hash of issuer for indexing
2605
2606=item --fingerprint  -- SHA1 digest of DER
2607
2608=item --serial       -- serial number
2609
2610=item --modulus      -- modulus of the public key
2611
2612=item --dates        -- start and end dates
2613
2614=item --purpose      -- displays allowed uses
2615
2616=item --C            -- C code description
2617
2618=back
2619
2620=head1   OPERATIONAL NOTES
2621
2622
2623=head2   Interactive Mode
2624
2625The application operates in interactive mode by default. In this mode
2626basic operations of offered and required input is queried through the
2627command line.
2628
2629Typical <tab> completion is provided when possible and field specific
2630help may be obtained by entering '?'.
2631
2632To turn off interactive mode, supply '--nointeractive' or '-I' on the
2633initial command line. Equivalantly, 'interactive = false' maybe placed
2634in the configuration file (see below).
2635
2636=head2   Configuration
2637
2638A configuration file may be supplied on the command line or found in a
2639default location (<snmpconfpath>/net-snmp-cert.conf). This file may
2640contain configuration parameters equivalent to those supplied on the
2641command line and effectively provides default values for these
2642values. If a command line parameter is supplied it will override the
2643value in the config file. If neither is present then an application
2644value will be used.
2645
2646=head2   Certificate Naming
2647
2648Filenames of created certificates, as stored in the configuration
2649directory, are chosen in the following manner. If and application tag
2650is supplied, it will be used as the basename for the certificate and
2651key generated. Otherwise, for CA certificates, the basename will be
2652derived from the Common Name. For non-CA certificates the application
2653tag defaults to 'snmp' which will then be used as the basename of the
2654certificate and key.
2655
2656=head1   EXAMPLES
2657
2658=over
2659
2660=item net-snmp-cert genca --cn ca-net-snmp.org --days 1000
2661
2662=item net-snmp-cert genca -f .snmp/net-snmp-cert.conf -i nocadm
2663
2664=item net-snmp-cert gencert -t snmpd --cn host.net-snmp.org
2665
2666=item net-snmp-cert gencsr -t snmpapp
2667
2668=item net-snmp-cert signcsr --csr snmpapp --with-ca ca-net-snmp.org
2669
2670=item net-snmp-cert showcerts --subject --issuer --dates snmpapp
2671
2672=item net-snmp-cert showcas --fingerprint ca-net-snmp.org --brief
2673
2674=item net-snmp-cert import ca-third-party.crt
2675
2676=item net-snmp-cert import signed-request.crt signed-request.key
2677
2678=back
2679
2680=head1 COPYRIGHT
2681
2682Copyright (c) 2010 Cobham Analytic Solutions - All rights reserved.
2683Copyright (c) 2010 G. S. Marzot -  All rights reserved.
2684
2685=head1   AUTHOR
2686
2687G. S. Marzot (marz@users.sourceforge.net)
2688
2689=cut
2690
2691