package CSP; use strict; use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); require Exporter; require AutoLoader; use IO::File; use Term::Prompt; use POSIX qw(strftime); use Date::Calc qw(Day_of_Week Gmtime Add_Delta_Days Add_Delta_DHMS); use Sys::Hostname; @ISA = qw(Exporter AutoLoader); # Items to export into callers namespace by default. Note: do not export # names by default without a very good reason. Use EXPORT_OK instead. # Do not simply export all your public functions/methods/constants. @EXPORT = qw(); @EXPORT_OK = qw($_openssl); $VERSION = '0.34'; # Preloaded methods go here. # Autoload methods go after =cut, and are processed by the autosplit program. $CSP::_openssl='openssl'; sub message { sprintf "[CSP][%-8s] %s",$_[0]->{name},$_[1]; } sub die { die CSP::message(@_)."\n"; } sub warn { warn CSP::message(@_)."\n"; } sub new { my $self = shift; my $class = ref $self || $self; my $dir = shift; my $name = shift; my $me = bless { dir=>$dir,name=>$name },$class; open ALIASES,"$dir/etc/aliases.txt"; while () { chomp; next unless /\s*([^:]+)\s*:\s*([^:]+)\s*/; $me->{aname}->{lc($1)} = $2; $me->{alias}->{lc($2)} = $1; } close ALIASES; $me->{openssl} = CSP::OpenSSL->new($me); $me; } sub DESTROY { my $self = shift; foreach my $file (keys %{$self->{_tmpfiles}}) { unlink $file; } my $dir = $self->caDir(); if (-d $dir) { unlink "$dir/serial.old"; unlink "$dir/index.txt.old"; } } sub setCA { my $on = $_[0]->{name}; $_[0]->{name}= $_[1]; return $on; } sub addFile { my $self = shift; my $cf = shift; my $fn = shift; open IN,$fn or die "unable to open $fn for reading"; while () { next if /^\#/; $cf->print($_); } close IN; } sub _rewrite { my $vars = shift; my $line = shift; while ($line =~ s/%{([a-zA-Z0-9_\.]+)}/$vars->{$1}/eg) { } $line; } sub mppFile { my $self = shift; my $cf = shift; my $vars = shift; my $fn = shift; my $ctx = CSP::Stack->new(1); open IN,$fn or $self->die("mpp: unable to open $fn for reading"); my $depth = 0; while () { next if /^\#/; SWITCH: { last SWITCH unless /^(%if|%ifdef|%endif)/ or $ctx->doPrint(); if (/^%ifdef\s+([A-Za-z0-9_\.]+)/) { $ctx->push(defined $vars->{$1}); last SWITCH; } if (/^%ifndef\s+([A-Za-z0-9_\.]+)/) { $ctx->push(not defined $vars->{$1}); last SWITCH; } if (/^%if\s+(.+)$/) { my $expr = $1; $expr =~ s/%{([A-Za-z0-9_\.]+)}/"\$vars->{\"$1\"}"/eg; my $result = eval $expr; $self->die("$@") if $@; $ctx->push($result); last SWITCH; } $ctx->pop(),last SWITCH if /^%endif/; $self->mppFile($cf,$vars,$1),last SWITCH if /^%include\s+(.+)/; print $cf &_rewrite($vars,$_); } } close IN; } sub writeConfig { my $self = shift; my $cmd = shift; my $args = shift; my $cadir = "$self->{dir}/csp/$self->{name}"; $ENV{TMPDIR} ||= ("$self->{dir}/tmp" || '/tmp'); my $cff = $self->tempFile("csp","conf"); my $cf = IO::File->new(); eval { $cf->open(">$cff") or die "Unable to open $cff for writing"; my $date = localtime(); $cf->print(<print(<print("[ oids ]\n"); $self->addFile($cf,"$self->{dir}/etc/oids.conf"); $cf->print("\n[ csp ]\n\n"); my ($k,$v); while (($k,$v) = each %{$args}) { $cf->print("$k\t= $v\n") if ($k ne 'keypass' && $k ne 'capass'); } $cf->print("home\t= $self->{dir}\n"); $cf->print("ca\t= $self->{name}\n"); $cf->print("\n"); ## Main sections $cf->print(<{name} [ $self->{name} ] dir = \${csp::home}/csp/\${csp::ca} certs = \$dir/certs database = \$dir/index.txt certificate = \$dir/ca.crt serial = \$dir/serial private_key = \$dir/private/ca.key RANDFILE = \$dir/private/.rand x509_extensions = extensions default_days = 365 default_crl_days = 30 default_md = sha1 preserve = no policy = policy [ req ] default_bits = \${csp::keysize} default_keyfile = privkey.pem distinguished_name = req_dn x509_extensions = extensions string_mask = nombstr prompt = no default_md = sha1 EOC ## Extension based on command my $type = $args->{type}; my $name = $self->{name}; if ($cmd eq 'x509' || $cmd eq 'req' || $cmd eq 'ca') { $cf->print("[ policy ]\n\n"); foreach my $attr (keys %{$args->{name_attributes}}) { next unless $attr; if ($self->{aname}->{lc($attr)}) { $cf->print("$self->{aname}->{lc($attr)} = optional\n") } else { $cf->print("$attr = optional\n"); } } ## Define a few CPP/MPP-style variables and run the prototype file $args->{uc("type_$type")}++; foreach my $x (qw(email url ip dns)) { $args->{uc($x)} = $args->{$x}; } $cf->print("\n\n"); if ($name) { my $econf = "$self->{dir}/csp/$name/extensions.conf"; $econf = "$self->{dir}/etc/extensions.conf" unless -f $econf; $self->mppFile($cf,$args,$econf); } $cf->print("\n\n"); if ($name) { my $econf = "$self->{dir}/csp/$name/crl_extensions.conf"; $econf = "$self->{dir}/etc/crl_extensions.conf" unless -f $econf; $self->mppFile($cf,$args,$econf); } $cf->print("\n"); } my $dn = $args->{dn}; if ($dn) { my %acount; $cf->print("[ req_dn ]\n\n"); foreach my $rdn (split /\s*,\s*/,$dn) { next unless $rdn =~ /^([^=]+)\s*=\s*([^=]+)$/; my $n = exists $self->{aname}->{lc($1)} ? $self->{aname}->{lc($1)} : $1; $args->{_nrdn}++; ## At the end of the run _nrdn contains the ## number of newlines to send to openssl. my $pos = $acount{$n}++; $cf->print($pos.".${n}\t\t= $2\n"); } $cf->print("\n"); } $cf->close; }; if ($@) { $cf->close; #unlink $cff; ## uncomment when debugging $self->die($@); } return $cff; } sub createFiles { my $self = shift; my $dir = $self->caDir(); mkdir $dir,00755; mkdir "$dir/certs",00755; open SERIAL,">$dir/serial"; print SERIAL "01\n"; close SERIAL; mkdir "$dir/tmp",00700; system('touch',"$dir/index.txt"); mkdir "$dir/private",00700; mkdir "$dir/private/keys",00700; system('cp','-p',"$self->{dir}/etc/extensions.conf","$dir/extensions.conf"); system('cp','-p',"$self->{dir}/etc/crl_extensions.conf","$dir/crl_extensions.conf"); system('cp','-rp',"$self->{dir}/etc/public_html","$dir/"); } sub caDir { my $self = shift; my $name = $self->{name}; $self->die("CA Name not set") unless $name; "$self->{dir}/csp/$self->{name}"; } sub confirm { my $self = shift; my $comment = shift; my $fail = shift; $self->die($fail) unless Term::Prompt::prompt("y",$comment,"","n"); } sub getPassword { my $self = shift; my $comment = shift; my $reenter = shift; my ($pw,$pwr); system("stty -echo") && $self->die("Unable to configure tty for password entry"); print STDERR $self->message("$comment: "); chop($pw = ); print STDERR "\n"; if ($reenter) { $pwr = $self->getPassword("Re-enter $comment"); } else { $pwr = $pw; } system("stty echo") && $self->die("Unable to configure tty for password entry"); $self->die("Passwords do not match") unless $pw eq $pwr; return undef if length($pw) == 0; $pw; } sub genkey { my $self = shift; my $args = shift; $self->die("Required parameter keyfile missing") unless $args->{keyfile}; $args->{keysize} = 1024 unless $args->{keysize} > 0; $args->{keypass} = $self->getPassword("Private key password",1) unless $args->{keypass}; my $cmd = "-out $args->{keyfile} $args->{keysize}"; $cmd = "-des3 -passout pass:$args->{keypass} ".$cmd if defined($args->{keypass}); $self->{openssl}->cmd('genrsa',$cmd,$args); } sub create { my $self = shift; my $args = shift; $self->createFiles(); $self->warn("Successfully created CA $self->{name}") if $args->{verbose}; } sub delete { my $self = shift; my $args = shift; my $dir = $self->caDir(); system('rm','-rf',$dir); $self->warn("Successfully deleted CA $self->{name}") if $args->{verbose}; } sub init { my $self = shift; my $args = shift; my $dir = $self->caDir(); $self->die("You must create the CA before it can be initialized") unless -d $dir; if ($args->{crtfile}) { system('cp',$args->{crtfile},"$dir/ca.crt"); $self->warn("Successfully initialized CA $self->{name}") if $args->{verbose}; } else { $self->die("Required parameter dn missing") unless $args->{dn}; $args->{type} = 'root' unless $args->{type}; $args->{days} = 3 * 365 unless $args->{days}; my $cakey = "$dir/private/ca.key"; my $cacert = "$dir/ca.crt"; unless (-f $args->{keyfile}) { ## Generate the CA key $self->warn("Generating CA key") if $args->{verbose}; $args->{keyfile} = $cakey; defined $self->genkey($args) or $self->die("Unable to generate CA key in $cakey"); $self->die("CA key must have a password") unless defined($args->{keypass}); } $args->{capass} = $args->{keypass}; ## Generate and optionally self-sign the request my $process; my $what; my $common_args = "-$args->{digest} -days $args->{days} ". " -key $cakey -passin pass:$args->{keypass}"; if ($args->{csrfile}) { $self->{openssl}->cmd('req',"-new $common_args -out $args->{csrfile}",$args); $what = "generated CA request for"; } else { $self->{openssl}->cmd('req',"-x509 $common_args -new -out $cacert",$args); $what = "initialized self-signed"; } $self->warn("Successfully $what CA $self->{name}") if $args->{verbose}; } } sub checkCA { my $self = shift; my $dir = $self->caDir(); $self->die("Uninitialized CA: missing or unreadable ca certificate") unless -r "$dir/ca.crt"; $self->die("Uninitialized CA: missing or unreadable ca private key") unless -r "$dir/private/ca.key"; $dir; } sub tempFile { my $self = shift; my $dir = $self->caDir(); my $base = shift; my $ext = shift; my $tmp = $ENV{TMPFILE} || "$dir/tmp"; my $file = "$dir/tmp/$base-$$.$ext"; $self->{_tmpfiles}->{$file}++; $file; } sub unTempFile { my $self = shift; my $file = shift; delete $self->{_tmpfiles}->{$file}; } sub keyFile { my $self = shift; my $dir = $self->caDir(); my $serial = shift; "$dir/private/keys/$serial.key"; } sub certFile { my $self = shift; my $dir = $self->caDir(); my $serial = shift; "$dir/certs/$serial.pem"; } sub request { my $self = shift; my $args = shift; $self->die("Required parameter dn missing") unless $args->{'dn'}; my $dir = $self->checkCA(); $args->{type} || $self->die("Required parameter 'type' not defined"); $args->{csrfile} = $self->tempFile("request","csr") unless $args->{csrfile}; $args->{keyfile} = $self->tempFile("request","key") unless $args->{keyfile}; ## Generate a key unless one already exists if (! -r $args->{keyfile}) { $self->warn("Generating new key") if $args->{verbose}; $self->genkey($args) or $self->die("Unable to generate key in $args->{keyfile}"); } ## Generate a certificate request $self->warn("Create certificate request for $args->{dn}") if $args->{verbose}; my $cmd = "-new -$args->{digest} -key $args->{keyfile} -out $args->{csrfile}"; $cmd .= " -passin pass:$args->{keypass}" if defined($args->{keypass}); $self->{openssl}->cmd('req',$cmd,$args); } sub updatedb { my $self = shift; my $args = shift; my $dir = $self->checkCA(); $args->{capass} = $self->getPassword("CA Private key password") unless $args->{capass}; $self->die("CA key must have a password") unless defined($args->{capass}); $self->{openssl}->cmd('ca',"-batch -passin pass:$args->{capass} -verbose -updatedb",$args); } sub gencrl { my $self = shift; my $args = shift; my $dir = $self->checkCA(); $args->{capass} = $self->getPassword("CA Private key password") unless $args->{capass}; $self->die("CA key must have a password") unless defined($args->{capass}); my $days = $args->{crldays} || 30; my $hours = $args->{crlhours}; my $time; if ($hours) { $time = "-crlhours $hours" } else { $time = "-crldays $days"; } my $common = "-batch -md $args->{digest} -passin pass:$args->{capass} -gencrl $time"; ## Generate both version 1 and version 2 (with extensions) CRLs ## and convert from PEM to DER format $self->{openssl}->cmd('ca',"$common -out $dir/crl-v1.pem",$args); $self->{openssl}->cmd('crl',"-outform DER -out $dir/crl-v1.crl -in $dir/crl-v1.pem"); $self->{openssl}->cmd('ca',"$common -crlexts crl_extensions -out $dir/crl-v2.pem",$args); $self->{openssl}-> cmd('crl',"-outform DER -out $dir/crl-v2.crl -in $dir/crl-v2.pem"); } sub list { my $self = shift; my $args = shift; my $eclass = shift; my $dir = $self->checkCA(); my $db = "$dir/index.txt"; open DB,$db; my @out; while () { chomp; my @row = split /\t/; next if ($row[0] ne 'V' && !$args->{all}); next if ($args->{serial} && $row[3] != $args->{serial}); my $entity = $eclass->new($self,\@row,$args->{xinfo},$args->{contents}); push(@out,$entity) if ref $entity; } close DB; @out; } sub _isodateandtime { my $date = shift; my @parts = split /\s+/,$date; #warn "$parts[0] $parts[1] $parts[3]"; my ($y,$m,$d) = Date::Calc::Parse_Date("$parts[0] $parts[1] $parts[3]"); $m = sprintf("%02d",$m); $d = sprintf("%02d",$d); my $t = $parts[2]; $t =~ s/://og; "$y$m$d$t"; } sub genPublic { my $self = shift; my $args = shift; $args->{all} = 1; $args->{xinfo} = 1; my $dir = $self->checkCA(); $self->updatedb($args); $args->{export} = "/mnt/floppy" unless $args->{export}; $self->die("Not a directory: $args->{export}") unless -d $args->{export}; my $odir = $args->{export}; mkdir "$odir/certs",00755; my $expired_count = 0; my $revoked_count = 0; my $valid_count = 0; my $valid_html = "\n"; my $revoked_html = "
\n"; my $expired_html = "
\n"; my $date = localtime(time); my $cinfo = $self->certinfo("$dir/ca.crt"); open XML,">$odir/certdb.xml"; print XML < EOXML foreach my $e ($self->list($args,'CSP::Entity')) { my $html = $self->getCertHTMLOutFile($e,$odir); my $vars = { DATE=>$date, HOSTNAME=>hostname, SUBJECT_SERIAL => $e->{serial}, SUBJECT_DN => $e->{subject}, ISSUER_DN => $cinfo->{subject}, SUBJECT_SHA1 => $e->{info}->{fingerprint_sha1}, SUBJECT_MD5 => $e->{info}->{fingerprint_md5}, SUBJECT_NOTBEFORE => $e->{info}->{notbefore}, SUBJECT_NOTAFTER => $e->{info}->{notafter} }; my $serial = $e->{serial}; my $from = _isodateandtime($e->{info}->{notbefore}); my $to = _isodateandtime($e->{info}->{notafter}); print XML < $e->{subject} $cinfo->{subject} $e->{info}->{fingerprint_sha1} $e->{info}->{fingerprint_md5} $e->{info}->{notbefore} $e->{info}->{notafter} EOXML if (-f "$dir/p12/$serial.p12") { $vars->{SUBJECT_PKCS12} = "$serial.p12"; system('cp',"$dir/p12/$serial.p12","$odir/certs/$serial.p12"); } my $file = $self->certFile($serial); $self->{openssl}-> cmd('x509',"-in $file -outform DER -out $odir/certs/$serial.crt",{noconfig=>1}); system('cp',$file,"$odir/certs/$serial.pem"); $self->mppFile($html,$vars,"$dir/public_html/certs/cert.html.mpp"); if ($e->{status} eq 'V') { $valid_html .= $self->genHTMLTableRow($args,$e); $valid_count++; } elsif ($e->{status} eq 'R') { $revoked_html .= $self->genHTMLTableRow($args,$e); $revoked_count++; } elsif ($e->{status} eq 'E') { $expired_html .= $self->genHTMLTableRow($args,$e); $expired_count++; } } print XML < EOXML close XML; $valid_html .= "
\n"; $revoked_html .= "\n"; $expired_html .= "\n"; my $pp = $self->{openssl}-> cmd('x509',"-inform PEM -in $dir/ca.crt -outform PEM -out $odir/ca.crt",{noconfig=>1}); #system('cp',"$dir/ca.crt","$odir/ca.crt"); system('cp',"$dir/crl-v1.crl","$odir/crl-v1.crl"); system('cp',"$dir/crl-v2.crl","$odir/crl-v2.crl"); my $vars = { DATE=>$date, HOSTNAME=>hostname, VALID=>$valid_html, VALID_COUNT=>$valid_count, REVOKED=>$revoked_html, REVOKED_COUNT=>$revoked_count, EXPIRED=>$expired_html, EXPIRED_COUNT=>$expired_count, SUBJECT_SERIAL=>$cinfo->{serial}, SUBJECT_NOTAFTER=>$cinfo->{notafter}, SUBJECT_NOTBEFORE=>$cinfo->{notbefore}, SUBJECT_DN=>$cinfo->{subject}, SUBJECT_MD5=>$cinfo->{fingerprint_md5}, SUBJECT_SHA1=>$cinfo->{fingerprint_sha1} }; for my $infile ($self->getTemplates("$dir/public_html/certs",".html.mpp")) { next if $infile eq 'cert.html.mpp'; ## The generic certificate template my $html = $self->getHTMLOutFile("$odir/certs/$infile"); $self->mppFile($html,$vars,"$dir/public_html/certs/$infile"); $html->close(); } for my $infile ($self->getTemplates("$dir/public_html",".html.mpp")) { my $html = $self->getHTMLOutFile("$odir/$infile"); $self->mppFile($html,$vars,"$dir/public_html/$infile"); $html->close(); } } sub genHTMLTableRow { my $self = shift; my $args = shift; my $e = shift; my $ser = $e->{serial}; my $pem = $ser.'.html'; "$ser$e->{subject}\n"; } sub getTemplates { my $self = shift; my $dir = shift; my $ext = shift; opendir(TD,$dir) or $self->die("Unable to open $dir"); my @dirs = readdir(TD); closedir TD; grep { /$ext$/ } @dirs; } sub getCertHTMLOutFile { my $self = shift; my $e = shift; my $dir = $self->checkCA(); my $odir = shift; my $serial = $e->{serial}; my $html = IO::File->new(); $html->open(">$odir/certs/$serial.html") or $self->die("Unable to open $odir/certs/$serial.html for writing: $!"); $html; } sub getHTMLOutFile { my $self = shift; my $fn = shift; my $html = IO::File->new(); my ($base) = $fn =~ /(.+)\.html\.mpp$/; $self->die("Filename missing .html.mpp extension: $fn") unless $base; $html->open(">$base.html") or $self->die("Unable to open $base.html for writing: $!"); $html; } sub dump { my $self = shift; my $args = shift; my $dir = $self->checkCA(); print $self->{openssl}->cmd('x509',"-noout -text -in $dir/ca.crt",{noconfig=>1}); } sub caBundle { my $self = shift; my $args = shift; open BUNDLE,">$args->{bundle}"; my $date = localtime(); print BUNDLE "##\n"; print BUNDLE "## Certificate bundle for use with OpenSSL\n"; print BUNDLE "## Generated by CSP ($date)\n"; print BUNDLE "##\n"; foreach my $certfile (@_) { my $info = $self->certinfo($certfile); print BUNDLE "\n$info->{subject}\n"; print BUNDLE "=========================================\n"; print BUNDLE "MD5 Fingerprint: $info->{fingerprint_md5}\n"; print BUNDLE "PEM Data:\n"; open CERT,$certfile; while () { print BUNDLE $_; } close CERT; my $process = $self->{openssl}-> cmd('x509',"-noout -text -in $certfile",{noconfig=>1}); my $fh = $process->handle(); while (<$fh>) { print BUNDLE $_; } $process->closeok(); } close BUNDLE; } sub revoke { my $self = shift; my $args = shift; my $dir = $self->checkCA(); my $serial = $args->{serial}; my $file = $self->certFile($serial); $self->die("Serial $serial not issued by this CA") unless -f $file; if ($args->{confirm}) { $self->dumpcert($file); $self->confirm("Really revoke this?","Bye..."); } $args->{capass} = $self->getPassword("CA Private key password") unless $args->{capass}; $self->die("CA key must have a password") unless defined($args->{capass}); $self->{openssl}->cmd('ca',"-passin pass:$args->{capass} -batch -updatedb -revoke $file",$args); } sub _time { my ($self,$Dd,$Dh,$Dm,$Ds) = @_; my ($year,$month,$day,$hour,$min,$sec,$doy,$dow,$dst) = Gmtime(); my ($nyear,$nmonth,$nday,$nhour,$nmin,$nsec) = Add_Delta_DHMS($year,$month,$day,$hour,$min,$sec,$Dd,$Dh,$Dm,$Ds); my $tmp = sprintf("%02d%02d%02d%02d%02d%02dZ",$nyear,$nmonth,$nday,$nhour,$nmin,$nsec); $tmp =~ s/^[0-9][0-9]//; $tmp; } sub issue { my $self = shift; my $args = shift; my $dir = $self->checkCA(); $args->{type} = 'user' unless $args->{type}; unless ($args->{csrfile}) { $args->{csrfile} = $self->tempFile("request","csr"); eval { $self->request($args); $args->{p12pass} = $args->{keypass}; }; if ($@) { $self->die("Unable to generate request: ".$self->exm($@)); } } # $self->die("No csr file $args->{csrfile}") # unless -r $args->{csrfile}; eval { if ($args->{confirm}) { $self->dumpreq($args->{csrfile}); $self->confirm("Really sign this?","Bye..."); } $self->warn("Signing request") if $args->{verbose}; my $serial; open SERIAL,"$dir/serial"; chomp($serial = ); close SERIAL; $args->{capass} = $self->getPassword("CA Private key password") unless $args->{capass}; $self->die("CA key must have a password") unless defined($args->{capass}); $args->{startdate} = $self->_time() unless $args->{startdate}; $args->{enddate} = $self->_time($args->{days} or 365,$args->{hours},$args->{mins},$args->{secs}) unless $args->{enddate}; $self->{openssl}->cmd('ca', "-batch -md $args->{digest} -startdate $args->{startdate} ". "-enddate $args->{enddate} ". "-passin pass:$args->{capass} -preserveDN -outdir $dir/certs ". "-in $args->{csrfile}",$args); rename $args->{keyfile},"$dir/private/keys/$serial.key"; $self->unTempFile($args->{keyfile}); $args->{serial} = $serial; }; if ($@) { $self->die("Unable to sign request: ".$self->exm($@)); } } sub export_pkcs12 { my $self = shift; my $args = shift; my $dir = $self->checkCA(); my $serial = $args->{serial}; $self->die("Missing serial number") unless $serial; $args->{keypass} = $self->getPassword("Private key password") unless defined($args->{keypass}); $args->{p12pass} = $self->getPassword("PKCS12 export password") unless defined($args->{p12pass}); my $othercerts; if (-f "$dir/certpath.crt") { $othercerts = "-certfile $dir/certpath.crt"; } else { $othercerts = "-certfile $dir/ca.crt"; } my $certFile = $self->certFile($serial); my $keyFile = $self->keyFile($serial); $self->die("The private key of $serial is not on-line") unless -f $keyFile; $self->die("The certificate of $serial is not on-line") unless -f $certFile; eval { mkdir "$dir/p12",00755 unless -d "$dir/p12"; my $p12File = "$dir/p12/$serial.p12"; my $cmd = "-export -des3 $othercerts -inkey $keyFile -in $certFile -out $p12File"; $cmd .= " -passout pass:$args->{p12pass}" if defined($args->{p12pass}); $cmd .= " -passin pass:$args->{keypass}" if defined($args->{keypass}); $self->{openssl}->cmd('pkcs12',$cmd,$args); }; if ($@) { $self->die("Unable to create pkcs12 object: ".$self->exm($@)); } } sub ppSubject { my $self = shift; my $dn = shift; my @rdns = split /\//,$dn; shift @rdns; foreach my $aname (keys %{$self->{alias}}) { map { s/$aname/$self->{alias}->{$aname}/ig; } @rdns; } join(',',@rdns); } sub getDN { my $self = shift; my $x = shift; my $args = shift; my $dn; SWITCH: { $dn = $x,last SWITCH if $x =~ /=/; ## probably a distinguished name $dn = $self->email2DN($1,$2,$args),last SWITCH if $x =~ /([^@]+)\@([^@]+)/; ## probably an email address $dn = $self->domainName2DN($x,$args),last SWITCH if $x =~/\./; ## probably a DNS domain name $self->die("Unknown name form: $x"); } foreach my $av (split /\s*[,\/]\s*/,$dn) { $self->die("Bad X.501 name $dn") unless $av =~ /([a-zA-Z]+)\s*=\s*([^=]+)/; my $tmp = $1; $tmp =~ s/^\s+//og; $tmp =~ s/\s+$//og; next unless $tmp; $args->{name_attributes}->{$tmp}++; } $dn; } sub email2DN { my $self = shift; my ($lp,$dp,$args) = @_; #my $attr = 'uid'; #$attr = 'CN' if $lp =~ /[-\.\_]/; my $attr = 'CN'; $args->{email} = "$lp\@$dp"; return $self->domainName2DN($dp).",$attr=$lp"; } sub domainName2DN { my $self = shift; my $dns = shift; my $args = shift; $dns =~ s/\.$//o; $args->{ip} = $dns; my @dn = split /\./,$dns; @dn = map { "dc=$_" } @dn; join(',',reverse @dn); } sub dumpcert { my $self = shift; my $certfile = shift; print $self->{openssl}-> cmd('x509', "-text -in $certfile -noout -nameopt RFC2253", {noconfig=>1,verbose=>1}); } sub dumpreq { my $self = shift; my $reqfile = shift; print $self->{openssl}-> cmd('req', "-text -in $reqfile -noout", {noconfig=>1,verbose=>1}); } sub exm { my $self = shift; my $ex = shift; $ex =~ s/\[CSP\]\[.+\]//og; $ex; } sub certinfo { my $self = shift; my $certfile = shift; my (%info,$fh,$process); $info{hash} = $self->{openssl}->cmd('x509',"-noout -hash -in $certfile",{noconfig=>1}); local $_ = $self->{openssl}->cmd('x509',"-noout -serial -dates -subject -issuer -in $certfile",{noconfig=>1}); while ($_) { s/^\s*\n//o; if (s/^subject=\s*(.+)//o) { $info{subject}=$1; $info{subject} =~ s/\//,/og; $info{subject} =~ s/^,//og; } elsif (s/^issuer=\s*(.+)//o) { $info{issuer}=$1; $info{issuer} =~ s/\//,/og; $info{issuer} =~ s/^,//og; } elsif (s/^notBefore=\s*(.+)//o) { $info{notbefore}=$1; } elsif (s/^notAfter=\s*(.+)//o) { $info{notafter}=$1; } elsif (s/^serial=\s*(.+)//o) { $info{serial}=$1; } } $_ = $self->{openssl}->cmd('x509',"-noout -md5 -fingerprint -in $certfile",{noconfig=>1}); while ($_) { chomp; s/^\s*\n//o; $info{fingerprint_md5}=$1,last if /MD5 Fingerprint=(.+)/o; } $_ = $self->{openssl}->cmd('x509',"-noout -sha1 -fingerprint -in $certfile",{noconfig=>1}); while ($_) { chomp; s/^\s*\n//o; $info{fingerprint_sha1}=$1,last if /SHA1 Fingerprint=(.+)/; } \%info; } package CSP::Stack; # just a context stack sub new { my $self = shift; my $class = ref $self || $self; my @stack = @_; bless \@stack,$class; } sub push { push @{$_[0]},$_[1]; } sub pop { pop @{$_[0]}; } sub doPrint { my @s = @{$_[0]}; $_[0]->[$#s]; } package CSP::Entity; # Just a db object @CSP::Entity::ISA = qw(CSP); use Date::Calc qw(Day_of_Week Gmtime Add_Delta_Days Add_Delta_DHMS); use POSIX qw(strftime); sub parse_date { my $str = shift; my ($y,$mon,$mday,$h,$m,$s) = $str =~ /([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})Z$/; $y += 100; my $wday = Day_of_Week($y+1999,$mon,$mday); my @date = ($s,$m,$h,$mday,$mon-1,$y,$wday,0); \@date; } sub new { my $self = shift; my $class = ref $self || $self; my $csp = shift; my $row = shift; my $getinfo = shift; my $getcontents = shift; my $serial = $row->[3]; my $file = ($row->[4] && $row->[4] ne 'unknown' ? $row->[4] : $csp->certFile($serial)); bless { csp => $csp, status => $row->[0], expires => parse_date($row->[1]), revoked => ($row->[2] ? parse_date($row->[2]) : undef), serial => $serial, file => $file, subject => $csp->ppSubject($row->[5]), info => ($getinfo ? $csp->certinfo($file) : {}), getcontents => $getcontents },$class; } my %_status = ('V' => 'Valid','R' => 'Revoked'); sub info { $_[0]->{info}->{$_[1]}; } sub dump { my $self = shift; printf "%-8s: %s\n",'Serial',$self->{serial}; my $status = $self->{status}; printf "%-8s: %s\n",'Status',exists $_status{$status} ? $_status{$status} : "Unknown"; printf "%-8s: %s\n",'Subject',$self->{subject}; printf "%-8s: %s\n",'Expires',strftime("%a %b %e %H:%M:%S %Y",@{$self->{expires}}); printf "%-8s: %s\n",'Revoked',strftime("%a %b %e %H:%M:%S %Y",@{$self->{revoked}}) if $self->{revoked}; printf "%-8s: %s\n",'SHA1',$self->info('fingerprint_sha1') if $self->info('fingerprint_sha1'); printf "%-8s: %s\n",'MD5',$self->info('fingerprint_md5') if $self->info('fingerprint_md5'); if ($self->{getcontents}) { $self->dumpcert($self->{file}); } print "\n"; } package CSP::OpenSSL; @CSP::OpenSSL::ISA = qw(CSP); my $_unset=<',\${$me{_out}},'2>&1',debug=>0) or die "Cannot start $ENV{OPENSSL}: $!\n"; bless \%me,$class; } sub cmd { my $self = shift; my $cmd = shift; my $cmdline = shift; my $args = shift; my $conf; my $cfgcmd; if ( (grep $_ eq $cmd,qw(req ca)) && !$args->{noconfig}) { $conf = $self->{csp}->writeConfig($cmd,$args); $self->{csp}->die("Unable to write configuration file") unless -f $conf; $cfgcmd = " -config $conf "; } elsif ($cmd eq 'x509' && !$args->{noconfig}) { $conf = $self->{csp}->writeConfig($cmd,$args); $self->{csp}->die("Unable to write configuration file") unless -f $conf; $cfgcmd = " -extfile $conf -extensions extensions "; } $cmd = '' if $cmd eq 'dummy'; ${$self->{_in}} = "$cmd $cfgcmd $cmdline"; $self->warn("# openssl $cmd $cfgcmd $cmdline\n") if $ENV{CSPDEBUG}; $self->{_handle}->pump while length ${$self->{_in}}; $self->{_handle}->finish; my @out = split /\n/,${$self->{_out}}; my @err; my @nout; foreach $_ (@out) { chomp; s/\s*OpenSSL>\s*//og; next unless $_; if (/:error:/) { push (@err,$_); } else { push (@nout,$_); } } $self->{csp}->die(sprintf "OpenSSL Error\n%s",join("\n",@err)) if @err; join("\n",@nout)."\n"; } sub DESTROY { $_[0]->{_handle}->close(); finish $_[0]->{_handle}; } sub rws_open { my $self = shift; my $rw = shift; my $csp = shift; my $cmd = shift; my $cmdline = shift; my $args = shift; my ($lp,$rp); if ($rw eq 'r') { $lp = ''; $rp = '|'; } elsif ($rw eq 'w') { $lp = '|'; $rp = ''; } my $cfgcmd; if ( (grep $_ eq $cmd,qw(req ca)) && !$args->{noconfig}) { $self->{conf} = $self->{csp}->writeConfig($cmd,$args); $self->{csp}->die("Unable to write configuration file") unless -f $self->{conf}; $cfgcmd = " -config $self->{conf} "; } elsif ($cmd eq 'x509' && !$args->{noconfig}) { $self->{conf} = $self->{csp}->writeConfig($cmd,$args); $self->{csp}->die("Unable to write configuration file") unless -f $self->{conf}; $cfgcmd = " -extfile $self->{conf} -extensions extensions "; } $self->{csp} = $csp; $cmd = '' if $cmd eq 'dummy'; my $engine = "-engine opensc" if $ENV{CSP_OPENSC}; my $redirect = ($args->{verbose} == 0 && $rw ne 'r' ? ">/dev/null 2>&1" : ""); warn "${lp}$self->{openssl} $cmd $cfgcmd $cmdline ${redirect}${rp}" if $ENV{CSPDEBUG}; if ($rw eq 's') { $self->{rc} = system("$self->{openssl} $cmd $engine $cfgcmd $cmdline ${redirect}"); } else { open $self->{fh},"${lp}$self->{openssl} $cmd $engine $cfgcmd $cmdline ${redirect}${rp}" or $self->{csp}->die("Unable to execute: $!"); } $self; } sub close { my $self = shift; close $self->{fh} if defined $self->{fh}; unless ($ENV{CSPDEBUG}) { unlink $self->{conf} if $self->{conf}; } (defined $_[0]->{rc} ? $_[0]->{rc} : $?); } sub closedie { $_[0]->close() == 0 or $_[0]->{csp}->die("$!"); } sub closeok { $_[0]->close() == 0; } sub print { my $self = shift; print {$self->{fh}} @_; } package CSP; 1; __END__ # Below is the stub of documentation for your module. You better edit it! =head1 NAME CSP - A wrapper around OpenSSL for maintaining multiple Certificate Authorities. =head1 SYNOPSIS # csp help =head1 DESCRIPTION CSP is a perl module which uses openssl (openssl version 0.9.6 or later is required). Features include =over 4 =item o CSP leaves subject naming policy to the user. No checks are made on the attribute names in the subject. This is good if you don't like being asked a lot of questions when issuing certificates. =item o Configuration is reduced to specification of extensions. This is simplified using a simple CPP/MPP-type macro interpreter in CSP. =item o CSP is designed to easily handle multiple distinct Certificate Authorities. Hence the name which stands for Certificate Service Provider. =item o CSP can be used to produce a web site (certificate repository, CRLs etc etc) without the need for cgi-scripts. =item o CSP tries to be as PKIX-compliant as OpenSSL allows. =back =head1 CSP IN PRACTICE The typical application for CSP is a small CA (which may or may not be part of a larger pki) issuing mainly server and object signing certificates and only few if any user certificates. The distinction between user and other certificates may seem arbitrary but experience shows that managing a large set of user certificates typically requires a more sofisticated system for managing and tracking requests. When setting up CSP for production use the author strongly recommends using a non network connected host for the CA operations. This computer will not use much CPU or disk resources and any old PC with Linux or *BSD should work admirably. An old laptop might be a very good choice since it can be locked away when not in use. It might be a good idea to equip the computer with a cd writer or some other means for making backups of the certificate directory. Day to day operations include the following tasks. =over 4 =item 1 Issuing certificates based on pkcs10 or out-of-band (non pkcs10) requests. =item 2 Backing up the csp main directory (see below) to read-only medium. =item 3 Producing the public web site and exporting it (typically using floppy or zip-drive) to your web server. =back =head1 CONFIGURATION First set the environment variable OPENSSL to contain the absolute path of your OpenSSL binary. This is a requirement for everything that follows. Next create a directory where you will keep your CAs. This can be any directory anywhere in your file system owned by anyone. A sample directory "ca" in the distribution is included as a reference. A good way to get started is to copy this directory somewhere. Set $CSPHOME to point to this directory. The layout of this directory is as follows: . |-- csp Certificate Authorities directory `-- etc |-- aliases.txt Alternative names for DN attributes |-- extensions.conf Default certificate extensions file |-- crl_extensions.conf Default crl extensions |-- oids.conf Extra OIDs (eg attribute types). |-- public_html Default web site template files `-- types.txt List of certificate types to support Most of these files are defaults that are copied to each new CA when created. This means that if you need defaults used by all CAs you create you must make those edits first of all. The oids.conf and aliases.txt should be edited to include support for any extra attribute types your CA systems must support. The format of these files should be obvious. The oids.conf includes support for the DC attribute. Next edit extensions.conf. This file is a default file which is copied to each new CA. Before you create your first CA edit this file to reflect extensions and defaults for all CAs created in this directory. The format of this file is explained in the EXTENSIONS section. Also edit the crl_extensions.conf to include support for any crl extensions you might need. Remember that after you create a CA you must edit the local copy of these files to reflect the requirements of that particular CA. Finally edit the files in public_html. The structure of this directory is explained in more detail in the section WEBSITE below. You are now ready to create your first CA. If this is a self-signed CA creation is done in two steps: First create the directories and copy the configuration files. The first argument "PCA" is a name of the CA in our example. Creation of the CA "PCA" is done using the command "csp create". [leifj@njal CSP]$ ./csp PCA create Next initialize "PCA". The last argument is the distinguished name of the CA. CSP does not impose any limit on names which means that you can get away with any DN as long as your oids.conf and aliases.txt include aliases and oids for the attribute types. [leifj@njal CSP]$ ./csp PCA init --keysize=2048 \ --days=2190 'CN=CA,dc=example,dc=com' [CSP][PCA ] Generating CA key [CSP][PCA ] Private key password: <...> [CSP][PCA ] Re-enter Private key password: <...> [CSP][PCA ] Successfully created CA PCA List the contents of this certificate provider: [leifj@njal CSP]$ ./csp --list PCA Use openssl to dump the contents of the CA certificate: [leifj@njal CSP]$ csp PCA dump ... stuff deleted ... Now issue a new server certificate signed by PCA: [leifj@njal CSP]$ ./csp PCA issue \ --keysize=1024 \ --noconfirm \ 'CN=www.example.com,dc=example,dc=com' [CSP][PCA ] Generating new key [CSP][PCA ] Private key password: <...> [CSP][PCA ] Re-enter Private key password: <...> [CSP][PCA ] Create certificate request for CN=CA,dc=su,dc=se [CSP][PCA ] Signing request Check the contents of PCA database (the --xinfo arg gives you the sha1 and md5 fingerprints) [leifj@njal CSP]$ ./csp PCA list --xinfo Serial : 01 Status : Valid Subject : CN=www.example.com,dc=example,dc=com Expires : Wed Oct 10 10:28:32 2001 SHA1 : A3:53:D7:DF:AE:A2:44:0A:0F:B8:29:31:E7:96:16:62:1F:52:DF:62 MD5 : 04:2A:3A:1B:BB:B9:1E:08:4D:C3:14:CE:71:72:CC:86 Finally generate the public web site [leifj@njal CSP]$ mkdir /tmp/export [leifj@njal CSP]$ ./csp PCA genpublic --export=/tmp/export Now move /tmp/export to a removable medium and transport it to your web server. =head1 EXTENSIONS Configuration of extensions is done in the etc/extensions.conf. The format of this file is equivalent to the openssl extensions format. Read more about this in the openssl documentation. This file also supports a simple macro format similar to transarc mpp which in turn is essentially CPP with '%' as the magic character. The following macros are supported: %ifdef/%endif %if/%endif %include The predicate in the %if macro can be almost any perl expression where macro %-expansion replaces perl variable expansion. =head1 WEB SITE The directory public_html in each CA directory contains template files (MPP macro expansion is applied to these files) for a public web site. The included example has the following organization public_html |-- certs | |-- cert.html.mpp | |-- expired.html.mpp | |-- index.html.mpp | |-- revoked.html.mpp | |-- valid.html.mpp `-- index.html.mpp The csp command "csp genpublic" will produce a directory in the export directory (specified by --export) which looks like this: |-- ca.crt |-- certs | |-- 01.crt | |-- 01.html | |-- 01.pem | |-- expired.html | |-- index.html | |-- revoked.html | `-- valid.html |-- crl-v1.crl |-- crl-v2.crl `-- index.html The ca.crt, crl-v1.crl and crl-v2.crl are copies of the corresponding files from the master repository. Each certificate in the repository is stored in /certs as DER, PEM and as an HTML page derived from public_html/certs/cert.html.mpp by macro expansion. All other pages are simply produced by macro expansion of the corresponding .mpp-files and apart from certs/cert.html.mpp none of these files need exist or have the names they have in the example above. When performing macro expansion on public_html/certs/cert.html.mpp the following variables are available: DATE The date (using localtime(time)) of the export operation. SUBJECT_SERIAL The serial number of the certificate. SUBJECT_DN The distinguished name (DN) of the certificate. ISSUER_DN The distinguished name (DN) of the CA certificate. SUBJECT_SHA1 The SHA1-fingerprint of the certificate. SUBJECT_MD5 The MD5-fingerprint of the certificate. SUBJECT_NOTBEFORE The date when this certificate becomes valid. SUBJECT_NOTAFTER The date when this certificate expires. When all other files in the directories (public_html and public_html/certs) are run through the macro preprocessor to produce HTML files the following variables are available: DATE The date (using localtime(time)) of the export operation. VALID An HTML table of valid certificates. VALID_COUNT The number of valid certificates. REVOKED An HTML table of revoked certificates. REVOKED_COUNT The number of revoked certificates. EXPIRED An HTML table of expired certificates. EXPIRED_COUNT The number of expired certificates. SUBJECT_SERIAL The serial number of the CA certificate. SUBJECT_NOTAFTER The date when the CA certificate expires. SUBJECT_NOTBEFORE The date when the CA certificate became valid. SUBJECT_DN The distinguished name (DN) of the CA certificate. SUBJECT_MD5 The MD5-fingerprint of the CA certificate. SUBJECT_SHA1 The SHA1-fingerprint of the CA certificate. =head1 AUTHOR Leif Johansson Stockholm University =head1 ACKNOWLEDGEMENTS The web site generation was inspired by work by Heinar Hillbom UMDAC, Umeå Universitet =head1 SEE ALSO perl(1), openssl(1). =cut