#!/usr/pkg/bin/perl

use strict;
use 5.8.0;

use Getopt::Long qw(:config gnu_getopt no_ignore_case pass_through);
use Cwd qw(getcwd realpath);
use File::Basename;
use File::Copy;
use File::Path;
use Sys::Hostname;
use Carp;

our @CC = (
["AF","AFGHANISTAN"],
["AX","ÅLAND ISLANDS"],
["AL","ALBANIA"],
["DZ","ALGERIA"],
["AS","AMERICAN SAMOA"],
["AD","ANDORRA"],
["AO","ANGOLA"],
["AI","ANGUILLA"],
["AQ","ANTARCTICA"],
["AG","ANTIGUA AND BARBUDA"],
["AR","ARGENTINA"],
["AM","ARMENIA"],
["AW","ARUBA"],
["AU","AUSTRALIA"],
["AT","AUSTRIA"],
["AZ","AZERBAIJAN"],
["BS","BAHAMAS"],
["BH","BAHRAIN"],
["BD","BANGLADESH"],
["BB","BARBADOS"],
["BY","BELARUS"],
["BE","BELGIUM"],
["BZ","BELIZE"],
["BJ","BENIN"],
["BM","BERMUDA"],
["BT","BHUTAN"],
["BO","BOLIVIA, PLURINATIONAL STATE OF"],
["BA","BOSNIA AND HERZEGOVINA"],
["BW","BOTSWANA"],
["BV","BOUVET ISLAND"],
["BR","BRAZIL"],
["IO","BRITISH INDIAN OCEAN TERRITORY"],
["BN","BRUNEI DARUSSALAM"],
["BG","BULGARIA"],
["BF","BURKINA FASO"],
["BI","BURUNDI"],
["KH","CAMBODIA"],
["CM","CAMEROON"],
["CA","CANADA"],
["CV","CAPE VERDE"],
["KY","CAYMAN ISLANDS"],
["CF","CENTRAL AFRICAN REPUBLIC"],
["TD","CHAD"],
["CL","CHILE"],
["CN","CHINA"],
["CX","CHRISTMAS ISLAND"],
["CC","COCOS (KEELING) ISLANDS"],
["CO","COLOMBIA"],
["KM","COMOROS"],
["CG","CONGO"],
["CD","CONGO, THE DEMOCRATIC REPUBLIC OF THE"],
["CK","COOK ISLANDS"],
["CR","COSTA RICA"],
["CI","CÔTE D'IVOIRE"],
["HR","CROATIA"],
["CU","CUBA"],
["CY","CYPRUS"],
["CZ","CZECH REPUBLIC"],
["DK","DENMARK"],
["DJ","DJIBOUTI"],
["DM","DOMINICA"],
["DO","DOMINICAN REPUBLIC"],
["EC","ECUADOR"],
["EG","EGYPT"],
["SV","EL SALVADOR"],
["GQ","EQUATORIAL GUINEA"],
["ER","ERITREA"],
["EE","ESTONIA"],
["ET","ETHIOPIA"],
["FK","FALKLAND ISLANDS (MALVINAS)"],
["FO","FAROE ISLANDS"],
["FJ","FIJI"],
["FI","FINLAND"],
["FR","FRANCE"],
["GF","FRENCH GUIANA"],
["PF","FRENCH POLYNESIA"],
["TF","FRENCH SOUTHERN TERRITORIES"],
["GA","GABON"],
["GM","GAMBIA"],
["GE","GEORGIA"],
["DE","GERMANY"],
["GH","GHANA"],
["GI","GIBRALTAR"],
["GR","GREECE"],
["GL","GREENLAND"],
["GD","GRENADA"],
["GP","GUADELOUPE"],
["GU","GUAM"],
["GT","GUATEMALA"],
["GG","GUERNSEY"],
["GN","GUINEA"],
["GW","GUINEA-BISSAU"],
["GY","GUYANA"],
["HT","HAITI"],
["HM","HEARD ISLAND AND MCDONALD ISLANDS"],
["VA","HOLY SEE (VATICAN CITY STATE)"],
["HN","HONDURAS"],
["HK","HONG KONG"],
["HU","HUNGARY"],
["IS","ICELAND"],
["IN","INDIA"],
["ID","INDONESIA"],
["IR","IRAN, ISLAMIC REPUBLIC OF"],
["IQ","IRAQ"],
["IE","IRELAND"],
["IM","ISLE OF MAN"],
["IL","ISRAEL"],
["IT","ITALY"],
["JM","JAMAICA"],
["JP","JAPAN"],
["JE","JERSEY"],
["JO","JORDAN"],
["KZ","KAZAKHSTAN"],
["KE","KENYA"],
["KI","KIRIBATI"],
["KP","KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF"],
["KR","KOREA, REPUBLIC OF"],
["KW","KUWAIT"],
["KG","KYRGYZSTAN"],
["LA","LAO PEOPLE'S DEMOCRATIC REPUBLIC"],
["LV","LATVIA"],
["LB","LEBANON"],
["LS","LESOTHO"],
["LR","LIBERIA"],
["LY","LIBYAN ARAB JAMAHIRIYA"],
["LI","LIECHTENSTEIN"],
["LT","LITHUANIA"],
["LU","LUXEMBOURG"],
["MO","MACAO"],
["MK","MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF"],
["MG","MADAGASCAR"],
["MW","MALAWI"],
["MY","MALAYSIA"],
["MV","MALDIVES"],
["ML","MALI"],
["MT","MALTA"],
["MH","MARSHALL ISLANDS"],
["MQ","MARTINIQUE"],
["MR","MAURITANIA"],
["MU","MAURITIUS"],
["YT","MAYOTTE"],
["MX","MEXICO"],
["FM","MICRONESIA, FEDERATED STATES OF"],
["MD","MOLDOVA, REPUBLIC OF"],
["MC","MONACO"],
["MN","MONGOLIA"],
["ME","MONTENEGRO"],
["MS","MONTSERRAT"],
["MA","MOROCCO"],
["MZ","MOZAMBIQUE"],
["MM","MYANMAR"],
["NA","NAMIBIA"],
["NR","NAURU"],
["NP","NEPAL"],
["NL","NETHERLANDS"],
["AN","NETHERLANDS ANTILLES"],
["NC","NEW CALEDONIA"],
["NZ","NEW ZEALAND"],
["NI","NICARAGUA"],
["NE","NIGER"],
["NG","NIGERIA"],
["NU","NIUE"],
["NF","NORFOLK ISLAND"],
["MP","NORTHERN MARIANA ISLANDS"],
["NO","NORWAY"],
["OM","OMAN"],
["PK","PAKISTAN"],
["PW","PALAU"],
["PS","PALESTINIAN TERRITORY, OCCUPIED"],
["PA","PANAMA"],
["PG","PAPUA NEW GUINEA"],
["PY","PARAGUAY"],
["PE","PERU"],
["PH","PHILIPPINES"],
["PN","PITCAIRN"],
["PL","POLAND"],
["PT","PORTUGAL"],
["PR","PUERTO RICO"],
["QA","QATAR"],
["RE","RÉUNION"],
["RO","ROMANIA"],
["RU","RUSSIAN FEDERATION"],
["RW","RWANDA"],
["BL","SAINT BARTHÉLEMY"],
["SH","SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA"],
["KN","SAINT KITTS AND NEVIS"],
["LC","SAINT LUCIA"],
["MF","SAINT MARTIN"],
["PM","SAINT PIERRE AND MIQUELON"],
["VC","SAINT VINCENT AND THE GRENADINES"],
["WS","SAMOA"],
["SM","SAN MARINO"],
["ST","SAO TOME AND PRINCIPE"],
["SA","SAUDI ARABIA"],
["SN","SENEGAL"],
["RS","SERBIA"],
["SC","SEYCHELLES"],
["SL","SIERRA LEONE"],
["SG","SINGAPORE"],
["SK","SLOVAKIA"],
["SI","SLOVENIA"],
["SB","SOLOMON ISLANDS"],
["SO","SOMALIA"],
["ZA","SOUTH AFRICA"],
["GS","SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS"],
["ES","SPAIN"],
["LK","SRI LANKA"],
["SD","SUDAN"],
["SR","SURINAME"],
["SJ","SVALBARD AND JAN MAYEN"],
["SZ","SWAZILAND"],
["SE","SWEDEN"],
["CH","SWITZERLAND"],
["SY","SYRIAN ARAB REPUBLIC"],
["TW","TAIWAN, PROVINCE OF CHINA"],
["TJ","TAJIKISTAN"],
["TZ","TANZANIA, UNITED REPUBLIC OF"],
["TH","THAILAND"],
["TL","TIMOR-LESTE"],
["TG","TOGO"],
["TK","TOKELAU"],
["TO","TONGA"],
["TT","TRINIDAD AND TOBAGO"],
["TN","TUNISIA"],
["TR","TURKEY"],
["TM","TURKMENISTAN"],
["TC","TURKS AND CAICOS ISLANDS"],
["TV","TUVALU"],
["UG","UGANDA"],
["UA","UKRAINE"],
["AE","UNITED ARAB EMIRATES"],
["GB","UNITED KINGDOM"],
["US","UNITED STATES"],
["UM","UNITED STATES MINOR OUTLYING ISLANDS"],
["UY","URUGUAY"],
["UZ","UZBEKISTAN"],
["VU","VANUATU"],
["VE","VENEZUELA, BOLIVARIAN REPUBLIC OF"],
["VN","VIET NAM"],
["VG","VIRGIN ISLANDS, BRITISH"],
["VI","VIRGIN ISLANDS, U.S."],
["WF","WALLIS AND FUTUNA"],
["EH","WESTERN SAHARA"],
["YE","YEMEN"],
["ZM","ZAMBIA"],
["ZW","ZIMBABWE"],
);

package NetSNMP::Term;
# gratefully taken from Wayne Thompson's Term::Complete
# if newer CORE modules could be used this could be removed
our($complete, $exit, $done, $kill, $erase1, $erase2, $tty_raw_noecho, $tty_restore, $stty, $tty_safe_restore);
our($tty_saved_state) = '';
CONFIG: {
  $exit   = "\003";
  $done   = "\004";
  $kill   = "\025";
  $erase1 =   "\177";
  $erase2 =   "\010";
  foreach my $s (qw(/bin/stty /usr/bin/stty)) {
    if (-x $s) {
      $tty_raw_noecho = "$s raw -echo";
      $tty_restore    = "$s -raw echo";
      $tty_safe_restore = $tty_restore;
      $stty = $s;
      last;
    }
  }
}

sub Complete {
  my($prompt, $dflt, $help, $match, @cmp_lst);
  my (%help, $cmp, $test, $l, @match);
  my ($return, $r, $exitting) = ("", 0, 0);
  my $tab;

  $prompt = shift;
  $dflt = shift;
  $help = shift;
  $match = shift;

  if (ref $_[0] and ref($_[0][0])) {
    @cmp_lst = @{$_[0]};
  } else {
    @cmp_lst = @_;
  }
  @cmp_lst = map {if (ref($_)) { $help{$_->[0]}=$_->[1]; $_->[0]} else {$_;}} @cmp_lst;

  # Attempt to save the current stty state, to be restored later
  if (defined $stty && defined $tty_saved_state && $tty_saved_state eq '') {
    $tty_saved_state = qx($stty -g 2>/dev/null);
    if ($?) {
      # stty -g not supported
      $tty_saved_state = undef;
    } else {
      $tty_saved_state =~ s/\s+$//g;
      $tty_restore = qq($stty "$tty_saved_state" 2>/dev/null);
    }
  }
  system $tty_raw_noecho if defined $tty_raw_noecho;
 LOOP: {
    local $_;
    print($prompt, $return);
    while (($_ = getc(STDIN)) ne "\r") {
    CASE: {
	# (TAB) attempt completion
	$_ eq "\t" && do {
	  if ($tab) {
	    print(join("\r\n", '', map {exists $help{$_} ? sprintf("\t%-10.10s - %s", $_, $help{$_}) :
					  "\t$_"} grep(/^\Q$return/, @cmp_lst)), "\r\n");
	    $tab--;
	    redo LOOP;
	  }
	  @match = grep(/^\Q$return/, @cmp_lst);
	  unless ($#match < 0) {
	    $l = length($test = shift(@match));
	    foreach $cmp (@match) {
	      until (substr($cmp, 0, $l) eq substr($test, 0, $l)) {
		$l--;
	      }
	    }
	    print("\a");
	    print($test = substr($test, $r, $l - $r));
	    $r = length($return .= $test);
	  }
	  $tab++;
	  last CASE;
	};

	# (^C) exit
	$_ eq $exit && do {
	  print("\r\naborting application...\r\n");
	  $exitting++;
	  undef $return;
	  last LOOP;
	};

	# (^D) done
	$_ eq $done && do {
	  undef $return;
	  last LOOP;
	};

	# (?) show help if available
	$_ eq '?' && do {
	  print("\r\n$help\r\n");
	  if (exists $help{$return}) {
	    print("\t$return - $help{$return}\r\n");
	  } else {
	    print(join("\r\n", map {exists $help{$_} ? "\t$_ - $help{$_}" :
				      "\t$_"} grep(/^\Q$return/, @cmp_lst)), "\r\n");
	  }
	  redo LOOP;
	};

	# (^U) kill
	$_ eq $kill && do {
	  if ($r) {
	    $r	= 0;
	    $return	= "";
	    print("\r\n");
	    redo LOOP;
	  }
	  last CASE;
	};

	# (DEL) || (BS) erase
	($_ eq $erase1 || $_ eq $erase2) && do {
	  if ($r) {
	    print("\b \b");
	    chop($return);
	    $r--;
	  }
	  last CASE;
	};

	# printable char
	ord >= 32 && do {
	  $return .= $_;
	  $r++;
	  print;
	  last CASE;
	};
      }
    }

    if (defined $return) {
      if (length($return) == 0 or $return eq $dflt) {
	$return = $dflt;
      } elsif ($match == $NetSNMP::Cert::MATCH and scalar(grep {/^$return$/} @cmp_lst) != 1 or
	       $match == $NetSNMP::Cert::PREMATCH and scalar(grep {$return=~/$_/} @cmp_lst) != 1) {
	$r	= 0;
	$return	= "";
	print("\r\nChoose a valid option, or ^D to exit\r\n");
	redo LOOP;
      }
    }
  }
 DONE:
  # system $tty_restore if defined $tty_restore;
  if (defined $tty_saved_state && defined $tty_restore && defined $tty_safe_restore) {
    system $tty_restore;
    if ($?) {
      # tty_restore caused error
      system $tty_safe_restore;
    }
  }

  exit(1) if $exitting;
  print("\n");
  return $return;
}

package NetSNMP::Cert;

our $VERSION      = '0.2.9';

our $PROG         = ::basename($0);
our $DEBUG        = 0;
our $OK           = 0;
our $ERR          = -1;

# user input param
our $MATCH        = 1;
our $PREMATCH     = 2;

# Load LWP if possible to import cert from URL
eval('use LWP::UserAgent;');
our $haveUserAgent = ($@ ? 0 : 1);

# required executables
our $OPENSSL     = $ENV{NET_SNMP_CRT_OPENSSL}  || 'openssl';
our $CFGTOOL     = $ENV{NET_SNMP_CRT_CFGTOOL}  || 'net-snmp-config';

# default app config file
our $CFGFILE     = $ENV{NET_SNMP_CRT_CFGFILE}  || 'net-snmp-cert.conf';

# default OpenSSL config files
our $SSLCFGIN    = $ENV{NET_SNMP_CRT_SSLCFGIN} || 'openssl.in';
our $SSLCFGOUT   = $ENV{NET_SNMP_CRT_SSLCFGIN} || '.openssl.conf';
our $SSLCFG      = $ENV{NET_SNMP_CRT_SSLCFG};

# default cmd logs
our $OUTLOG      = $ENV{NET_SNMP_CRT_OUTLOG}   || '.cmd.out.log';
our $ERRLOG      = $ENV{NET_SNMP_CRT_ERRLOG}   || '.cmd.err.log';

# default cert dirs
our $TLSDIR      = $ENV{NET_SNMP_CRT_TLSDIR}   || 'tls';
our $CRTDIR      = $ENV{NET_SNMP_CRT_CRTDIR}   || 'certs';
our $NEWCRTDIR   = $ENV{NET_SNMP_CRT_NEWCRTDIR}|| 'newcerts';
our $CADIR       = $ENV{NET_SNMP_CRT_CADIR}    || 'ca-certs';
our $PRIVDIR     = $ENV{NET_SNMP_CRT_PRIVDIR}  || 'private';

our $SERIAL      = $ENV{NET_SNMP_CRT_SERIAL}   || '.serial';
our $INDEX       = $ENV{NET_SNMP_CRT_INDEX}    || '.index';

our $DEFCADAYS   = $ENV{NET_SNMP_CRT_DEFCADAYS} || 1825;
our $DEFCRTDAYS  = $ENV{NET_SNMP_CRT_DEFCRTDAYS}|| 365;

our @TLSDIRS     = ($CRTDIR, $NEWCRTDIR, $CADIR, $PRIVDIR);

our @CRTSUFFIXES = qw(.pem .csr .der .crt);

our @X509FMTS = qw(text modulus serial subject_hash issuer_hash hash subject
		    purpose issuer startdate enddate dates fingerprint C);

sub dprint {
  my $str = shift;
  my $opts = shift;
  my $dlevel = shift || 1;
  my $flag = (defined $opts ? int($opts->{'debug'} >= $dlevel) : 1);
  my ($pkg, $file, $line, $sub) = caller(1);
  my ($pkg0, $file0, $line0) = caller(0);
  print("${sub}():$line0: $str") if $flag;
}

sub dwarn {
  my $str = shift;
  my $opts = shift;
  my $dlevel = shift || 1;
  my $flag = (defined $opts ? $opts->{'debug'} >= $dlevel : 1);
  my ($pkg, $file, $line, $sub) = caller(1);
  my ($pkg0, $file0, $line0) = caller(0);
  warn("${sub}():$line0: $str") if $flag;
}

sub vprint {
  my $str = shift;
  my $opts = shift;
  my $flag = (defined $opts ? $opts->{'verbose'} : 1);
  my $debug = (defined $opts ? $opts->{'debug'} : 1);
  my ($pkg, $file, $line, $sub) = caller(1);
  my ($pkg0, $file0, $line0) = caller(0);
  $str = ($debug ? "${sub}():$line0: $str" : "$str");
  print("$str") if $flag;
}

sub vwarn {
  my $str = shift;
  my $opts = shift;
  my $flag = (defined $opts ? $opts->{'verbose'} : 1);
  my ($pkg, $file, $line, $sub) = caller(1);
  my ($pkg0, $file0, $line0) = caller(0);
  warn("${sub}():$line0: $str") if $flag;
}

sub rsystem {
  my $cmd = shift;
  my $flag = shift;

  # if not running as root try to use sudo
  if ($>) {
    $cmd = "sudo $flag $cmd";
  } else {
    if ($flag =~ /-b/) {
      $cmd = "$cmd \&";
    }
  }
  die("cmd failed($!): $cmd\n") if system("$cmd");
}

sub usystem {
  my $cmd = shift;
  my $opts = shift;
  my $ret;

  unlink $NetSNMP::Cert::OUTLOG if -e $NetSNMP::Cert::OUTLOG;
  unlink $NetSNMP::Cert::ERRLOG if -e $NetSNMP::Cert::ERRLOG;

  $ret = system("$cmd");

  if ($ret) {
    if ($opts->{'verbose'}) {
      system("cat $NetSNMP::Cert::OUTLOG") if -r $NetSNMP::Cert::OUTLOG;
      system("cat $NetSNMP::Cert::ERRLOG") if -r $NetSNMP::Cert::ERRLOG;
    }
    die("cmd failed($!): $cmd\n");
  }
}

sub home_dir {
  my $cdir = ::getcwd();

  chdir("~");
  my $dir = ::getcwd();
  chdir($cdir);

  return $dir;
}

sub find_bin_dir {
  # This code finds the path to the currently invoked program and
  my $dir;

  $0 =~ m%^(([^/]*)(.*)/)([^/]+)$%;
  if ($1) {
    if ($2) {
      # Invoked using a relative path.  CD there and use pwd.
      $dir = `cd $1 && pwd`;
      chomp $dir;
    } else {
      # Invoked using an absolute path; that's it!
      $dir = $3;
    }
  } else {
    # No path.  Look in PATH for the first instance.
    foreach my $p (split /:/, $ENV{PATH}) {
      $p ||= '.';
      -x "$p/$4" or next;
      $dir = $p;
    }
  }
  $dir or die "Cannot locate program '$0'!";
}

sub fq_rel_path {
  my $path = shift;
  my $cwd = ::getcwd();
  my $rdir = shift || $cwd;

  chdir($rdir) or die("can't change directory: $rdir");
  # get fully qualified path
  if ($path !~ m|^/|) {
    my $pwd = `pwd`; chomp $pwd;
    $path = "$pwd/$path";
    $path = ::realpath($path) if -e $path;
  }
  chdir($cwd) or die("can't change directory: $cwd");

  return $path;
}

sub dir_empty
  {
    my $path = shift;
    opendir(DIR, $path);
    foreach (readdir(DIR)) {
      next if /^\.\.?$/;
      closedir(DIR);
      return 0;
    }
    closedir(DIR);
    return 1;
  }

sub make_dirs {
  my $opts = shift;
  my $dir = shift;
  my $mode = shift;
  my @dirs = @_;

  my $wd = ::getcwd();

  NetSNMP::Cert::dprint("make dirs [$dir:@dirs] from $wd\n", $opts);

  ::mkpath($dir, $opts->{'debug'}, $mode) or die("error - can't make $dir")
    if defined $dir and not -d $dir;

  foreach my $subdir (@dirs) {
    my $d = "$subdir";
    $d = "$dir/$d" if defined $dir;
    NetSNMP::Cert::dprint("making directory: $d\n", $opts) unless -d $d;
    mkdir($d, $mode) or die("error - can't make $d") unless -d $d;
  }
}

sub is_url {
  my $url = shift;

  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})*)?$/;
}

sub in_set {
  my $elem = shift;
  my $set = shift;
  for my $e (eval($set)) {
    return 1 if $e == $elem;
  }
  return 0;
}

sub in_arr {
  my $elem = shift;
  my $arr = shift;
  for my $e (@{$arr}) {
    return 1 if $e eq $elem;
  }
  return 0;
}

sub map_bool {
  my $val = shift;

  return 1 if $val =~ /^true$/i;
  return 0 if $val =~ /^false$/i;
  return $val;
}

sub version {
  my $ret = (@_ ? shift : 1);
  print "$NetSNMP::Cert::PROG: $NetSNMP::Cert::VERSION\n";
  exit($ret);
}

sub GetOptsFromArray {
  my $args = shift;
  local *ARGV;
  @ARGV = @$args;
  my $ret = ::GetOptions(@_);
  @$args = @ARGV; # GetOptions strips out the ones it handles and leaves others
  return $ret;
}
sub pull_cmd {
  my $args = shift;
  my $cmd;

  foreach (@{$args}) {
    if (/^(gence?rt|genca|gencsr|signcsr|showcas?|showce?rts?|import)$/) {
      $cmd = $1;
    }
  }

  @{$args} = grep {!/^$cmd$/} @{$args};

  return $cmd;
}


sub usage {
  my $ret = (@_ ? shift : 1);
  my $arg = shift;

  print "Command not implmeneted yet: $arg\n" if $ret == 2;
  print "Unknown: $arg\n" if $ret == 3;
  print "\n   NAME:\n";
  print "     $NetSNMP::Cert::PROG: [$NetSNMP::Cert::VERSION] - ";
  print "Net-SNMP Certificate Management Tool\n";
  print "\n   DESCRIPTION:\n";
  print "     net-snmp-cert creates, signs, installs and displays X.509\n";
  print "     certificates used in the operation of Net-SNMP/(D)TLS.\n";
  print "\n   SYNOPSIS:\n";
  print "     net-snmp-cert [--help|-?]\n";
  print "     net-snmp-cert [--version|-V]\n";
  print "     net-snmp-cert genca    [<flags>] [<dn-opts>] [--with-ca <ca>]\n";
  print "     net-snmp-cert gencert  [<flags>] [<dn-opts>] [--with-ca <ca>]\n";
  print "     net-snmp-cert gencsr   [<flags>] [<dn-opts>] [--from-crt <crt>]\n";
  print "     net-snmp-cert signcsr  [<flags>] [--install] --csr <csr> --with-ca <ca>\n";
  print "     net-snmp-cert showca   [<flags>] [<format-opts>] [<file>|<search-tag>]\n";
  print "     net-snmp-cert showcert [<flags>] [<format-opts>] [<file>|<search-tag>]\n";
  print "     net-snmp-cert import   [<flags>] <file|url> [<key>]\n";
  print "\n   COMMANDS:\n";
  print "     genca    -- generate a signed CA certificate suitable for signing other\n";
  print "                 certificates. default: self-signed unless --with-ca <ca> supplied\n\n";
  print "     gencert  -- generate a signed certificate suitable for identification, or\n";
  print "                 validation. default: self-signed unless --with-ca <ca> supplied\n\n";
  print "     gencsr   -- generate a certificate signing request. will create a new\n";
  print "                 key and certificate unless --from-crt <crt> supplied\n\n";
  print "     signcsr  -- sign a certificate signing request specified by --csr <csr>\n";
  print "                 with the CA certificate specified by --with-ca <ca>\n";
  print "     import   -- import an identity or CA certificate, optionally import <key>\n";
  print "                 if an URL is passed, will attempt to import certificate from site\n";
  print "     showca,\n";
  print "     showcert -- show CA or identity certificate(s). may pass fully qualified\n";
  print "                 file or directory name, or a search-tag which prefix matches\n";
  print "                 installed CA or identity certificate file name(s)\n";
  print "                 see FORMAT OPTIONS to specify output format\n\n";
  print "\n   FLAGS:\n";
  print "     -?, --help            -- show this text and exit\n";
  print "     -V, --version         -- show version string and exit\n";
  print "     -D, --debug           -- raise debug level (-D -D for higher level)\n";
  print "     -F, --force           -- force overwrite of existing output files\n";
  print "     -I, --nointeractive   -- non-interactive run (default interactive)\n";
  print "     -Q, --noverbose       -- non-verbose output (default verbose)\n";
  print "     -C, --cfgdir   <dir>  -- configuration dir (see man(5) snmp_config)\n";
  print "     -T, --tlsdir   <dir>  -- root for cert storage (default <cfgdir>/tls)\n";
  print "     -f, --cfgfile  <file> -- config (default <cfgdir>/net-snmp-cert.conf)\n";
  print "     -i, --identity <id>   -- identity to use from config\n";
  print "     -t, --tag      <tag>  -- application tag (default 'snmp')\n";
  print "     --<cfg-param>[=<val>] -- additional config params\n";  
  print "\n   CERTIFICATE OPTIONS (<cert-opts>):\n";
  print "     -a, --with-ca <ca>    -- CA certificate used to sign request\n";
  print "     -A, --ca <ca>         -- explicit output CA certificate\n";
  print "     -r, --csr <csr>       -- certificate signing request\n";
  print "     -x, --from-crt <crt>  -- input certificate for current operation\n";
  print "     -X, --crt <crt>       -- explicit output certificate\n";
  print "     -y, --install         -- install result in local repository\n";
  print "\n   DISTINGUISHED NAME OPTIONS (<dn-opts>):\n";
  print "     -e, --email <email>       -- email name\n";
  print "     -h, --host <host>         -- DNS host name, or IP address\n";
  print "     -d, --days <days>         -- number of days certificate is valid\n";
  print "     -n, --cn <cn>             -- common name (CN)\n";
  print "     -o, --org <org>           -- organiztion name\n";
  print "     -u, --unit <unit>         -- organiztion unit name\n";
  print "     -c, --country <country>   -- country code (e.g., US)\n";
  print "     -p, --province <province> -- province name (synomynous w/ state)\n";
  print "     -p, --state <state>       -- state name (synomynous w/ province)\n";
  print "     -l, --locality <locality> -- locality name (e.g, town)\n";
  print "     -s, --san <san>           -- subjectAltName, repeat for each <san>\n";
  print "                               -- <san> value format (<FMT>:<VAL>):\n";
  print "                               --   dirName:/usr/share/snmp/tls\n";
  print "                               --   DNS:net-snmp.org\n";
  print "                               --   email:admin\@net-snmp.org\n";
  print "                               --   IP:192.168.1.1\n";
  print "                               --   RID:1.1.3.6\n";
  print "                               --   URI:http://net-snmp.org\n";
  print "\n   FORMAT OPTIONS (<format-opts>): \n";
  print "     --brief        -- minimized output (values only where applicable)\n";
  print "     --text         -- full text description\n";
  print "     --subject      -- subject description\n";
  print "     --subject_hash -- hash of subject for indexing\n";
  print "     --issuer       -- issuer description\n";
  print "     --issuer_hash  -- hash of issuer for indexing\n";
  print "     --fingerprint  -- SHA1 digest of DER\n";
  print "     --serial       -- serial number\n";
  print "     --modulus      -- modulus of the public key\n";
  print "     --dates        -- start and end dates\n";
  print "     --purpose      -- displays allowed uses\n";
  print "     --C            -- C code description\n";
  print "\n   EXAMPLES: \n";
  print "     net-snmp-cert genca --cn ca-net-snmp.org --days 1000\n";
  print "     net-snmp-cert genca -f .snmp/net-snmp-cert.conf -i nocadm -I\n";
  print "     net-snmp-cert gencert -t snmpd --cn host.net-snmp.org\n";
  print "     net-snmp-cert gencsr -t snmpapp\n";
  print "     net-snmp-cert signcsr --csr snmpapp --with-ca ca-net-snmp.org\n";
  print "     net-snmp-cert showcerts --subject --issuer --dates snmpapp\n";
  print "     net-snmp-cert showcas --fingerprint ca-net-snmp.org --brief\n";
  print "     net-snmp-cert import ca-third-party.crt\n";
  print "     net-snmp-cert import signed-request.crt signed-request.key\n\n";

  exit $ret;
}

sub set_default {
  my $opts = shift;
  my $config = shift;
  my $field = shift;
  my $val = shift;

  if (not defined $opts->{$field}) {
    my $cval = $config->inherit($field);
    $opts->{$field} = (defined $cval ? $cval : $val);
  }
}

sub cfg_path {
  my $path;

  $path = `$NetSNMP::Cert::CFGTOOL --snmpconfpath`;
  chomp $path;
  return (wantarray ? split(':', $path) : $path);
}

sub find_cfgfile {
  my $dir = shift;
  my $file = shift;

  if (defined $dir and -d $dir and 
      defined $file and $file !~ /^[\.\/]/) {
    return fq_rel_path("$dir/$file") if -f "$dir/$file";
  }

  if (defined $file) {
    if (-f $file) {
      return fq_rel_path($file);
    } else {
      return $file; # file is not found, complain later
    }
  }

  my @path = cfg_path();
  unshift(@path, $dir) if defined $dir;
  while (@path) {
    my $p = pop(@path);
    next if $p eq '/var/lib/snmp';
    if (-r "$p/$NetSNMP::Cert::CFGFILE") {
      return fq_rel_path("$p/$NetSNMP::Cert::CFGFILE");
    }
  }

  return $file;
}

sub find_cfgdir {
  my $dir = shift;
  my $file = shift;

  if (defined $dir) {
    $dir = NetSNMP::Cert::fq_rel_path($dir);
    return $dir;
  }

  if (defined $file and -f $file) {
    $dir = ::dirname($file);
    return NetSNMP::Cert::fq_rel_path($dir);
  } else {
    my @path = cfg_path();
    # search first for writeable tls dir
    # for root search top down, for user bottom up
    while (@path) {
      $dir = ($> ? pop(@path): shift(@path));
      next if $dir eq '/var/lib/snmp';
      return $dir if -d "$dir/$NetSNMP::Cert::TLSDIR" and -w "$dir/$NetSNMP::Cert::TLSDIR";
    }
    @path = cfg_path();
    # if no tls dir found, pick first writable config dir
    # for root search top down, for user bottom up
    while (@path) {
      $dir = ($> ? pop(@path): shift(@path));
      next if $dir eq '/var/lib/snmp';
      return $dir if -d $dir and -w $dir;
    }
    my $home = $ENV{HOME} || die "Unable to determine home directory: set \$HOME\n";
    return ($> ? "$home/.snmp" : "/usr/share/snmp"); # XXX hack - no dirs existed or were writable
  }
  # this should never happen
  return undef;
}

sub find_certs {
  my $opts = shift;
  my $dir = shift || 'certs';
  my $targ = shift;
  my $suffix_regex;
  my $cwd = ::getcwd();

  if ($dir eq 'csrs') {
    $dir = $NetSNMP::Cert::NEWCRTDIR;
    $suffix_regex = '\.csr|\.pem';
  } else {
    $suffix_regex = '\.crt|\.pem';
  }

  NetSNMP::Cert::dprint("find_certs(1:$cwd):$dir:$targ:$suffix_regex\n", $opts);

  if ($targ =~ /\.?\//) {
    # see if targ could be file - calc orig. rel. path
    my $arg = NetSNMP::Cert::fq_rel_path($targ, $opts->{'rdir'});
    NetSNMP::Cert::dprint("find_certs(2):$dir:$targ:$arg\n", $opts);
    # targ is a file name - use it
    return (wantarray ? ($arg) : $arg) if -f $arg;
    # else mark as dir if it is
    $targ = "$arg" if -d $arg;
  }

  $targ =~ s/\/*$/\// if -d $targ;

  NetSNMP::Cert::dprint("find_certs(3):$dir:$targ\n", $opts);

  # find certs in targ if a dir (in tlsdir, or relative)
  $dir = $1 if $targ =~ s/^(\S+)\/([^\/\s]*)$/$2/ and -d $1;

  NetSNMP::Cert::dprint("find_certs(4):${dir}:$targ:$cwd\n", $opts);

  my @certs;
  my $glob = "$dir/$targ";
  foreach (<$dir/*>) {
    NetSNMP::Cert::dprint("checking($dir:$targ): $_\n", $opts);
    next unless /^$dir\/$targ(.*$suffix_regex)?$/;
    # return exact match if not wantarray()
    return $_ if (not wantarray()) and /^$dir\/$targ($suffix_regex)?$/;
    NetSNMP::Cert::dprint("pushing: $_\n", $opts);
    push(@certs, $_);
  }

  return (wantarray ? @certs : $certs[0]);
}

sub check_output_file {
  my $opts = shift;
  my $file = shift;
  my $interactive = shift;
  my $force = shift;
  my $continue = 1;

  if (-w $file) {
    if ($interactive and not $force) {
      print "Output file ($file) exists; will be overwritten\nContinue [y/N]: ";
      $continue = <STDIN>; chomp $continue;
      $continue = 0 if $continue =~ /^\s*$|^no?\s*$/i;
    } else {
      if ($force) {
	NetSNMP::Cert::vprint("Output file ($file) exists; overwriting...\n", $opts);
      } else {
	NetSNMP::Cert::vprint("Output file ($file) exists; exiting...\n", $opts);
	$continue = 0;
      }
    }
  } elsif (-e $file) {
    NetSNMP::Cert::vprint("Output file ($file) not writable; exiting...\n", $opts);
    $continue = 0;
  }
  exit(1) unless $continue;
}

sub is_server {
  my $tag = shift;
  return 1 if $tag eq 'snmpd' or $tag eq 'snmptrapd';
  return 0;
}

sub is_ca_cert {
  my $crt = shift;
  my $output = `openssl x509 -in '$crt' -text -noout`;
  return ($output =~ /^\s*CA:TRUE\s*$/m ? 1 : 0);
}

sub x509_format {
  my $opts = shift;
  my $fmt;

  foreach my $f (@NetSNMP::Cert::X509FMTS) {
    $fmt .= " -$f" if defined $opts->{$f};
  }

  return $fmt;
}

sub make_openssl_conf {
  my $file = shift;
  return if -r $file;

  open(F, ">$file") or die("could not open $file");

  print F <<'END';
#
# Net-SNMP (D)TLS OpenSSL configuration file.
#

rdir            = .
dir		= $ENV::DIR
RANDFILE	= $rdir/.rand
MD		= sha512
KSIZE		= 2048
CN		= net-snmp.org
EMAIL		= admin@net-snmp.org
COUNTRY	        = US
STATE		= CA
LOCALITY	= Davis
ORG		= Net-SNMP
ORG_UNIT	= Development
SAN             = email:copy
DAYS            = 365
CRLDAYS         = 30

default_days	= $ENV::DAYS		# how long to certify for
default_crl_days= $ENV::CRLDAYS		# how long before next CRL
default_md	= $ENV::MD		# which md to use.

database	= $dir/.index		# database index file.
serial		= $dir/.serial 		# The current serial number
certs		= $rdir/certs		# identity certs
new_certs_dir	= $dir/newcerts	# default place for new certs.
ca_certs_dir	= $rdir/ca-certs	# default place for new certs.
key_dir		= $rdir/private

crl_dir		= $dir/crl		# crl's
crlnumber	= $dir/.crlnumber	# the current crl number
					# must be commented out to leave V1 CRL
crl		= $crl_dir/crl.pem	# The current CRL

preserve	= no			# keep passed DN ordering
unique_subject	= yes			# Set to 'no' to allow creation of
					# certificates with same subject.
# Extra OBJECT IDENTIFIER info:
oid_section		= new_oids

[ new_oids ]

# Add new OIDs in here for use by 'ca' and 'req'.
# Add a simple OID like this:
# testoid1=1.2.3.4
# Use config file substitution like this:
# testoid2=${testoid1}.5.6

####################################################################
[ ca ]
default_ca	= CA_default		# The default ca section

####################################################################
[ CA_default ]
# certificate	= $ca_certs_dir/$ENV::TAG.crt 	# CA certificate so sign with
name_opt 	= ca_default		# Subject Name options
cert_opt 	= ca_default		# Certificate field options
policy		= policy_match
copy_extensions = copy                  # copy v3 extensions (subjectAltName)
subjectAltName  = copy

# For the CA policy
[ policy_match ]
countryName		= match
stateOrProvinceName	= match
organizationName	= match
organizationalUnitName	= optional
commonName		= supplied
emailAddress		= optional

# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName		= optional
stateOrProvinceName	= optional
localityName		= optional
organizationName	= optional
organizationalUnitName	= optional
commonName		= supplied
emailAddress		= optional

####################################################################
[ req ]
default_bits		= $ENV::KSIZE
default_md		= $ENV::MD
distinguished_name	= req_distinguished_name
string_mask 		= MASK:0x2002
req_extensions          = v3_req
x509_extensions         = v3_user_create

[ req_distinguished_name ]
countryName			= Country Name (2 letter code)
countryName_default		= $ENV::COUNTRY
countryName_min			= 2
countryName_max			= 2

stateOrProvinceName		= State or Province Name (full name)
stateOrProvinceName_default	= $ENV::STATE

localityName			= Locality Name (eg, city)
localityName_default		= $ENV::LOCALITY

0.organizationName		= Organization Name (eg, company)
0.organizationName_default	= $ENV::ORG

organizationalUnitName		= Organizational Unit Name (eg, section)
organizationalUnitName_default	= $ENV::ORG_UNIT

commonName			= Common Name (eg, your name or your server\'s hostname)
commonName_max			= 64
commonName_default              = $ENV::CN

emailAddress			= Email Address
emailAddress_max		= 64
emailAddress_default            = $ENV::EMAIL

[ v3_req ]

# Extensions to add to a certificate request
basicConstraints = CA:FALSE

# Import the email address and/or specified SANs.
%[subjectAltName = $ENV::SAN]

[ v3_user_create ]

# Extensions to add to a certificate request
basicConstraints = CA:FALSE
#keyUsage = nonRepudiation, digitalSignature, keyEncipherment

# PKIX recommendation.
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always

# Import the email address and/or specified SANs.
%[subjectAltName = $ENV::SAN]

[ v3_ca_create ]
# Extensions to add to a CA certificate
basicConstraints = CA:TRUE
# This will be displayed in Netscape's comment listbox.
nsComment = "OpenSSL Generated Certificate (net-snmp)"

# PKIX recommendation.
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always

# Import the email address and/or specified SANs.
%[subjectAltName = $ENV::SAN]

[ v3_ca_sign ]
# Extensions to add when 'ca' signs a request.
basicConstraints = CA:FALSE
# This will be displayed in Netscape's comment listbox.
nsComment = "OpenSSL Generated Certificate (net-snmp)"

keyUsage = nonRepudiation, digitalSignature, keyEncipherment

# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always

[ v3_ca_sign_ca ]
# Extensions to add when 'ca' signs a ca request.
basicConstraints = CA:TRUE

# This will be displayed in Netscape's comment listbox.
nsComment = "OpenSSL Generated Certificate (net-snmp)"

# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always

END

}


package NetSNMP::Cert::Obj;

sub new {
  my $class = shift;
  my $cfield = shift;
  my $parent = shift;
  my $ind = shift;
  my $this = {};
  bless($this, $class);

  # initialize hash of keys which are dynamically created or internal
  $this->{'AUTOKEYS'}{'AUTOKEYS'}++;

  # store a reference to ourselves so our children can find us
  $this->autoSet('CFIELD', $cfield);
  $this->autoSet($cfield , $this);
  $this->autoSet('CHILDREN', []);
  $this->autoSet('INDEX', $ind) if defined $ind;

  if (defined $parent) {
    # cache 'APP' in all objs for easy reference
    $this->autoSet('APP', $parent->inherit('APP'));
    my $app = $this->{'APP'};

    die("net-snmp-cert: error: no NetSNMP::Cert::App context provided")
      if not defined $app or not ref $app eq 'NetSNMP::Cert::App';
    # save children for list traversal
    push(@{$parent->{'CHILDREN'}}, $this) unless $parent eq $this;
  } else {
    # we are the top of the list
    $parent = $this;
  }
  $this->autoSet('PARENT' , $parent);

  return $this;
}

sub autoSet {
  my $this = shift;
  my $key = shift;
  if (@_) {
    my $val = shift;
    $this->{'AUTOKEYS'}{$key}++;
    $this->{$key} = $val;
  }
  return exists $this->{'AUTOKEYS'}{$key};
}

sub inherit {
  my $this = shift;
  my $field = shift;
  my $id;

  # cmd opts override config settings
  if (exists $this->{'APP'} and exists $this->{'APP'}{'OPTS'}) {
    my $opts = $this->{'APP'}{'OPTS'};
    $id = $opts->{'identity'};
    return $opts->{$field} if defined $opts->{$field};
  }

  if (defined $id and exists $this->{'identity'} and
      exists $this->{'identity'}{$id} and
      exists $this->{'identity'}{$id}{$field}) {
    return $this->{'identity'}{$id}{$field};
  }

  # return our field if we have it
  return $this->{$field} if defined $this->{$field};

  # terminate recursion at top and return undef if not found
  return undef if not defined $this->{'PARENT'} or $this->{'PARENT'} eq $this;

  # recurse to parent
  $this->{'PARENT'}->inherit($field);
}

sub resolve {
  my $this = shift;
  my $opts = $this->inherit('OPTS');
  my $val = shift;

  NetSNMP::Cert::dprint("resolving: $val\n", $opts);

  $val =~ s/(\$(\w+))/$this->inherit($2) or die("unresolved reference in config: $1")/ge;
  $val =~ s/(\&\{(.*?)\})/$2/gee;

  NetSNMP::Cert::dprint("resolved: $val\n", $opts);

  return $val
}

sub delete {
  my $this = shift;
  my $opts = $this->inherit('OPTS');

  NetSNMP::Cert::dprint("Obj::delete: ($this) [$this->{CFIELD}]\n", $opts);

  my $parent = $this->{'PARENT'};
  my @children = @{$this->{'CHILDREN'}};

  foreach my $child (@children) {
    $child->delete();
  }

  NetSNMP::Cert::dwarn("Obj: children not freed\n", $opts) if @{$this->{'CHILDREN'}};

  # delete all our self-references
  delete($this->{$this->{'CFIELD'}}) if exists $this->{'CFIELD'};
  delete($this->{'PARENT'});
  delete($this->{'CHILDREN'});

  $parent->disown($this) if defined $parent and ref($parent) =~ /NetSNMP::Cert/;
}

sub disown {
  my $this = shift;
  my $thechild = shift;
  my $opts = $this->inherit('OPTS');
  my $ind = 0;

  NetSNMP::Cert::dprint("Obj::disown: ($this) [$this->{CFIELD}] disowning ($thechild) [$thechild->{CFIELD}]\n", $opts);

  foreach my $child (@{$this->{'CHILDREN'}}) {
    last if $child eq $thechild;
    $ind++;
  }
  if ($ind < @{$this->{'CHILDREN'}}) {
    splice(@{$this->{'CHILDREN'}}, $ind, 1);
  } else {
    NetSNMP::Cert::dwarn("Child ($thechild) not found in object ($this)\n", $opts);
  }
}

sub disabled {
  my $this = shift;
  return (exists $this->{'disable'} ? $this->{'disable'} : 0);
}

my %cfg = (
	   'name' => 1,
	   'type' => 2,
	   'id' => 6,
	  );

sub cfgsort {
  my $self = shift;
  my $a = shift;
  my $b = shift;
  return -1 if exists $cfg{$a} and not exists $cfg{$b};
  return 1 if exists $cfg{$b} and not exists $cfg{$a};
  return $cfg{$a} <=> $cfg{$b} if exists $cfg{$a} and exists $cfg{$b};
  return -1 if !ref($self->{$a}) and ref($self->{$b});
  return 1 if !ref($self->{$b}) and ref($self->{$a});
  return -1 if ref($self->{$a}) =~ /NetSNMP::Cert/ and ref($self->{$b}) !~ /NetSNMP::Cert/;
  return 1 if ref($self->{$b}) =~ /NetSNMP::Cert/ and ref($self->{$a}) !~ /NetSNMP::Cert/;
  return 0;
}

sub dump {
  my $self = shift;
  my $indent = shift;
  my $self_str = $self->{'CFIELD'};
  my @data_keys =  grep {!$self->autoSet($_)} keys %$self;
  my @lines;

  push(@lines, "\n" . ' ' x $indent . "$self_str = {\n") if defined $indent;

  {
    my $indent = (defined $indent ? $indent + 3 : 0);
    foreach my $key (sort {cfgsort($self,$NetSNMP::Cert::Obj::a,
				   $NetSNMP::Cert::Obj::b)} (sort @data_keys)) {
      if (ref($self->{$key}) =~ /NetSNMP::Cert/) {
	push(@lines, $self->{$key}->dump($indent));
      } elsif (ref($self->{$key}) =~ /ARRAY/) {
	push(@lines, "\n") if ref(${$self->{$key}}[0]) !~ /NetSNMP::Cert/;
	foreach my $elem (@{$self->{$key}}) {
	  if (ref($elem) =~ /NetSNMP::Cert/) {
	    push(@lines, $elem->dump($indent));
	  } else {
	    push(@lines, ' ' x $indent . "$key = $elem\n");
	  }
	}
      } else {
	my $str = $key . (defined $self->{$key} ? " = $self->{$key}\n" : "\n");
	push(@lines, ' ' x $indent . $str);
      }
    }
  }

  push(@lines, ' ' x $indent . "}; # end $self_str\n") if defined $indent;
  return @lines;
}

sub DESTROY {
  my $this = shift;

  print("Obj::DESTROY $this [", ref $this, "]\n") if $NetSNMP::Cert::DEBUG >= 3;
}

package NetSNMP::Cert::App;
use vars qw(@ISA);

@ISA = qw(NetSNMP::Cert::Obj);

sub new {
  my $class = shift;

  # the app is god, it is its own parent
  my $this = $class->SUPER::new('APP');

  bless($this, $class);

  # verify required tools or die
  $this->checkReqs();

  # internal intitialization
  $this->initOpts();

  # make a new empty config and init (not parsed)
  $this->{'config'} = new NetSNMP::Cert::Config($this);

  return $this;
}

sub checkReqs {
  my $app = shift;

  die("$NetSNMP::Cert::OPENSSL does not exist or is not executable")
    if system("$NetSNMP::Cert::OPENSSL version > /dev/null 2>&1");

  my $ossl_ver = `$NetSNMP::Cert::OPENSSL version`; chomp $ossl_ver;
  $ossl_ver =~ s/^OpenSSL\s+([\d\.]+).*$/$1/;
  my $ossl_min_ver = $NetSNMP::Cert::OPENSSL_MIN_VER;

  die("$NetSNMP::Cert::OPENSSL (v$ossl_ver): must be $ossl_min_ver or later")
    if ($ossl_ver cmp $ossl_min_ver) < 0;

  die("$NetSNMP::Cert::CFGTOOL not found: please install")
    if system("$NetSNMP::Cert::CFGTOOL > /dev/null 2>&1");
}

sub initOpts {
  my $app = shift;
  my $opts = {};
  $app->autoSet('OPTS', $opts);

  # Define directories we need.
  $opts->{'bindir'} = NetSNMP::Cert::find_bin_dir();

  $opts->{'out'} = "> $NetSNMP::Cert::OUTLOG";
  $opts->{'err'} = "2> $NetSNMP::Cert::ERRLOG";

  # set up paths for app install and runtime env
  $ENV{PATH} = "/sbin:/usr/sbin:$ENV{PATH}";
  $ENV{PATH} = "$opts->{'bindir'}:$ENV{PATH}";

  # default all privs to -rw-------
  umask(077);
}

sub init {
  my $app = shift;
  my $opts = $app->{'OPTS'};
  my $config = $app->{'config'};
  my @args = @_;   # pass external args (typically ARGV)

  # parse command line
  $app->parseArgs(@args);

  # lazy config parsing postponed until here, will not reparse
  $config->parse();

  # set defaults here if not already set in cmdline or config
  # guided interactive mode by default
  NetSNMP::Cert::set_default($opts, $config, 'interactive', 1);
  # verbose output
  NetSNMP::Cert::set_default($opts, $config, 'verbose', 1);

  # find tlsdir/subdirs or make it
  $opts->{'tlsdir'} = $app->createTlsDir();
}

sub parseArgs {
  my $app = shift;
  my $opts = $app->{'OPTS'};
  my @args = @_;

  NetSNMP::Cert::GetOptsFromArray(\@args, $opts, 'help|?', 'version|V',
		      'interactive!', 'I', 'verbose!', 'Q', 'force|F',
		      'cfgfile|f=s', 'cfgdir|C=s', 'tlsdir|tlsDir|T=s',
		      'tag|t=s', 'identity|i=s', 'debug|D+',);

  NetSNMP::Cert::version(0) if $opts->{'version'};

  NetSNMP::Cert::usage(0) if $opts->{'help'};

  # pull out the cmd - getOpts should leave it untouched for us
  $opts->{'cmd'} = NetSNMP::Cert::pull_cmd(\@args);

  # save extra args for command specific processing
  $opts->{'cmdargs'} = [@args];

  # Check debug option first
  $NetSNMP::Cert::DEBUG = $opts->{'debug'};
  $opts->{'err'} = $opts->{'out'} = "" if $opts->{'debug'};
  $opts->{'interactive'} = not $opts->{'I'} if defined $opts->{'I'};
  $opts->{'verbose'} = not $opts->{'Q'} if defined $opts->{'Q'};

  # search for cfgdir and cfgfile based on opts and confpath
  $opts->{'cfgdir'} = NetSNMP::Cert::find_cfgdir($opts->{'cfgdir'},
						 $opts->{'cfgfile'});
  $opts->{'cfgfile'} = NetSNMP::Cert::find_cfgfile($opts->{'cfgdir'},
						   $opts->{'cfgfile'});
}

sub createTlsDir {
  my $app = shift;
  my $opts = $app->{'OPTS'};
  my $config = $app->{'config'};
  my $dir;
  my $file;
  my $cmd;

  $dir = $config->inherit('tlsDir');

  if (not defined $dir) {
    my $cfgdir = $opts->{'cfgdir'};
    die("undefined cfgdir: unable to creat tlsdir: exiting...\n") unless defined $cfgdir;
    $dir = "$cfgdir/$NetSNMP::Cert::TLSDIR";
  }

  NetSNMP::Cert::dprint("tlsDir is: $dir\n", $opts);
  $dir = NetSNMP::Cert::fq_rel_path($dir);
  NetSNMP::Cert::dprint("tlsDir is: $dir\n", $opts);

  NetSNMP::Cert::dprint("tlsDir not found, creating\n", $opts) unless -d $dir;
  NetSNMP::Cert::make_dirs($opts, $dir, 0700, @NetSNMP::Cert::TLSDIRS);

  my $ssl_cfg_in = NetSNMP::Cert::fq_rel_path($NetSNMP::Cert::SSLCFGIN,$dir);

  # Existing openssl.conf tmpl will be preserved
  if (-f $ssl_cfg_in) {
    NetSNMP::Cert::dwarn("OpenSSL template exists ($ssl_cfg_in): preserving...", $opts);
  }

  if (not -f $ssl_cfg_in) {
    NetSNMP::Cert::dprint("make_openssl_conf($ssl_cfg_in)", $opts);
    NetSNMP::Cert::make_openssl_conf($ssl_cfg_in);
    chmod(0600, $ssl_cfg_in);
  }

  NetSNMP::Cert::dprint("createTlsDir: done\n", $opts);
  return $dir;
}

my @interactive_ops = (['gencert', "Generate a signed certificate"],
	   ['genca', "Generate a CA certificate"],
	   ['gencsr', "Generate a Certificate Signing Request"],
	   ['signcsr', "Sign a Certificate Signing Request"],
	  );

sub run {
  my $app = shift;
  my $opts = $app->{'OPTS'};
  my $cmd = $opts->{'cmd'};

  # must have a command in non-Interactive mode
  NetSNMP::Cert::usage(3) if !$opts->{'interactive'} and !$opts->{'cmd'};

  #  change dir tls dir - the cwd for all commands - save cwd first
  $opts->{'rdir'} = ::getcwd();
  chdir($opts->{'tlsdir'}) or die("could'nt change directory: $opts->{tlsdir}");
  # display context
  NetSNMP::Cert::dprint("PATH: $ENV{PATH}\n\n", $opts);
  NetSNMP::Cert::dprint("config file: $opts->{cfgfile}\n", $opts);
  NetSNMP::Cert::dprint("config dir:  $opts->{cfgdir}\n", $opts);
  NetSNMP::Cert::dprint("tls dir:     $opts->{tlsdir}\n", $opts);

  my $cmdstr = join(' ', $cmd, @{$opts->{'cmdargs'}});
  NetSNMP::Cert::dprint("command:     $cmdstr\n", $opts);

  NetSNMP::Cert::GetOptsFromArray(\@{$opts->{'cmdargs'}}, $opts,
				  'with-ca|a=s', 'ca|A=s','csr|r=s',
				  'from-crt|x=s', 'crt|X=s', 'days|d=s',
				  'cn|n=s', 'email|e=s', 'host|h=s',
				  'san|s=s@', 'org|o=s', 'unit|u=s',
				  'country|c=s', 'state|province|p=s',
				  'locality|l=s', 'brief|b',
				  @NetSNMP::Cert::X509FMTS);

  # process extra args; --<cfg-name>[=<val>]
  $app->processExtraArgs();

  # If in interactive mode - fill missing info by interviewing user
  if (not defined $cmd or grep {/$cmd/} map {$_->[0]} @interactive_ops) {
    $app->userInput() if $opts->{interactive};
    $cmd = $opts->{'cmd'}; # may have changed
  }

  # resolve input args to filenames
  $app->resolveCrtArgs();

  # use env. or merge template for OpenSSL config
  $NetSNMP::Cert::SSLCFG ||= $app->opensslCfg();

  if ($cmd =~ /^genca$/) {
    NetSNMP::Cert::usage(1) if defined $opts->{'from-crt'} or defined $opts->{'csr'};
    $app->genCa($opts->{'with-ca'});
  } elsif ($cmd =~ /^gence?rt$/) {
    NetSNMP::Cert::usage(1) if defined $opts->{'from-crt'} or defined $opts->{'csr'};
    $app->genCert($opts->{'with-ca'});
  } elsif ($cmd =~ /^gencsr$/) {
    NetSNMP::Cert::usage(1) if defined $opts->{'with-ca'} or defined $opts->{'crt'};
    $app->genCsr();
  } elsif ($cmd =~ /^signcsr$/) {
    NetSNMP::Cert::usage(1) unless defined $opts->{'with-ca'} and defined $opts->{'csr'};
    $app->signCsr();
  } elsif ($cmd =~ /^show(ce?rts?)?$/) {
    $app->show('certs');
  } elsif ($cmd =~ /^showcas?$/) {
    $app->show('ca-certs');
  } elsif ($cmd =~ /^import$/) {
    $app->import();
  } else {
    NetSNMP::Cert::usage();
  }
}

sub processExtraArgs {
  my $app = shift;
  my $opts = $app->{'OPTS'};
  my @args;

  NetSNMP::Cert::dprint("processing extra args...\n", $opts);

  while (@{$opts->{'cmdargs'}}) {
    my $arg = shift(@{$opts->{'cmdargs'}});
    NetSNMP::Cert::dprint("found: arg --> $arg\n", $opts);
    if ($arg =~ /^-+(\w+)(?:=(.*?))?\s*$/) {
      NetSNMP::Cert::dprint("found: arg($1) val($2)\n", $opts);
      if (exists $opts->{$1}) {
   	NetSNMP::Cert::vwarn("option ($1) already set: overwriting\n", $opts);
      } 
      $opts->{$1} = (defined $2 ? $2 : 1);
    } else {
      push(@args, $arg);
    }
  }
  @{$opts->{'cmdargs'}} = @args;
}

sub resolveCrtArgs {
  my $app = shift;
  my $opts = $app->{'OPTS'};

  # find ca, crt, csr args if present and return fully qualified path
  if (defined $opts->{'with-ca'}) {
    my $ca;
    $ca = NetSNMP::Cert::find_certs($opts, 'ca-certs', $opts->{'with-ca'});
    die("unable to locate CA certificate ($opts->{'with-ca'})\n") unless -e $ca;
    die("unable read CA certificate ($opts->{'with-ca'})\n") unless -r $ca;
    $opts->{'with-ca'} = $ca;
  }

  if (defined $opts->{'from-crt'}) {
    my $crt;
    $crt = NetSNMP::Cert::find_certs($opts, 'certs', $opts->{'from-crt'});
    die("unable to locate certificate ($opts->{'from-crt'})\n") unless -e $crt;
    die("unable read certificate ($opts->{'from-crt'})\n") unless -r $crt;
    $opts->{'from-crt'} = $crt;
  }

  if (defined $opts->{'csr'}) {
    my $csr;
    $csr = NetSNMP::Cert::find_certs($opts, 'csrs', $opts->{'csr'});
    die("unable to locate CSR certificate ($opts->{csr})\n") unless -e $csr;
    die("unable read CSR certificate ($opts->{csr})\n") unless -r $csr;
    $opts->{'csr'} = $csr;
  }
}

sub opensslCfg {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $san = $config->inherit('san') || $config->inherit('subjectAltName');
  my $ssl_cfg_in = NetSNMP::Cert::fq_rel_path($NetSNMP::Cert::SSLCFGIN);
  my $ssl_cfg_out = NetSNMP::Cert::fq_rel_path($NetSNMP::Cert::SSLCFGOUT);

  if (not -f $ssl_cfg_in) {
    NetSNMP::Cert::vwarn("OpenSSL template not found: $ssl_cfg_in\n", $opts);
    die("no OpenSSL template");
  }

  open(IN, $ssl_cfg_in) or die("unable to open OpenSSL template: $ssl_cfg_in\n");
  open(OUT, ">$ssl_cfg_out") or die("unable to open OpenSSL config: $ssl_cfg_out\n");

  print OUT "#######################################################\n";
  print OUT "#####    Warning: Do Not Edit - Generated File    #####\n";
  print OUT "#######################################################\n";
  while (<IN>) {
    if ($san) {
      s/\%\[([^\]]*?)\]/$1/;
    } else {
      s/\%\[([^\]]*?)\]//;
    }
    print OUT $_;
  }
  close(IN);
  close(OUT);

  return $ssl_cfg_out;
}

sub opensslEnv {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};

  my $cn = shift;
  my $days = shift;
  my $dir = shift || ".";
  # XXX - need to handle config'd identity here
  my $name = $config->inherit("name");
  my $host = $config->inherit("host");
  my $email = $config->inherit("email");
  my $country = $config->inherit("country");
  my $state = $config->inherit("state");
  my $locality = $config->inherit("locality");
  my $org = $config->inherit("org");
  my $org_unit = $config->inherit("unit") || $config->inherit("orgUnit");
  my $san;
  my $san_arr_ref;
  my $md = $config->inherit("msgDigest");
  my $ksize = $config->inherit("keySize");

  my $env;

  $env .= " KSIZE=$ksize" if defined $ksize;
  $env .= " DAYS=$days" if defined $days;
  $env .= " MD=$md" if defined $md;

  $env .= " DIR='$dir'" if defined $dir;

  $env .= " EMAIL=$email" if defined $email;
  $env .= " CN='$cn'" if defined $cn;
  $env .= " ORG='$org'" if defined $org;
  $env .= " ORG_UNIT='$org_unit'" if defined $org_unit;
  $env .= " COUNTRY=$country" if defined $country;
  $env .= " STATE=$state" if defined $state;
  $env .= " LOCALITY=$locality" if defined $locality;

  $san_arr_ref = $config->inherit('subjectAltName');
  $san = join('\,\ ', @{$san_arr_ref}) if ref $san_arr_ref;
  $san_arr_ref = $config->inherit('san');
  $san .= join('\,\ ', @{$san_arr_ref}) if ref $san_arr_ref;
  $san =~ s/EMAIL:/email:/g;
  $env .= " SAN=$san" if defined $san;

  NetSNMP::Cert::dprint("opensslEnv: $env\n", $opts);

  return $env;
}
our @san_prefix = (['dirName:', "e.g., dirName:/usr/share/snmp/tls"],
		   ['DNS:', "e.g., DNS:test.net-snmp.org)"],
		   ['email:', "e.g., email:admin\@net-snmp.org"],
		   ['IP:', "e.g., IP:192.168.1.1"],
		   ['RID:', "e.g., RID:1.1.3.6.20"],
		   ['URI:', "e.g., URI:http://www.net-snmp.org"]);

sub userInputDN {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $prompt;
  my $ret;
  my $host = $config->inherit("host") || ::hostname();
  my $email = $config->inherit('email') || getlogin() . "\@$host";

 # get EMAIL
  $prompt = "Enter Email";
  $ret = $email;
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "\tEmail Address - (e.g., <name>@<domain>)");
  $email = $opts->{'email'} = $ret if defined $ret;
  # get CN
  $prompt = "Enter Common Name";
  $ret = ($opts->{'cmd'} eq 'genca' ? "ca-$host" : $email);
  $ret = $config->inherit('cn') || $config->inherit('commonName') || $ret;
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "\tCommon Name - (e.g., net-snmp.org)");
  $opts->{'cn'} = $ret if defined $ret;
  # get ORG
  $prompt = "Enter Organization";
  $ret = $config->inherit('org') || 'Net-SNMP';
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "\tOrganization - (e.g., Net-SNMP)");
  $opts->{'org'} = $ret if defined $ret;
  # get ORG_UNIT
  $prompt = "Enter Organizational Unit";
  $ret = $config->inherit('unit') || 'Development';
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "\tOrganizational Unit - (e.g., Development)");
  $opts->{'unit'} = $ret if defined $ret;
  # get COUNTRY
  $prompt = "Enter Country Code";
  $ret = $config->inherit('country') || 'US';
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "Country Code, 2 letters (<tab> for options)",
				 $NetSNMP::Cert::MATCH, \@CC);
  $opts->{'country'} = $ret if defined $ret;
  # get STATE(Province)
  $prompt = "Enter State or Province";
  $ret = $config->inherit('state') || 'CA';
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "\tState or Province - (e.g., CA)");
  $opts->{'state'} = $ret if defined $ret;
  # get LOCALITY
  $prompt = "Enter Locality";
  $ret = $config->inherit('locality') || 'Davis';
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret, "\tLocality - (e.g., Davis)");
  $opts->{'locality'} = $ret if defined $ret;
  # get SAN (loop)
  if (!$config->{'brief'}) {
      print "Enter Subject Alt Names.  Examples:\n";
      foreach my $pair (@san_prefix) {
	  printf("\t%-10.10s %s\n", $pair->[0], $pair->[1]);
      }
  }
  do {
    $ret = 'done';
    $prompt = "Enter Subject Alt Name (enter 'done' when finished) [$ret]: ";
    $ret = NetSNMP::Term::Complete ($prompt, $ret,
			    "\tSubject Alt Name - (<type>:<val>)",
			     $NetSNMP::Cert::PREMATCH, \@san_prefix);
    push(@{$opts->{'san'}}, $ret) if defined $ret and $ret ne 'done';
  } while (defined $ret and $ret ne 'done');
}

our @snmp_apps = (['snmp', 'Generic Certificate'],
		  ['snmpapp','Client Certificate'],
		  ['snmpd','Agent Certificate'],
		  ['snmptrapd','Trap-agent Certificate']);

sub userInputTag {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $ret;
  my $prompt;

  print "Application Tag:\n\tused to name the certificate and dictate its filename\n";
  print "\tIt may also associate it with a particular application (eg \"snmpd\")\n";
  print "\tif 'none', a name will be generated from other parameters\n";
  print "\tenter <tab><tab> for typical options, or enter new name\n";
  $prompt = "Enter Application Tag";
  $ret = $config->inherit('tag') || 'none';
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "Application Tag assocaiated with certificate",
				 (not $NetSNMP::Cert::MATCH), \@snmp_apps);
  $opts->{'tag'} = $ret if defined $ret and $ret ne 'none';
}

sub userInput {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $prompt;
  my $ret;

  print "Choose an operation:\n";
  foreach my $op (@interactive_ops) {
    print "\t$op->[0]\t-  $op->[1]\n";
  }

  $prompt = "Operation";
  $ret = $config->inherit('cmd') || $interactive_ops[0][0];
  $prompt .= (defined $ret ? " [$ret]: " : ": ");
  $ret = NetSNMP::Term::Complete($prompt, $ret,
				 "Certifciate Operation to perform",
				 $NetSNMP::Cert::MATCH, \@interactive_ops);

  $opts->{'cmd'} = $ret;

  if ($ret =~ /^gencert$/) {
    # get tag
    $app->userInputTag();
    # get DN
    $app->userInputDN();
    # self-signed/CA-signed(ca-cert)
  } elsif ($ret =~ /^genca$/) {
    # get DN
    $app->userInputDN();
  } elsif ($ret =~ /^gencsr$/) {
    # get tag
    $app->userInputTag();
    # get DN
    $app->userInputDN();
  } elsif ($ret =~ /^signcsr$/) {
    # get csr
    $prompt = "Choose Certificate Signing Request";
    $ret = $config->inherit('csr');
    $prompt .= (defined $ret ? " [$ret]: " : ": ");
    $ret = NetSNMP::Term::Complete($prompt, $ret);
    $opts->{'csr'} = $ret if defined $ret;
    # get ca
    $prompt = "Choose CA Certificate";
    $ret = $config->inherit('with-ca');
    $prompt .= (defined $ret ? " [$ret]: " : ": ");
    $ret = NetSNMP::Term::Complete($prompt, $ret);
    $opts->{'with-ca'} = $ret if defined $ret;
  } else {
    NetSNMP::Cert::vwarn("aborting operation: exiting...\n", $opts);
    exit(1);
  }
}

sub createCaDir {
  my $app = shift;
  my $dir = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $file;
  my $cmd;

  NetSNMP::Cert::make_dirs($opts, $dir, 0700,'newcerts','private');

  $file = "$dir/$NetSNMP::Cert::SERIAL";
  if (not -f $file) {
    $cmd = "echo '01' > '$file'";
    NetSNMP::Cert::dprint("$cmd\n", $opts);
    NetSNMP::Cert::usystem($cmd, $opts);
    chmod(0600, $file);
  }

  $file = "$dir/$NetSNMP::Cert::INDEX";
  if (not -f $file) {
    $cmd = "touch '$file'";
    NetSNMP::Cert::dprint("$cmd\n", $opts);
    NetSNMP::Cert::usystem($cmd, $opts);
    chmod(0600, $file);
  }
}

sub genCa {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $host = $config->inherit('host') || ::hostname();
  my $days = $config->inherit('days') || $config->inherit('caDays') || 
             $NetSNMP::Cert::DEFCADAYS;
  my $cn = $config->inherit('cn') || $config->inherit('commonName') ||
           "ca-$host";
  my $ca = $config->inherit('with-ca');
  my $tag = $config->inherit('tag') || $cn;

  # create CA dir
  my $dir = ".ca/$tag";
  $app->createCaDir($dir);

  my $outCrt = "$NetSNMP::Cert::CADIR/$tag.crt";
  my $outKey = "$NetSNMP::Cert::PRIVDIR/$tag.key";

  # set command env
  my $env = $app->opensslEnv($cn, $days);

  NetSNMP::Cert::check_output_file($opts, $outCrt,
				   $config->inherit('interactive'),
				   $config->inherit('force'));
  NetSNMP::Cert::check_output_file($opts, $outKey,
				   $config->inherit('interactive'),
				   $config->inherit('force'));

  my $cmd = "$env openssl req -extensions v3_ca_create -new -days $days -batch -config $NetSNMP::Cert::SSLCFG -keyout '$outKey'";
  $cmd .= " -nodes";

  if (defined $ca) {
    # we have to gen a csr and then sign it, must preserve CA:TRUE
    my $outCsr = "$NetSNMP::Cert::NEWCRTDIR/$tag.csr";
    NetSNMP::Cert::check_output_file($opts, $outCsr,
				   $config->inherit('interactive'),
				   $config->inherit('force'));
    $cmd .= " -out '$outCsr'";

    NetSNMP::Cert::dprint("genCa (gencsr): $cmd\n", $opts);
    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);

    my $ca_base = ::basename($ca, @NetSNMP::Cert::CRTSUFFIXES);
    NetSNMP::Cert::dprint("ca_base: $ca_base\n", $opts);

    # set command env
    $env = $app->opensslEnv($cn, $days, ".ca/$ca_base");

    $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'";

    NetSNMP::Cert::dprint("genCa (signcsr): $cmd\n", $opts);
    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
  } else {
    $cmd .= " -x509 -out '$outCrt'";

    NetSNMP::Cert::dprint("genCa: $cmd\n", $opts);
    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
  }

  NetSNMP::Cert::vprint("CA Generated:\n", $opts);
  NetSNMP::Cert::vprint("  $outCrt\n", $opts);
  NetSNMP::Cert::vprint("  $outKey\n", $opts);
}

sub genCert {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $host = $config->inherit("host") || ::hostname();
  my $email = $config->inherit("email") || getlogin() . "\@$host";
  my $days = $config->inherit('days') || $config->inherit('crtDays') || $NetSNMP::Cert::DEFCRTDAYS;
  my $cn = $config->inherit('cn') || $config->inherit('commonName');
  my $ca = $config->inherit('with-ca');
  my $cmd;

  my $tag = $opts->{'tag'} || 'snmp';
  $cn ||= (NetSNMP::Cert::is_server($tag) ? $host : $email);

  my $env = $app->opensslEnv($cn, $days);

  my $outCrt = "$NetSNMP::Cert::CRTDIR/$tag.crt";
  my $outKey = "$NetSNMP::Cert::PRIVDIR/$tag.key";

  NetSNMP::Cert::check_output_file($opts, $outCrt,
				   $config->inherit('interactive'),
				   $config->inherit('force'));
  NetSNMP::Cert::check_output_file($opts, $outKey,
				   $config->inherit('interactive'),
				   $config->inherit('force'));

  $cmd = "$env openssl req -extensions v3_user_create -new -days $days -keyout '$outKey' -batch -config $NetSNMP::Cert::SSLCFG";
  $cmd .= " -nodes";

  if (defined $ca) {
    my $outCsr = "$NetSNMP::Cert::NEWCRTDIR/$tag.csr";
    NetSNMP::Cert::check_output_file($opts, $outCsr,
				   $config->inherit('interactive'),
				   $config->inherit('force'));
    $cmd .= " -out '$outCsr'";

    NetSNMP::Cert::dprint("genCert (gencsr): $cmd\n", $opts);
    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);

    # XXX cleanup this temp CSR
    my $ca_base = ::basename($ca, @NetSNMP::Cert::CRTSUFFIXES);
    NetSNMP::Cert::dprint("ca_base: $ca_base\n", $opts);

    # set command env
    $env = $app->opensslEnv($cn, $days, ".ca/$ca_base");

    $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'";

    NetSNMP::Cert::dprint("gencert (signcsr): $cmd\n", $opts);
    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
  } else {
    $cmd .= " -x509 -out '$outCrt'";

    NetSNMP::Cert::dprint("genCert: $cmd\n", $opts);
    NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);
  }

  NetSNMP::Cert::vprint("Certificate Generated:\n", $opts);
  NetSNMP::Cert::vprint("  $outCrt\n", $opts);
  NetSNMP::Cert::vprint("  $outKey\n", $opts);
}

sub genCsr {
  my $app = shift;
  my $isCa = shift; # XXX - not implemented yet
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $host = $config->inherit("host") || ::hostname();
  my $email = $config->inherit("email") || getlogin() . "\@$host";
  my $days = $config->inherit('days') || $config->inherit('crtDays') || $NetSNMP::Cert::DEFCRTDAYS;
  my $cn = $config->inherit('cn') || $config->inherit('commonName');
  my $tag = $config->inherit('tag');
  my $inCrt = $config->inherit('from-crt') || $config->inherit('fromCert');
  my $outCsr;
  my $csrKey;

  if (defined $inCrt) {
    $inCrt = NetSNMP::Cert::find_certs($opts, 'certs', $inCrt);
    my $crt = ::basename($inCrt, @NetSNMP::Cert::CRTSUFFIXES);
    $csrKey = "$NetSNMP::Cert::PRIVDIR/$crt.key";
    $tag ||= $crt;
  } else {
    $tag ||= 'snmp';
    $csrKey ||= "$NetSNMP::Cert::PRIVDIR/$tag.key";
  }

  $outCsr = "$NetSNMP::Cert::NEWCRTDIR/$tag.csr";

  $cn ||= (NetSNMP::Cert::is_server($tag) ? $host : $email);

  my $env = $app->opensslEnv($cn, $days);

  NetSNMP::Cert::check_output_file($opts, $outCsr,
				   $config->inherit('interactive'),
				   $config->inherit('force'));
  NetSNMP::Cert::check_output_file($opts, $csrKey,
				   $config->inherit('interactive'),
				   $config->inherit('force'));

  my $cmd = (defined $inCrt ?
  "$env openssl x509 -x509toreq -in $inCrt -out '$outCsr' -signkey '$csrKey' -nodes -days $days -batch -config $NetSNMP::Cert::SSLCFG" :
  "$env openssl req -new -nodes -days $days -batch -keyout '$csrKey' -out '$outCsr' -config $NetSNMP::Cert::SSLCFG");

  $cmd .= ($isCa ? " -extensions v3_ca_create" : " -extensions v3_user_create");

  NetSNMP::Cert::dprint("genCsr: $cmd\n", $opts);

  NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);

  NetSNMP::Cert::vprint("Certificate Signing Request Generated:\n", $opts);
  NetSNMP::Cert::vprint("  $outCsr\n", $opts);
  NetSNMP::Cert::vprint("  $csrKey\n", $opts);
}

sub signCsr {
  my $app = shift;
  my $isCa = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $host = $config->inherit("host") || ::hostname();
  my $email = $config->inherit("email") || getlogin() . "\@$host";
  my $days = $config->inherit('days') || $config->inherit('crtDays') || $NetSNMP::Cert::DEFCRTDAYS;
  my $cn = $config->inherit('cn') || $config->inherit('commonName');
  my $tag = $config->inherit('tag') || 'snmp';
  my $install = $config->inherit('install');

  $cn = (NetSNMP::Cert::is_server($tag) ? $host : $email);

  my $ca = $opts->{'with-ca'};
  NetSNMP::Cert::dprint("ca: $ca\n", $opts);
  my $ca_base = ::basename($ca, @NetSNMP::Cert::CRTSUFFIXES);
  NetSNMP::Cert::dprint("ca_base: $ca_base\n", $opts);
  my $ca_key = "$NetSNMP::Cert::PRIVDIR/$ca_base.key"; 

  my $csr = $opts->{'csr'};
  NetSNMP::Cert::dprint("csr: $csr\n", $opts);
  my $csr_base = ::basename($csr, @NetSNMP::Cert::CRTSUFFIXES);
  NetSNMP::Cert::dprint("csr_base: $csr_base\n", $opts);
  my $outdir = ($install ? $NetSNMP::Cert::CRTDIR : $NetSNMP::Cert::NEWCRTDIR);
  my $outCrt = "$outdir/$csr_base.crt";

  my $env = $app->opensslEnv($cn, $days, ".ca/$ca_base");

  NetSNMP::Cert::check_output_file($opts, $outCrt,
				   $config->inherit('interactive'),
				   $config->inherit('force'));

  # XXX - handle keyfile search??
  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";

  # $cmd .= ($isCa ? " -extensions v3_ca_sign_ca" : " -extensions v3_ca_sign");

  NetSNMP::Cert::dprint("signCsr: $cmd\n", $opts);

  NetSNMP::Cert::usystem("$cmd $opts->{out} $opts->{err}", $opts);

  NetSNMP::Cert::vprint("Signed Certificate Signing Request:\n", $opts);
  NetSNMP::Cert::vprint("  $csr\n", $opts);
  NetSNMP::Cert::vprint("with CA:\n", $opts);
  NetSNMP::Cert::vprint("  $ca\n", $opts);
  NetSNMP::Cert::vprint("  $ca_key\n", $opts);
  NetSNMP::Cert::vprint("Generated Certificate:\n", $opts);
  NetSNMP::Cert::vprint("  $outCrt\n", $opts);
}

sub show {
  my $app = shift;
  my $type = shift || 'certs';
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $stag = shift(@{$opts->{'cmdargs'}});
  my $fmt = NetSNMP::Cert::x509_format($opts) || '-subject';
  my $brief = $config->inherit('brief');
  my $output;
  my $cmd;

  my $cwd = ::getcwd();
  NetSNMP::Cert::dprint("show ($cwd):$type:$stag:$fmt\n", $opts);
  NetSNMP::Cert::vprint("$opts->{'tlsdir'}:\n", $opts) unless $brief;

  foreach my $c (NetSNMP::Cert::find_certs($opts, $type, $stag)) {
    print "\n$c:\n" unless $brief;
    $cmd = "openssl x509 -in '$c' -noout $fmt";
    NetSNMP::Cert::dprint("show: $cmd\n", $opts);

    $output = `$cmd`; chomp $output;
    NetSNMP::Cert::vwarn("show-$type failed ($?): $output\n", $opts) if $?;

    $output =~ s/^[^\n=]+=// if $brief;

    print "$output\n";
    print "\n" unless $brief;
  }
}

sub import_file {
  my ($file, $suffix, $targ, $rdir, $tag) = @_;

  if (NetSNMP::Cert::is_url($file)) {
    if ($NetSNMP::Cert::haveUserAgent) {

      require File::Temp;
      import File::Temp qw(tempfile);

      my ($fh, $newfilename) = tempfile(SUFFIX => $suffix);
      return if (!$fh || !$newfilename);
      my $ua = LWP::UserAgent->new;
      my $response = $ua->get($file);
      if ($response->is_success) {
	print $fh $response->decoded_content();
      } else {
	NetSNMP::Cert::vwarn("failed to download a certificate from $file");
	return;
      }
      $fh->close;
      $file = $newfilename;
    } else {
      NetSNMP::Cert::vwarn("LWP::UserAgent not installed: unable to import certificate");
      return;
    }
  }

  $file = NetSNMP::Cert::fq_rel_path($file, $rdir);
  die("file unreadable: $file\n") unless -r $file;


  if (! $targ) {
    $targ = (NetSNMP::Cert::is_ca_cert($file) ? $NetSNMP::Cert::CADIR : $NetSNMP::Cert::CRTDIR);
  }

  $targ .= "/" . $tag . $suffix if ($tag);
  ::copy($file, $targ);
}


sub import {
  my $app = shift;
  my $config = $app->{'config'};
  my $opts = $app->{'OPTS'};
  my $carg = shift(@{$opts->{'cmdargs'}});
  my $karg = shift(@{$opts->{'cmdargs'}});
  my $targ;

  if (not defined $carg) {
    NetSNMP::Cert::vwarn("import: no certificate supplied\n", $opts);
    NetSNMP::Cert::usage(1);
  }

  import_file($carg, '.crt', '',,
	      $opts->{'rdir'}, $opts->{'tag'});

  return unless defined $karg;

  import_file($karg, '.key', 'private',,
	      $opts->{'rdir'}, $opts->{'tag'});
}


package NetSNMP::Cert::Config;
use vars qw(@ISA);
@ISA = qw(NetSNMP::Cert::Obj);

sub new {
  my $class = shift;
  my $parent = shift;
  my $this = $class->SUPER::new('config', $parent);

  bless($this, $class);
}

sub parse {
  my $config = shift;
  my $app = $config->{'APP'};
  my $opts = $app->{'OPTS'};
  my $cfgfile = shift;
  $cfgfile ||= $opts->{'cfgfile'};

  return '0 but true' if $config->{'PARSED'};
  return '0 but true' unless defined $cfgfile;

  open( CONFIG, "<$cfgfile" )
    or die "error - could not open configuration file: $cfgfile";

  while (<CONFIG>) {
    next if /^\s*#/ or /^\s*$/;

    if (/^\s*(\w+)(?:\(([\)\(\d\,\.]+)\))?\s*=\s*{/) {
      my $obj = $1;

      NetSNMP::Cert::dprint("object: $obj ($2) = {\n", $opts);
      die "error - identity: indices not supported: $2" if defined $2;

      # Found an object.
      if ( $obj eq 'identity' ) {
	my $identity = NetSNMP::Cert::Identity::parse(*CONFIG, $config);
	my $id = $identity->{'id'};
	die "error - identity: 'id' not defined" unless defined $id;
	die "error - identity: duplicate '$id'" if exists $config->{$obj}{$id};
	$config->{$obj}{$id} = $identity;
      } else {
        die "error - unrecognized object ($1) at scope (config)";
      }
    } elsif (/^\s*(\w+)\s*=?\s*(.*?)\s*$/) {
      my $key = $1;
      my $val = $2;

      $val = $config->resolve($val) if $val =~ /\$\w+|\&\{.*?\}/;
      # Found a symbol.
      NetSNMP::Cert::dprint("  $key = $val\n", $opts);
      if ($key eq 'subjectAltName' or $key eq 'san') {
	push(@{$config->{$key}}, $val);
      } elsif ( defined $config->{$key} ) {
        die "error - duplicate symbol $key";
      } else {
        $config->{$key} = (defined $val ? NetSNMP::Cert::map_bool($val) : 1 );
      }
    } elsif (/^\s*env\s*(\w+=\S+)\s*$/) {
      # Found an environment variable.
      NetSNMP::Cert::dprint("$&\n", $opts);
      push(@{$config->{'env'}}, $1);
    } else {
      die("error in config file [$cfgfile:line $.]");
    }
  }

  NetSNMP::Cert::dprint("end parse config\n", $opts);
  close(CONFIG);

  # augment with any config directives supplied in opts
  foreach my $cfg (@{$opts->{'config'}}) {
    NetSNMP::Cert::dprint("augmenting config: $cfg\n", $opts);

    $config->autoSet($1, (defined($2) ? $2 : 1 ))
      if $cfg =~ /^\s*(\w+)\s*=?\s*(.*?)\s*$/;
  }
  $config->autoSet('PARSED', 1);
  return $config->{'PARSED'};
}

package NetSNMP::Cert::Identity;
use vars qw(@ISA);
@ISA = qw(NetSNMP::Cert::Obj);

sub new {
  my $class = shift;
  my $this = shift || $class->SUPER::new('identity', @_);
  my $ind = $this->{'INDEX'} || 1;

  $this->autoSet('name', "$this->{type}.$ind") unless exists $this->{'name'};

  $this->autoSet('LOG','') unless exists $this->{'LOG'};
  $this->autoSet('TTY_LOG','') unless exists $this->{TTY_LOG};

  bless($this, $class);
}


sub parse {
  my $FILE = shift;
  my $parent = shift;
  my $opts = $parent->inherit('OPTS');
  my $identity = new NetSNMP::Cert::Obj('identity', $parent);

  NetSNMP::Cert::dprint("parse identity\n", $opts);

  while (<$FILE>) {
    next if /^\s*#/ or /^\s*$/;

    if (/^\s*(\w+)\s*=\s*{/) {
      # Found an object.
      die "error - can't have nested $1";
    } elsif (/^\s*(\w+)\s*=?\s*(.*?)\s*$/) {
      my $key = $1;
      my $val = $2;

      # Found a symbol.
      NetSNMP::Cert::dprint("  $key = $val\n", $opts);

      $val = $identity->resolve($val) if $val =~ /\$\w+|\&\{.*?\}/;

      if ( $key eq 'subjectAltName' or $key eq 'san') {
	push(@{$identity->{$key}}, $val);
      } elsif ( defined $identity->{$key} ) {
        die "error - duplicate symbol $key";
      } else {
        $identity->{$key} = (defined $val ? NetSNMP::Cert::map_bool($val) : 1 );
      }
    } elsif (/\s*\}\s*\;/) {
      # End of definition.
      NetSNMP::Cert::dprint("end parse identity\n", $opts);
      return new NetSNMP::Cert::Identity($identity);
    } else {
      die("error in config file [$opts->{cfgfile}:line $.]");
    }
  }
  die "error - unexpected end of conf file";
}

package main;

my $app = new NetSNMP::Cert::App();
$app->init(@ARGV);
$app->run();

__END__
=pod

=head1   NAME

net-snmp-cert - Net-SNMP Certificate Management Tool

=head1   SYNOPSIS

=over

=item $ net-snmp-cert genca -I --cn ca-Net-SNMP

=item $ net-snmp-cert gencert -I -t snmpapp --with-ca ca-Net-SNMP

=item $ net-snmp-cert gencert -I -t snmpd --cn net-snmp.org

=item $ net-snmp-cert showcas

=item $ net-snmp-cert showcerts

=back

=head1   DESCRIPTION

net-snmp-cert creates, signs, installs and displays X.509
certificates used in the operation of Net-SNMP/(D)TLS.

=head1   SYNTAX

=over

=item net-snmp-cert [--help|-?]

=item net-snmp-cert [--version|-V]

=item net-snmp-cert genca    [<flags>] [<dn-opts>] [--with-ca <ca>]

=item net-snmp-cert gencert  [<flags>] [<dn-opts>] [--with-ca <ca>]

=item net-snmp-cert gencsr   [<flags>] [<dn-opts>] [--from-crt <crt>]

=item net-snmp-cert signcsr  [<flags>] [--install] --csr <csr> --with-ca <ca>

=item net-snmp-cert showca   [<flags>] [<format-opts>] [<file>|<search-tag>]

=item net-snmp-cert showcert [<flags>] [<format-opts>] [<file>|<search-tag>]

=item net-snmp-cert import   [<flags>] <file|url> [<key>]

=back

=head1   COMMANDS

=over

=item genca

generate a signed CA certificate suitable for signing other
certificates. default: self-signed unless --with-ca <ca> supplied

=item gencert

generate a signed certificate suitable for identification, or
validation. default: self-signed unless --with-ca <ca> supplied

=item gencsr

generate a certificate signing request. will create a new
key and certificate unless --from-crt <crt> supplied

=item signcsr

sign a certificate signing request specified by --csr <csr>
with the CA certificate specified by --with-ca <ca>

=item import

import an identity or CA certificate, optionally import <key>
if an URL is passed, will attempt to import certificate from site

=item showca, showcert

show CA or identity certificate(s). may pass fully qualified
file or directory name, or a search-tag which prefix matches
installed CA or identity certificate file name(s)
see FORMAT OPTIONS to specify output format


=back

=head1   FLAGS

=over

=item -?, --help            -- show this text and exit

=item -V, --version         -- show version string and exit

=item -D, --debug           -- raise debug level (-D -D for higher level)

=item -F, --force           -- force overwrite of existing output files

=item -I, --nointeractive   -- non-interactive run (default interactive)

=item -Q, --noverbose       -- non-verbose output (default verbose)

=item -C, --cfgdir   <dir>  -- configuration dir (see man(5) snmp_config)

=item -T, --tlsdir   <dir>  -- root for cert storage (default <cfgdir>/tls)

=item -f, --cfgfile  <file> -- config (default <cfgdir>/net-snmp-cert.conf)

=item -i, --identity <id>   -- identity to use from config

=item -t, --tag      <tag>  -- application tag (default 'snmp')

=item --<cfg-param>[=<val>] -- additional config params

=back

=head1   CERTIFICATE OPTIONS (<cert-opts>)

=over

=item -a, --with-ca <ca>    -- CA certificate used to sign request

=item -A, --ca <ca>         -- explicit output CA certificate

=item -r, --csr <csr>       -- certificate signing request

=item -x, --from-crt <crt>  -- input certificate for current operation

=item -X, --crt <crt>       -- explicit output certificate

=item -y, --install         -- install result in local repository

=back

=head1   DISTINGUISHED NAME OPTIONS (<dn-opts>)

=over

=item -e, --email <email>       -- email name

=item -h, --host <host>         -- DNS host name, or IP address

=item -d, --days <days>         -- number of days certificate is valid

=item -n, --cn <cn>             -- common name (CN)

=item -o, --org <org>           -- organiztion name

=item -u, --unit <unit>         -- organiztion unit name

=item -c, --country <country>   -- country code (e.g., US)

=item -p, --province <province> -- province name (synomynous w/ state)

=item -p, --state <state>       -- state name (synomynous w/ province)

=item -l, --locality <locality> -- locality name (e.g, town)

=item -s, --san <san>           -- subjectAltName, repeat for each <san>

=over 2

=item    <san> value format (<FMT>:<VAL>):

=over 3

=item       dirName:/usr/share/snmp/tls

=item       DNS:net-snmp.org

=item       email:admin@net-snmp.org

=item       IP:192.168.1.1

=item       RID:1.1.3.6

=item       URI:http://net-snmp.org

=back

=back

=back

=head1   FORMAT OPTIONS (<format-opts>)

=over

=item --brief        -- minimized output (values only where applicable)

=item --text         -- full text description

=item --subject      -- subject description

=item --subject_hash -- hash of subject for indexing

=item --issuer       -- issuer description

=item --issuer_hash  -- hash of issuer for indexing

=item --fingerprint  -- SHA1 digest of DER

=item --serial       -- serial number

=item --modulus      -- modulus of the public key

=item --dates        -- start and end dates

=item --purpose      -- displays allowed uses

=item --C            -- C code description

=back

=head1   OPERATIONAL NOTES


=head2   Interactive Mode

The application operates in interactive mode by default. In this mode
basic operations of offered and required input is queried through the
command line. 

Typical <tab> completion is provided when possible and field specific
help may be obtained by entering '?'.

To turn off interactive mode, supply '--nointeractive' or '-I' on the
initial command line. Equivalantly, 'interactive = false' maybe placed
in the configuration file (see below).

=head2   Configuration

A configuration file may be supplied on the command line or found in a
default location (<snmpconfpath>/net-snmp-cert.conf). This file may
contain configuration parameters equivalent to those supplied on the
command line and effectively provides default values for these
values. If a command line parameter is supplied it will override the
value in the config file. If neither is present then an application
value will be used.

=head2   Certificate Naming

Filenames of created certificates, as stored in the configuration
directory, are chosen in the following manner. If and application tag
is supplied, it will be used as the basename for the certificate and
key generated. Otherwise, for CA certificates, the basename will be
derived from the Common Name. For non-CA certificates the application
tag defaults to 'snmp' which will then be used as the basename of the
certificate and key.

=head1   EXAMPLES

=over

=item net-snmp-cert genca --cn ca-net-snmp.org --days 1000

=item net-snmp-cert genca -f .snmp/net-snmp-cert.conf -i nocadm

=item net-snmp-cert gencert -t snmpd --cn host.net-snmp.org

=item net-snmp-cert gencsr -t snmpapp

=item net-snmp-cert signcsr --csr snmpapp --with-ca ca-net-snmp.org

=item net-snmp-cert showcerts --subject --issuer --dates snmpapp

=item net-snmp-cert showcas --fingerprint ca-net-snmp.org --brief

=item net-snmp-cert import ca-third-party.crt

=item net-snmp-cert import signed-request.crt signed-request.key

=back

=head1 COPYRIGHT

Copyright (c) 2010 Cobham Analytic Solutions - All rights reserved.
Copyright (c) 2010 G. S. Marzot -  All rights reserved.

=head1   AUTHOR

G. S. Marzot (marz@users.sourceforge.net)

=cut

