1#!/usr/bin/perl -w 2 3use strict; 4use warnings; 5use English; 6use OpenXPKI::Debug; 7use OpenXPKI::Control; 8use Getopt::Long; 9use Pod::Usage; 10use Proc::SafeExec; 11use POSIX ":sys_wait_h"; 12use Errno; 13use File::Spec; 14use File::Basename; 15use YAML; 16use MIME::Base64; 17use Crypt::X509; 18use JSON; # early use of JSON avoids "Subroutine JSON::PP::Boolean::(++ redefined at /usr/share/perl/5.28/overload.pm line 48" 19 20# For the password hasher 21use IO::Prompt; 22use OpenXPKI::Password; 23 24 25# Config Builder 26use Storable qw(freeze); 27 28use OpenXPKI::FileUtils; 29use OpenXPKI::Server::Init; 30use OpenXPKI::i18n; 31use OpenXPKI::Server::Context qw( CTX ); 32use OpenXPKI::Config::Backend; 33use OpenXPKI::Client::Simple; 34use Log::Log4perl qw (:easy); 35 36use OpenXPKI::VERSION; 37 38use Data::Dumper; 39 40binmode(STDOUT, ":utf8"); 41 42my %params = ( module => [] ); 43my @options_spec = ('config=s','instance|i=s','verbose','debug|:s'); 44my $cmd = shift || 'help'; 45my $ret = 255; 46 47 48sub certificate_id { 49 50 my $format = $params{format} || 'openxpki'; 51 52 my $filename = $params{file}; 53 54 if ( !-r $filename ) { 55 print STDERR "ERROR: filename '$filename' is not readable\n"; 56 return 2; 57 } 58 59 if ($format eq 'openssl') { 60 my @exec = ('openssl','x509','-noout','-hash','-inform','PEM','-in', $filename); 61 my ($id, undef) = Proc::SafeExec::backtick(@exec); 62 chomp $id; 63 print "$id\n"; 64 return 0; 65 } elsif ($format ne 'openxpki') { 66 print STDERR "Invalid format - supported formats are openssl|openxpki\n"; 67 return 1; 68 } 69 70 my (undef, $cert_identifier) = __read_cert_from_file($filename); 71 72 print $cert_identifier; 73 print "\n"; 74 75 return 0; 76 77 } 78 79sub certificate_import { 80 81 print STDERR "Starting import\n"; 82 83 my $extracted_certdata = __read_cert_from_file($params{file}); 84 85 my $api_param = { data => $extracted_certdata }; 86 87 if ($params{issuer}) { 88 $api_param->{issuer} = $params{issuer}; 89 } 90 91 if ($params{realm}) { 92 $api_param->{pki_realm} = $params{realm}; 93 } 94 95 if ($params{"force-no-chain"}) { 96 $api_param->{force_nochain} = 1; 97 } 98 99 if ($params{"force-no-verify"}) { 100 $api_param->{force_noverify} = 1; 101 } 102 103 if ($params{"force-issuer"}) { 104 if (!$params{issuer}) { 105 die "You need to specify the issuer with --issuer when using --force-issuer\n" 106 } 107 $api_param->{force_issuer} = 1; 108 } 109 110 if ($params{"force-certificate-already-exists"}) { 111 $api_param->{update} = 1; 112 } elsif ($params{"force-certificate-ignore-existing"}) { 113 $api_param->{ignore_existing} = 1; 114 } 115 116 if ($params{revoked}) { 117 $api_param->{revoked} = 1; 118 } 119 120 if ($params{profile}) { 121 $api_param->{profile} = $params{profile}; 122 } 123 124 CTX('dbi')->start_txn(); 125 my $res = CTX('api2')->import_certificate( %$api_param ); 126 127 if (!$res) { 128 print "Certificate already in database - ignored\n"; 129 return 0; 130 } else { 131 print "Successfully imported certificate into database:\n"; 132 print " Subject: " . $res->{subject} . "\n"; 133 print " Issuer: " . $res->{issuer_dn} . "\n"; 134 print " Identifier: " . $res->{identifier} . "\n"; 135 print " Realm: " . ($res->{pki_realm} || 'none'). "\n"; 136 } 137 my $return = 0; 138 139 # directly register alias 140 if ($params{alias} || $params{gen} || $params{token} || $params{group}) { 141 print "Deprecated - please use openxpkiadm alias with --file option instead"; 142 if (!$params{realm}) { 143 # cert existed so no data in result -> lookup using get_cert 144 if (!$res) { 145 $res = CTX('api2')->get_cert( identifer => CTX('api2')->get_cert_identifier( cert => $extracted_certdata ) ); 146 } 147 if ($res->{pki_realm}) { 148 $params{realm} = $res->{pki_realm}; 149 # global realm only if no token group is used 150 } elsif (!$params{token}) { 151 $params{realm} = '_global'; 152 } else { 153 print "*Unable to register alias without realm!*\n"; 154 } 155 } 156 157 if ($res) { 158 $params{identifier} = $res->{identifer}; 159 } else { 160 $params{identifier} = CTX('api2')->get_cert_identifier( cert => $extracted_certdata ); 161 } 162 print "\n"; 163 $return = alias_add(); 164 } else { 165 # alias_add closes the txn already 166 CTX('dbi')->commit(); 167 } 168 169 return $return; 170} 171 172 173sub certificate_remove { 174 175 my $name = $params{name}; 176 my $realm = $params{realm}; 177 178 my $identifier = $name; 179 180 if ( $realm ) { 181 $identifier = __resolve_alias({ 182 NAME => $name, 183 REALM => $realm, 184 }); 185 } else { 186 $realm = '_any'; 187 } 188 189 # we dont need those checks if force-is-issuer is set 190 if (!defined $params{'force-is-issuer'}) { 191 192 # check if certificate is issuer of something 193 my $cnt = CTX('api2')->search_cert_count( 194 issuer_identifier => $identifier, 195 pki_realm => $realm, 196 ); 197 198 my $is_issuer = 0; 199 if ($cnt > 1) { 200 # this is definitly an active issuer certificate 201 $is_issuer = 1; 202 } elsif ($cnt == 1) { 203 # might be a self-signed certificate 204 my $cert = CTX('api2')->search_cert( 205 issuer_identifier => $identifier, 206 pki_realm => $realm 207 ); 208 $is_issuer = ( $cert->[0]->{'issuer_identifier'} ne $identifier ); 209 } 210 211 if ( $is_issuer ) { 212 print STDERR "ERROR: Certificate not deleted because it is referenced as the issuer of " 213 . $cnt . " certificate(s) in the database.\n"; 214 return 2; 215 } 216 } 217 218 my $dbi = CTX('dbi'); 219 my $certificate = $dbi->select_one( 220 from => 'certificate', 221 columns => ['identifier'], 222 where => { identifier => $identifier, }, 223 ); 224 if ( defined $certificate ) { 225 $dbi->delete( 226 from => 'certificate', 227 where => { identifier => $identifier, }, 228 ); 229 $dbi->commit(); 230 print "Successfully deleted certificate $name " 231 . "(identifier: $identifier) from database.\n"; 232 return 0; 233 } 234 else { 235 print STDERR "ERROR: Certificate $name " 236 . "(identifier: $identifier) not found in database.\n"; 237 return 2; 238 } 239} 240 241sub alias_add { 242 243 my %insert_hash = (); 244 my ($extracted_certdata, $extracted_keydata); 245 246 $insert_hash{pki_realm} = $params{realm}; 247 248 # use file to get identifier 249 if (!$params{identifier} && $params{file}) { 250 ($extracted_certdata, $params{identifier}) = __read_cert_from_file($params{file}); 251 } 252 253 my $dbi = CTX('dbi'); 254 $dbi->start_txn(); 255 256 # Import is possible with symbolic token or group name 257 if ( $params{token} || $params{group} ) { 258 259 my $group; 260 if ($params{group}) { 261 $group = $params{group}; 262 } else { 263 $group = ($params{token} eq "root") ? 'root' : 264 CTX('config')->get(['realm', $params{realm}, 'crypto','type', $params{token}]); 265 266 if (!$group) { 267 print STDERR "There is no token of type $params{token} defined\n"; 268 return 2; 269 } 270 } 271 272 # explicit generation 273 if ($params{gen} && $params{gen} =~ m{\A \d+ \z}x) { 274 275 # check for duplicate 276 my $check_duplicate = $dbi->select_one( 277 from => 'aliases', 278 columns => ['*'], 279 where => { 280 pki_realm => $params{realm}, 281 group_id => $group, 282 generation => $params{gen}, 283 # write to self is checked later with force flags 284 identifier => { "!=", $params{identifier} } 285 } 286 ); 287 if ($check_duplicate) { 288 print STDERR sprintf("A token with generation %01d for group %s already exists:\n", 289 $params{gen}, $params{token}); 290 print STDERR __alias_print($check_duplicate); 291 return 2; 292 } 293 294 $insert_hash{generation} = $params{gen}; 295 296 # no generation, autodetected 297 } else { 298 299 # query aliases to get next generation id 300 my $next_generation = $dbi->select_one( 301 from => 'aliases', 302 columns => ['*'], 303 where => { 304 pki_realm => $params{realm}, 305 group_id => $group, 306 }, 307 order_by => '-generation', 308 ); 309 $insert_hash{generation} = ($next_generation->{generation} || 0) + 1; 310 } 311 $insert_hash{group_id} = $group; 312 $insert_hash{alias} = sprintf "%s-%01d", $group, $insert_hash{generation}; 313 314 } elsif ( !exists( $params{alias} ) || $params{alias} eq '' ) { 315 print STDERR "Please specify an alias with --alias or use --token/--group\n"; 316 return 1; 317 318 } else { 319 320 $insert_hash{alias} = $params{alias}; 321 } 322 323 if ( !exists( $params{identifier} ) || $params{identifier} eq '' ) { 324 print STDERR "Please specify an identifier with --identifier\n"; 325 return 1; 326 } else { 327 $insert_hash{identifier} = $params{identifier}; 328 } 329 330 # Prevent duplicate entries (each identifier is allowed only once per group) 331 my $duplicate_alias = $dbi->select_one( 332 from => 'aliases', 333 columns => ['*'], 334 where => { 335 identifier => $insert_hash{identifier}, 336 pki_realm => $params{realm}, 337 group_id => $insert_hash{group_id} 338 }, 339 ); 340 341 if ($duplicate_alias) { 342 # if no explicit alias or generation was given 343 # we ignore this request as the alias is already there 344 if (!$params{gen} && !$params{alias}) { 345 print "Certificate already registered as alias:\n"; 346 print __alias_print($duplicate_alias); 347 $insert_hash{alias} = $duplicate_alias->{alias}; 348 return if ($params{"force-ignore-existing"}); 349 # if the given alias matches the existing one 350 } elsif ($insert_hash{alias} eq $duplicate_alias->{alias}) { 351 return if ($params{"force-ignore-existing"}); 352 } else { 353 print STDERR "ERROR: certificate exisits in group with a different alias\n"; 354 print STDERR __alias_print($duplicate_alias); 355 return 2; 356 } 357 358 if (!$params{"force-update-existing"}) { 359 print STDERR "ERROR: certificate already exisits in group\n"; 360 print STDERR "Alias: " . $duplicate_alias->{alias} . "\n"; 361 return 2; 362 } 363 364 } 365 366 # if a file is given we import it with the ignore flag 367 CTX('api2')->import_certificate( 368 pki_realm => $params{realm}, 369 data => $extracted_certdata, 370 ignore_existing => 1 371 ) if ($extracted_certdata); 372 373 my $alias = __alias_update(\%insert_hash); 374 return unless($alias); 375 376 print "Successfully wrote alias:\n"; 377 print __alias_print( $alias ); 378 379 # Check if the alias is for an issuing ca cert -> create root ca alias 380 my $cs_group = CTX('config')->get(['realm', $params{realm}, 'crypto', 'type', 'certsign']); 381 if ( $cs_group && ($insert_hash{alias} =~ /^$cs_group-(\d+)/ )) { 382 print "\nToken is certsign, looking for root...\n"; 383 my $gen = $1; 384 385 my $chain_ref = CTX('api2')->get_chain( 'start_with' => $insert_hash{identifier} ); 386 387 if ($chain_ref->{complete} != 1) { 388 print STDERR "ERROR: unable to find root certificate\n"; 389 return 2; 390 } 391 my $root_identifier = pop @{$chain_ref->{identifiers}}; 392 393 # check if this root is already defined 394 my $root_alias = $dbi->select_one( 395 from => 'aliases', 396 columns => ['*'], 397 where => { 398 identifier => $root_identifier, 399 pki_realm => $params{realm}, 400 group_id => 'root' 401 }, 402 ); 403 if ($root_alias) { 404 print "Root ca already in alias table:\n"; 405 } else { 406 print "Creating alias for root ca:\n"; 407 # Get the notebefore/notafter date 408 my $certificate = $dbi->select_one( 409 from => 'certificate', 410 columns => ['notbefore' , 'notafter' ], 411 where => { identifier => $root_identifier }, 412 ); 413 414 $root_alias = { 415 identifier => $root_identifier, 416 pki_realm => $params{realm}, 417 group_id => 'root', 418 generation => $gen, 419 alias => sprintf ('root-%01d', $gen), 420 notbefore => $certificate->{notbefore}, 421 notafter => $certificate->{notafter}, 422 }; 423 $dbi->insert( 424 into => 'aliases', 425 values => $root_alias, 426 ); 427 } 428 429 print __alias_print( $root_alias ); 430 431 } 432 433 $dbi->commit(); 434 435 return 0; 436 437} 438 439sub alias_update { 440 441 my %query_hash = ( 442 pki_realm => $params{realm} 443 ); 444 445 if ($params{alias}) { 446 $query_hash{alias} = $params{alias}; 447 } elsif ($params{identifier}) { 448 $query_hash{identifier} = $params{identifier}; 449 } elsif ($params{file}) { 450 (undef, $query_hash{identifier}) = __read_cert_from_file($params{file}); 451 } else { 452 print STDERR "You must specify either --alias, --identifier or --file\n"; 453 return 1; 454 } 455 456 my $dbi = CTX('dbi'); 457 458 my $alias = $dbi->select_one( 459 from => 'aliases', 460 columns => ['*'], 461 where => \%query_hash, 462 ); 463 464 if (!$alias) { 465 print STDERR "No alias entry found matching your request\n"; 466 return 2; 467 } 468 469 return unless(__alias_update($alias)); 470 471 $dbi->commit(); 472 473 $alias = $dbi->select_one( 474 from => 'aliases', 475 columns => ['*'], 476 where => \%query_hash, 477 ); 478 479 print "Successfully updated alias:\n"; 480 print __alias_print( $alias ); 481 482 483 return 0; 484 485} 486 487# expects the hash to merge into the db 488# adds notbefore, notafter, key from params if set 489sub __alias_update { 490 491 my $insert_hash = shift; 492 493 my $dbi = CTX('dbi'); 494 # query certificate table to check whether --identifer actually exists 495 # throws an exception if the cert is not found so no error handling here 496 my $certificate = CTX('api2')->get_cert( identifier => $insert_hash->{identifier}, format => "DBINFO" ); 497 498 if ($params{notbefore}) { 499 if ($params{notbefore} =~ /^\d+$/) { 500 $insert_hash->{notbefore} = $params{notbefore}; 501 } else { 502 my $dt; 503 eval { 504 $dt = OpenXPKI::DateTime::parse_date_utc( $params{notbefore} ); 505 }; 506 if ($EVAL_ERROR || !$dt) { 507 print STDERR "ERROR: Could not parse notbefore date\n"; 508 return; 509 } 510 511 $insert_hash->{notbefore} = $dt->epoch(); 512 513 if ($insert_hash->{notbefore} < $certificate->{notbefore}) { 514 print STDERR "ERROR: notbefore exceeds certificate validity\n"; 515 return; 516 } 517 } 518 } else { 519 $insert_hash->{notbefore} = $certificate->{notbefore}; 520 } 521 522 523 if ($params{notafter}) { 524 if ($params{notafter} =~ /^\d+$/) { 525 $insert_hash->{notafter} = $params{notafter}; 526 } else { 527 my $dt; 528 eval { 529 $dt = OpenXPKI::DateTime::parse_date_utc( $params{notafter} ); 530 }; 531 if ($EVAL_ERROR || !$dt) { 532 print STDERR "ERROR: Could not parse notafter date\n"; 533 return; 534 } 535 $insert_hash->{notafter} = $dt->epoch(); 536 537 if ($insert_hash->{notafter} > $certificate->{notafter}) { 538 print STDERR "ERROR: notafter exceeds certificate validity\n"; 539 return; 540 } 541 } 542 } else { 543 $insert_hash->{notafter} = $certificate->{notafter}; 544 } 545 546 #### insert_hash : Dumper($insert_hash) 547 $dbi->merge( 548 into => 'aliases', 549 set => $insert_hash, 550 where => { 551 alias => $insert_hash->{alias}, 552 pki_realm => $insert_hash->{pki_realm} 553 } 554 ); 555 556 # check if a key was given 557 if ($params{key}) { 558 my $filename = $params{key}; 559 if ( !-r $filename ) { 560 print STDERR "ERROR: filename '$filename' is not readable\n"; 561 return; 562 } 563 564 my $key = OpenXPKI::FileUtils->new()->read_file($filename) || ''; 565 my ($extracted_keydata) = $key =~ m{ \A (-----BEGIN\ ([\w\s]*)PRIVATE\ KEY----- .+? -----END\ \2PRIVATE\ KEY-----) \Z }xms; 566 if ( !$extracted_keydata ) { 567 print STDERR "ERROR: Could not parse private key data\n"; 568 return; 569 } 570 571 my $client = OpenXPKI::Client::Simple->new({ 572 config => { 573 realm => $insert_hash->{pki_realm}, 574 socket => CTX('config')->get(['system','server','socket_file']), 575 }, 576 auth => { stack => '_System' }, 577 }); 578 579 my $token = $client->run_command("get_token_info", { alias => $insert_hash->{alias} }); 580 if (!$token || !$token->{key_store}) { 581 print STDERR "ERROR: Unable to get token information!\n"; 582 return; 583 } elsif ($token->{key_store} eq "DATAPOOL") { 584 585 my $check_dv = $client->run_command("get_datavault_status", { check_online => 1 }); 586 if (!$check_dv) { 587 print STDERR "ERROR: You must setup a datavault token before you can import keys into the system!\n"; 588 return; 589 } 590 if (!$check_dv->{online}) { 591 print STDERR "ERROR: Your datavault token is not online, unable to import key!\n"; 592 return; 593 } 594 595 my $res; 596 eval{ 597 $res = $client->run_command("set_data_pool_entry", { 598 namespace => "sys.crypto.keys", 599 encrypt => 1, 600 force => $params{'force-update-key'}, 601 key => $token->{key_name}, 602 value => $extracted_keydata, 603 }); 604 }; 605 if ($res) { 606 printf "Successfully wrote key to datapool with key '%s'\n", $token->{key_name}; 607 } else { 608 print STDERR "ERROR: Problems writing key to datapool!\n"; 609 return; 610 } 611 612 } elsif ($token->{key_store} eq "OPENXPKI") { 613 614 # name of the keyfile 615 my $keyfile = $token->{key_name}; 616 if(-e $keyfile && !$params{'force-update-key'}) { 617 print STDERR "ERROR: key file '$keyfile' exists, won't override!\n"; 618 return; 619 } 620 if (!-d dirname($keyfile)) { 621 print STDERR "ERROR: directory for '$keyfile' does not exists, won't create it!\n"; 622 return; 623 } 624 open (my $fh, ">", $keyfile) || die "Unable to open $keyfile for writing\n"; 625 print $fh $extracted_keydata; 626 close $fh; 627 628 my $user = CTX('config')->get(['system','server','user']); 629 my $uid = getpwnam($user) or die "$user not known"; 630 631 my $group = CTX('config')->get(['system','server','user']); 632 my $gid = getgrnam($group) or die "$group not known"; 633 chown ($uid, $gid, $keyfile) || die "Unable to chown $keyfile to $uid/$gid"; 634 chmod oct("0400"), $keyfile || die "Unable to change mode on $keyfile"; 635 print "Successfully wrote key to $keyfile\n"; 636 } else { 637 printf STDERR "ERROR: Unsupported key storage method (%s)!\n", $token->{key_store}; 638 return; 639 } 640 } 641 642 return $insert_hash; 643} 644 645sub __read_cert_from_file { 646 647 my $filename = shift; 648 if ( !-r $filename ) { 649 print STDERR "ERROR: filename '$filename' is not readable\n"; 650 exit 2; 651 } 652 653 my $FileUtils = OpenXPKI::FileUtils->new(); 654 my $certdata = $FileUtils->read_file($filename) || ''; 655 656 my ($extracted_certdata) = $certdata =~ m{ \A .* (-----BEGIN\ CERTIFICATE----- .* -----END\ CERTIFICATE-----) .* \z}xms; 657 if ( !$extracted_certdata ) { 658 659 # DER encoded? 660 my $cert = Crypt::X509->new( cert => $certdata ); 661 if ($cert->error) { 662 print STDERR "ERROR: Could not parse certificate data\n"; 663 exit 2; 664 } 665 my $pem = encode_base64($certdata, ''); 666 chomp $pem; 667 $extracted_certdata = "-----BEGIN CERTIFICATE-----\n$pem\n-----END CERTIFICATE-----"; 668 } 669 670 if (wantarray) { 671 return ($extracted_certdata, CTX('api2')->get_cert_identifier( 'cert' => $extracted_certdata )); 672 } 673 return $extracted_certdata; 674 675} 676 677 678sub alias_list { 679 680 my $realm = $params{realm} || '_global'; 681 # get names of groups 682 my $groups = CTX('config')->get_hash(['realm',$realm,'crypto','type']); 683 684 my $dbi = CTX('dbi'); 685 my $alias; 686 my $db_alias; 687 my $cert; 688 my $filter = $params{filter} || 'current'; 689 my $show_subject = $params{subject} ? 1 :0; 690 691 # Prepare template for where part based on filter 692 my $where = { 'pki_realm' => { value => $realm } }; 693 my $limit = 999; 694 695 if ($filter ne 'all') { 696 # not all => active or current 697 my $now = time(); 698 $where->{'notbefore'} = { '<' => $now }; 699 $where->{'notafter'} = { '>' => $now }; 700 701 # Current is only the latest one 702 if ($filter ne 'active') { 703 $limit = 1; 704 } 705 } 706 707 # No group list 708 if ($params{nogroup}) { 709 $where->{group_id} = undef; 710 $db_alias = $dbi->select( 711 from => 'aliases', 712 columns => ['*'], 713 where => $where, 714 order_by => [ '-alias', '-notbefore' ], 715 ); 716 717 print "=== alias without group ===\n" if ($db_alias->rows); 718 while (my $alias = $db_alias->fetchrow_hashref) { 719 print __alias_print( $alias, $show_subject); 720 } 721 return 0; 722 } 723 724 if ($params{group}) { 725 $where->{group_id} = $params{group}; 726 727 } elsif ($params{token}) { 728 my $group = ($params{token} eq "root") ? 'root' : $groups->{$params{token}}; 729 if (!$group) { 730 print STDERR "There is no token of type $params{token} defined\n"; 731 return 2; 732 } 733 $where->{group_id} = $group; 734 } else { 735 $where->{group_id} = { '!=', '' }; 736 } 737 738 # Load the list of exisiting aliased groups as there can be custom tokens 739 # outside the main groups (e.g. alternative scep tokens) 740 my $db_results = $dbi->select( 741 from => 'aliases', 742 columns => ['*'], 743 where => $where, 744 order_by => '-notbefore', 745 ); 746 747 my %anon_groups; 748 while (my $entry = $db_results->fetchrow_hashref) { 749 $anon_groups{ $entry->{group_id} } = 1; 750 } 751 # remove root from the list 752 delete $anon_groups{'root'} unless($params{group} && $params{group} eq 'root'); 753 754 print "=== functional token ===\n" if (!$params{group} && $groups); 755 foreach my $type (keys %{$groups}) { 756 757 my $group = $groups->{$type}; 758 759 next if ($params{group} && $group ne $params{group}); 760 761 print "$group ($type):\n"; 762 763 $where->{'group_id'} = $group; 764 765 $db_alias = $dbi->select( 766 from => 'aliases', 767 columns => ['*'], 768 where => $where, 769 limit => $limit, 770 order_by => [ '-notbefore' ], 771 ); 772 773 while (my $alias = $db_alias->fetchrow_hashref) { 774 print __alias_print( $alias, $show_subject ); 775 } 776 777 # print empty message if none found and not in group mode 778 if (!$db_alias->rows) { 779 print __alias_print( undef ); 780 } 781 782 # unset in anon group list 783 delete $anon_groups{$group}; 784 785 } 786 787 print "=== anonymous groups ===\n" if (%anon_groups); 788 foreach my $group (keys %anon_groups) { 789 790 print "$group:\n"; 791 792 $where->{'group_id'} = $group; 793 794 $db_alias = $dbi->select( 795 from => 'aliases', 796 columns => ['*'], 797 where => $where, 798 limit => $limit, 799 order_by => [ '-notbefore' ], 800 ); 801 802 while (my $alias = $db_alias->fetchrow_hashref) { 803 print __alias_print( $alias, $show_subject ); 804 } 805 806 } 807 808 # do not proceed in group mode or in global realm 809 return 0 if ($params{group} || $realm eq '_global'); 810 811 # Check for root ca 812 $alias = $dbi->select_one( 813 from => 'aliases', 814 columns => ['*'], 815 where => { 816 'pki_realm' => $realm, 817 'group_id' => 'root', 818 'notbefore' => { '<' => time() }, 819 'notafter' => { '>' => time() }, 820 }, 821 order_by => '-notbefore' 822 ); 823 824 print "=== root ca ===\ncurrent root ca:\n"; 825 print __alias_print( $alias, $show_subject ); 826 827 # Check for root ca 828 $alias = $dbi->select_one( 829 from => 'aliases', 830 columns => ['*'], 831 where => { 832 'pki_realm' => $realm, 833 'group_id' => 'root', 834 'notbefore' => { '>', time() }, 835 }, 836 order_by => '-notbefore', 837 ); 838 839 print "upcoming root ca:\n"; 840 print __alias_print( $alias, $show_subject ); 841 842 return 0; 843 844 845} 846 847sub alias_show { 848 849 my $alias = CTX('dbi')->select_one( 850 from => 'aliases', 851 columns => ['*'], 852 where => { 853 alias => $params{alias}, 854 pki_realm => $params{realm} || '_global', 855 }, 856 limit => 1, 857 ); 858 859 __alias_print($alias, $params{subject}); 860 return 2 unless($alias); 861 862 my $file = $params{export} || ''; 863 if ($file eq "-") { 864 print CTX('api2')->get_cert( identifier => $alias->{identifier}, format => 'TXTPEM' ); 865 print "\n"; 866 } elsif ($file) { 867 868 my $pem = CTX('api2')->get_cert( identifier => $alias->{identifier}, format => 'PEM' ); 869 if (-d $file) { 870 $file .= sprintf('/%s.pem', $alias->{alias}); 871 } 872 if (-e $file) { 873 print "Export location $file exists - override (y/N)?"; 874 chomp(my $input = <STDIN>); 875 if ($input !~ /y/i) { 876 print "Aborted!\n"; 877 return; 878 } 879 } 880 open(my $fh, ">", $file) || die "Unable to write to $file"; 881 print $fh $pem; 882 close ($fh); 883 print "Certificate was written to $file\n"; 884 } 885 886 return 0; 887 888} 889 890sub __alias_print { 891 892 my $alias = shift; 893 my $subject = shift; 894 895 if (!$alias || !$alias->{alias}) { 896 return " not set\n\n"; 897 } 898 899 my $cert = CTX('api2')->get_cert( identifier => $alias->{identifier}, format => 'DBINFO' ); 900 901 my @out; 902 push @out, " Alias : $alias->{alias}\n"; 903 push @out, " Identifier: $alias->{identifier}\n"; 904 905 push @out, " Subject : $cert->{subject}\n" if ($subject); 906 907 push @out, " NotBefore : " . DateTime->from_epoch( epoch => $alias->{notbefore} )->strftime("%F %T"); 908 push @out, DateTime->from_epoch( epoch => $cert->{notbefore} )->strftime(" (%F %T)") 909 if ($cert && $cert->{notbefore} != $alias->{notbefore}); 910 911 push @out, "\n"; 912 push @out, " NotAfter : " . DateTime->from_epoch( epoch => $alias->{notafter} )->strftime("%F %T"); 913 914 push @out, DateTime->from_epoch( epoch => $cert->{notafter} )->strftime(" (%F %T)") 915 if ($cert && $cert->{notafter} != $alias->{notafter}); 916 917 push @out, "\n\n"; 918 return join "", @out; 919 920} 921 922 923sub certificate_chain { 924 925 my $cert_name; 926 my $issuer_name; 927 if ( !exists( $params{name} ) || $params{name} eq '' ) { 928 print STDERR "Please specify a certificate name with --name\n"; 929 return 1; 930 } 931 else { 932 $cert_name = $params{name}; 933 } 934 if ( !exists( $params{issuer} ) || $params{issuer} eq '' ) { 935 print STDERR "Please specify an issuer name with --issuer\n"; 936 return 1; 937 } 938 else { 939 $issuer_name = $params{issuer}; 940 } 941 942 # maybe the certificate name is an alias, try to resolve it 943 my $cert_identifier = __resolve_alias({ 944 NAME => $cert_name, 945 REALM => $params{realm}, 946 }); 947 948 # check whether the certificate is in the DB 949 my $certificate = CTX('dbi')->select_one( 950 columns => [ '*' ], 951 from => 'certificate', 952 where => { 953 'identifier' => $cert_identifier, 954 pki_realm => $params{realm} 955 } 956 ); 957 958 if ( !defined $certificate 959 && !defined $params{'force-certificate-not-found'} ) 960 { 961 print STDERR "ERROR: Certificate '$cert_name' not found in realm " 962 . "$params{realm}.\n"; 963 return 2; 964 } 965 966 my $issuer_identifier; 967 968 # maybe the issuer name is an alias, try resolve it 969 my $realm; 970 if ( defined $params{'issuer-realm'} ) { 971 $realm = $params{'issuer-realm'}; 972 } 973 else { 974 $realm = $params{realm}; 975 } 976 $issuer_identifier = __resolve_alias({ 977 NAME => $issuer_name, 978 REALM => $realm, 979 }); 980 981 my $dbi = CTX('dbi'); 982 # check whether the issuer is in the DB 983 my $issuer = $dbi->select_one( 984 columns => [ '*' ], 985 from => 'certificate', 986 where => { 987 'identifier' => $issuer_identifier}, 988 ); 989 if ( !defined $issuer 990 && !defined $params{'force-issuer-certificate-not-found'} ) 991 { 992 print STDERR "ERROR: Issuer certificate '$issuer_name' " 993 . "(identifier: $issuer_identifier) not found in database.\n"; 994 return 2; 995 } 996 997 # set the issuer_identifier for the given certificate 998 $dbi->update( 999 table => 'certificate', 1000 set => { issuer_identifier => $issuer_identifier }, 1001 where => { 1002 cert_key => $certificate->{cert_key}, 1003 identifier => $cert_identifier, 1004 pki_realm => $certificate->{pki_realm}, 1005 }, 1006 ); 1007 $dbi->commit(); 1008 print "Successfully set $issuer_name (identifier: $issuer_identifier) " 1009 . "as issuer of certificate $cert_name (identifier: " 1010 . "$cert_identifier).\n"; 1011 1012 # TODO: maybe don't warn only, but let the user use --force to 1013 # specify that he knows what he is doing ...? 1014 if ( $issuer->{subject_key_identifier} ne 1015 $certificate->{authority_key_identifier} ) 1016 { 1017 print STDERR "WARNING: The issuer's subject key identifier " 1018 . "extension ($issuer->{SUBJECT_KEY_IDENTIFIER}) does not " 1019 . "match the authority key identifier extension contained " 1020 . "in the certificate " 1021 . "($certificate->{authority_key_identifier}). Are you sure " 1022 . "your chain is correct?\n"; 1023 } 1024 if ( $issuer->{subject} ne $certificate->{issuer_dn} ) { 1025 print STDERR "WARNING: The issuer's subject ($issuer->{SUBJECT}) " 1026 . "does not match the issuer DN contained in the certificate " 1027 . "($certificate->{ISSUER_DN}). Are you sure your chain is " 1028 . "correct?\n"; 1029 } 1030 return 0; 1031 1032} 1033 1034sub certificate_list { 1035 1036 my @realms; 1037 if ( defined $params{realm} ) { 1038 push @realms, $params{realm}; 1039 } 1040 else { 1041 @realms = CTX('config')->get_keys(['system','realms']); 1042 push @realms, undef; # add the magic empty realm 1043 } 1044 1045 my $dbi = CTX('dbi'); 1046 foreach my $realm (@realms) { 1047 if ( defined $realm ) { 1048 print "\nCertificates in $realm:\n"; 1049 } 1050 else { 1051 print "\nCertificates in self-signed pseudo-realm:\n"; 1052 } 1053 my $certificates; 1054 if ( defined $params{all} ) { 1055 $certificates = $dbi->select( 1056 from => 'certificate', 1057 columns => ['*'], 1058 where => { pki_realm => $realm, }, 1059 ); 1060 } 1061 else { 1062 $certificates = $dbi->select( 1063 from_join => 'aliases aliases.identifier=certificate.identifier certificate', 1064 columns => [ 1065 'aliases.alias', 1066 'aliases.identifier', 1067 'certificate.subject', 1068 'certificate.issuer_dn', 1069 'certificate.cert_key', 1070 'certificate.issuer_identifier', 1071 'certificate.data', 1072 'certificate.status', 1073 'certificate.subject_key_identifier', 1074 'certificate.authority_key_identifier', 1075 'certificate.notafter', 1076 'certificate.notbefore', 1077 'certificate.req_key', 1078 ], 1079 where => { 'aliases.pki_realm' => $realm, }, 1080 ); 1081 } 1082 1083 while (my $cert = $certificates->fetchrow_hashref) { 1084 my $identifier; 1085 if ( defined $params{all} ) { # look up aliases 1086 $identifier = $cert->{identifier}; 1087 my $status = $cert->{status}; 1088 if ( defined $status && $status eq 'REVOKED' ) { 1089 print "\n Identifier: " 1090 . $cert->{identifier} 1091 . " (REVOKED)\n"; 1092 } 1093 else { 1094 print "\n Identifier: " . $cert->{identifier} . "\n"; 1095 } 1096 my $aliases = $dbi->select( 1097 from => 'aliases', 1098 columns => ['*'], 1099 where => { identifier => $cert->{identifier}, }, 1100 ); 1101 while (my $alias = $aliases->fetchrow_hashref) { 1102 print " Alias:\n " 1103 . $alias->{alias} . " (in realm: " . $alias->{pki_realm} . ")\n"; 1104 } 1105 } 1106 else { 1107 $identifier = $cert->{'identifier'}; 1108 my $status = $cert->{'status'}; 1109 if ( defined $status && $status eq 'REVOKED' ) { 1110 print "\n Identifier: " 1111 . $cert->{'identifier'} 1112 . " (REVOKED)\n"; 1113 } 1114 else { 1115 print "\n Identifier: " . $cert->{'identifier'} . "\n"; 1116 } 1117 print " Alias:\n " 1118 . $cert->{'alias'} . "\n"; 1119 } 1120 1121 my $prefix = ''; 1122 if ( defined $params{v} && $params{v} > 0 ) { 1123 1124 # show subject and issuer dn 1125 my $subject = $cert->{ 'subject' }; 1126 my $issuer_dn = $cert->{ 'issuer_dn' }; 1127 print " Subject:\n " . $subject . "\n"; 1128 print " Issuer DN:\n " . $issuer_dn . "\n"; 1129 } 1130 if ( defined $params{v} && $params{v} > 1 ) { 1131 1132 # show chain 1133 my $chain = CTX('api2')->get_chain( start_with => $identifier ); 1134 my $chain_str = join( ' -> ', @{ $chain->{identifiers} } ); 1135 1136 print " Chain:\n $chain_str"; 1137 if ( $chain->{complete} == 1 ) { 1138 print "(complete)\n"; 1139 } 1140 else { 1141 print "(INcomplete!)\n"; 1142 } 1143 } 1144 if ( defined $params{v} && $params{v} > 2 ) { 1145 1146 # show database entry 1147 my @fields = qw( 1148 subject_key_identifier 1149 authority_key_identifier 1150 cert_key 1151 issuer_identifier 1152 status 1153 notafter 1154 notbefore 1155 req_key 1156 ); 1157 1158 if ( $params{v} > 3 ) { 1159 push @fields, qw(data); 1160 } 1161 1162 foreach my $field (@fields) { 1163 my $value; 1164 if ( defined $cert->{ $field } ) { 1165 $value = $cert->{ $field }; 1166 } 1167 else { 1168 $value = 'NULL'; 1169 } 1170 $field =~ s/_/ /g; 1171 print " $field:\n " . $value . "\n"; 1172 } 1173 } 1174 } 1175 } 1176 exit 0; 1177} 1178 1179sub alias_del { 1180 1181 my %delete_hash = (); 1182 1183 $delete_hash{PKI_REALM} = $params{realm}; 1184 1185 if ($params{identifier}) { 1186 $delete_hash{identifier} = $params{identifier}; 1187 } elsif ($params{alias}) { 1188 $delete_hash{alias} = $params{alias}; 1189 } else { 1190 print STDERR "You must specify either --identifier or --alias\n"; 1191 return 1; 1192 } 1193 1194 my $dbi = CTX('dbi'); 1195 my $alias = $dbi->select_one( 1196 from => 'aliases', 1197 columns => ['*'], 1198 where => \%delete_hash, 1199 ); 1200 1201 if (!$alias) { 1202 print STDERR "No alias entry found matching your request\n"; 1203 return 2; 1204 } 1205 1206 $dbi->delete( 1207 from => 'aliases', 1208 where => { 1209 alias => $alias->{alias}, 1210 pki_realm => $alias->{pki_realm}, 1211 } 1212 ); 1213 $dbi->commit(); 1214 1215 print "Successfully removed the alias $alias->{alias}:\n"; 1216 print " Identifier: $alias->{identifier}\n"; 1217 print " Realm: $alias->{pki_realm}\n"; 1218 1219 return 0; 1220 1221} 1222 1223sub key_list { 1224 1225 my $dbi = CTX('dbi'); 1226 my $config = CTX('config'); 1227 my $realm = $params{realm}; 1228 1229 # TODO - Improve! 1230 # We use the alias table to find all keys in the realm 1231 # For the moment we assume the keys are defined explicit in the config 1232 # this will change in the future when we allow autodiscovery and default inheritance 1233 my $token_class = $config->get_hash(['realm',$realm,'crypto','type']); 1234 1235 foreach my $class (keys %{$token_class}) { 1236 my $db_alias = $dbi->select( 1237 from => 'aliases', 1238 columns => ['*'], 1239 where => { 1240 group_id => $token_class->{$class}, 1241 pki_realm => $realm, 1242 }, 1243 ); 1244 1245 print "Keys for token group $token_class->{$class}\n"; 1246 while (my $entry = $db_alias->fetchrow_hashref) { 1247 my $alias = $entry->{alias}; 1248 my $key = $config->get(['realm',$realm,'crypto','token',$alias,'key']); 1249 1250 my $status_flag = '?'; 1251 if (!$key) { 1252 $status_flag = 'c'; 1253 } elsif ( -e $key && ( !-s $key ) ) { 1254 $status_flag = '0'; # file exists but is of size zero 1255 } elsif ( -e $key ) { # file exists and is non-zero 1256 $status_flag = '+'; 1257 } else { # file does not exist (yet) 1258 $status_flag = '!'; 1259 } 1260 print ' ' . $status_flag . ' ' . $alias . "\n"; 1261 } 1262 } 1263 1264 return 0; 1265} 1266 1267sub hash_password { 1268 1269 print 'Please type your password, end with return: '; 1270 1271 my $passwd; 1272 if ($params{plain}) { 1273 $passwd = prompt; 1274 chomp $passwd; 1275 1276 } else { 1277 1278 $passwd = prompt -echo => '*'; 1279 1280 print "Please re-type your password: "; 1281 my $retype = prompt -echo => '*'; 1282 1283 chomp $passwd; 1284 chomp $retype; 1285 1286 if ($passwd ne $retype) { 1287 print "Sorry, the passwords do not match\n"; 1288 return 0; 1289 } 1290 } 1291 1292 if (!$passwd) { 1293 return 0; 1294 } 1295 1296 1297 my $hash = OpenXPKI::Password::hash($params{scheme},$passwd); 1298 1299 if (!$hash) { 1300 die "Unable to compute hash\n"; 1301 } 1302 1303 printf "Your hashed password is:\n%s\n", $hash; 1304 1305 return 0; 1306} 1307 1308sub __resolve_alias { 1309 1310 my $arg_ref = shift; 1311 my $alias = CTX('dbi')->select_one( 1312 from => 'aliases', 1313 columns => [ 'identifier' ], 1314 where => { 1315 'alias' => $arg_ref->{NAME}, 1316 'pki_realm' => $arg_ref->{REALM}, 1317 } 1318 ); 1319 1320 if (!$alias->{identifier}) { 1321 # likely it was an identifier already 1322 return $arg_ref->{NAME}; 1323 } 1324 return $alias->{identifier}; 1325 1326} 1327 1328sub __init { 1329 1330 GetOptions( \%params, @options_spec ) or pod2usage( -verbose => 0 ); 1331 1332 Log::Log4perl->easy_init( { 1333 level => defined $params{debug} ? $DEBUG : ( $params{verbose} ? $INFO : $WARN ), 1334 layout => '[%p] %m%n' 1335 }); 1336 1337 if ($params{config}) { 1338 # we set the ENV here to outrule an external ENV setting and to have 1339 # it ready for the reload action (needed to find the correct pidfile) 1340 $ENV{OPENXPKI_CONF_PATH} = $params{config}; 1341 } elsif ($params{instance}) { 1342 $ENV{OPENXPKI_CONF_PATH} = sprintf '/etc/openxpki/%s/config.d', $params{instance}; 1343 } 1344 1345} 1346 1347sub __check_realm { 1348 1349 my $realm = $params{realm}; 1350 1351 if (!$realm) { 1352 my @realms = CTX('config')->get_keys(['system','realms']); 1353 if (scalar @realms == 1) { 1354 $params{realm} = shift @realms; 1355 } else { 1356 die "You must specify a realm using --realm\n"; 1357 } 1358 } elsif (!CTX('config')->exists(['system','realms',$params{realm}])) { 1359 die "The realm $realm does not exist!\n"; 1360 } 1361 1362 return 1; 1363} 1364 1365 1366eval { 1367 1368if ($cmd eq 'initdb') { 1369 1370 push @options_spec, qw(dry-run force); 1371 die "initdb is no longer supported!\nPlease use the provided schema dumps to setup your database."; 1372 1373} elsif ($cmd eq 'certificate') { 1374 1375 my $subcmd = shift; 1376 1377 if ($subcmd eq 'id') { 1378 1379 push @options_spec, qw( 1380 file=s 1381 format=s 1382 ); 1383 __init(); 1384 1385 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1}); 1386 1387 $ret = certificate_id(); 1388 1389 } elsif ($subcmd eq 'import') { 1390 1391 push @options_spec, qw( 1392 realm=s 1393 file=s 1394 key=s 1395 issuer=s 1396 force-no-chain 1397 force-issuer 1398 force-certificate-already-exists 1399 force-certificate-ignore-existing 1400 force-no-verify 1401 revoked 1402 1403 alias=s 1404 gen|generation=s 1405 group=s 1406 token=s 1407 notbefore=s 1408 notafter=s 1409 1410 profile=s 1411 ); 1412 1413 # alias, gen, group, token are for alias shortcut 1414# force-really-self-signed 1415# force-issuer-not-found 1416# force-certificate-already-exists 1417 1418 1419 __init(); 1420 1421 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1}); 1422 1423 if (!$params{file}) { 1424 die "You need to specify the certificate to import with --file\n"; 1425 } 1426 1427 __check_realm() if ($params{realm} && $params{realm} ne '_global'); 1428 1429# if ((!$params{alias} && $params{group}) || ($params{alias} && !$params{group})) { 1430# die "You must always specify both --alias and --group \n"; 1431# } 1432 1433 $ret = certificate_import(); 1434 1435 } elsif ($subcmd eq 'remove') { 1436 1437 push @options_spec, qw( 1438 realm=s 1439 name=s 1440 force-is-issuer 1441 ); 1442 __init(); 1443 1444 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1}); 1445 1446 __check_realm() if ($params{realm}); 1447 1448 $ret = certificate_remove(); 1449 1450 } elsif ($subcmd eq 'chain') { 1451 1452 push @options_spec, qw( 1453 issuer=s 1454 issuer-realm=s 1455 realm=s 1456 name=s 1457 force-certificate-not-found 1458 force-issuer-certificate-not-found 1459 ); 1460 __init(); 1461 1462 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1}); 1463 1464 $ret = certificate_chain(); 1465 1466 } elsif ($subcmd eq 'list') { 1467 1468 push @options_spec, qw( 1469 realm=s 1470 all 1471 v+ 1472 ); 1473 __init(); 1474 1475 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1}); 1476 1477 __check_realm(); 1478 1479 $ret = certificate_list(); 1480 } 1481 1482 1483} elsif ($cmd eq 'alias') { 1484 1485 push @options_spec, qw( 1486 alias=s 1487 remove 1488 update 1489 notbefore=s 1490 notafter=s 1491 realm=s 1492 gen|generation=s 1493 group=s 1494 token=s 1495 filter=s 1496 identifier=s 1497 subject 1498 nogroup 1499 export|o=s 1500 file=s 1501 key=s 1502 force-update-existing 1503 force-ignore-existing 1504 force-update-key 1505 ); 1506 __init(); 1507 1508 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1}); 1509 1510 __check_realm() if (!$params{realm} || $params{realm} ne '_global'); 1511 1512 my $subcmd = shift || ''; 1513 1514 if ($params{remove} || $subcmd eq 'remove') { 1515 $ret = alias_del(); 1516 } elsif ($params{update} || $subcmd eq 'update') { 1517 $ret = alias_update(); 1518 } elsif ($params{alias}) { 1519 $ret = alias_show(); 1520 } elsif ($params{identifier} || $params{file}) { 1521 $ret = alias_add(); 1522 } else { 1523 $ret = alias_list(); 1524 } 1525} elsif ($cmd eq 'key') { 1526 1527 my $subcmd = shift || ''; 1528 1529 if ($subcmd eq 'list') { 1530 1531 push @options_spec, qw( 1532 realm=s 1533 ); 1534 __init(); 1535 1536 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','api2','dbi'], SILENT => 1, CLI => 1}); 1537 1538 __check_realm(); 1539 1540 $ret = key_list(); 1541 1542 } 1543} elsif ($cmd eq 'hashpwd') { 1544 1545 push @options_spec, qw( 1546 scheme|s=s 1547 plain 1548 ); 1549 __init(); 1550 OpenXPKI::Server::Init::init({TASKS => ['config_versioned','log','api2','dbi'], SILENT => 1, CLI => 1}); 1551 1552 if (!$params{scheme}) { 1553 $params{scheme} = 'ssha256'; 1554 } elsif (!OpenXPKI::Password::has_scheme($params{scheme})) { 1555 die "Unsupported scheme - see perldoc OpenXPKI::Password\n"; 1556 } 1557 1558 $ret = hash_password(); 1559 1560 1561} elsif ($cmd eq 'buildconfig') { 1562 1563 push @options_spec, qw( 1564 output=s 1565 key=s 1566 cert=s 1567 chain=s 1568 openssl=s 1569 force 1570 ); 1571 __init(); 1572 1573 $Storable::canonical = 1; 1574 1575 delete $ENV{OPENXPKI_CONF_PATH}; 1576 1577 if (!$params{config}) { 1578 die "You must provide the path to the source config using --config!"; 1579 } 1580 1581 -e $params{config} || die "Given config path does not exist!"; 1582 my $config = OpenXPKI::Config::Backend->new( LOCATION => $params{config} ); 1583 if (!$config->exists('system')) { 1584 die "Loaded config does not contain the system node!"; 1585 } 1586 1587 my $target = $params{output} || 'config.oxi'; 1588 1589 if (-e $target) { 1590 if (!$params{force}) { 1591 die "Target $target already exists, please remove first\n"; 1592 } 1593 unlink $target; 1594 } 1595 1596 my $raw = freeze($config->_config()); 1597 open FILE, ">", $target || die "Unable to open/write to $target"; 1598 # Signed config 1599 if ($params{key} && $params{cert}) { 1600 my $FileUtils = OpenXPKI::FileUtils->new; 1601 my $infile = $FileUtils->get_safe_tmpfile({ TMP => "/tmp"}); 1602 $FileUtils->write_file({ FILENAME => $infile, CONTENT => $raw, FORCE => 1 }); 1603 my $outfile = $FileUtils->get_safe_tmpfile({ TMP => "/tmp"}); 1604 1605 my @command = ( $params{openssl} || 'openssl', 1606 'smime', '-md', 'sha256', '-binary', '-sign', '-nodetach', 1607 '-outform', 'PEM', 1608 '-in', $infile, 1609 '-signer', $params{cert}, 1610 '-inkey', $params{key}, 1611 '-out', $outfile, 1612 ); 1613 1614 push @command, '-certfile', $params{chain} if ($params{chain}); 1615 1616 my $command = Proc::SafeExec->new({ 1617 exec => \@command, 1618 #stdin => 'new', 1619 #stdout => 'new', 1620 #stderr => 'new', 1621 }); 1622 $command->wait(); 1623 1624 if ($command->exit_status() != 0 || ! -e $outfile) { 1625 die "Signature creation failed (OpenSSL returned ".$command->exit_status().")"; 1626 } 1627 1628 my $pkcs7 = $FileUtils->read_file($outfile); 1629 # replace headers 1630 $pkcs7 =~ s{-----(BEGIN|END)[^-]+-----}{-----$1 OPENXPKI SIGNED CONFIG V1-----}g; 1631 print FILE $pkcs7; 1632 } else { 1633 print FILE "-----BEGIN OPENXPKI CONFIG V1 -----\n".encode_base64($raw)."-----END OPENXPKI CONFIG V1-----"; 1634 } 1635 close (FILE); 1636 1637 print "File written to $target\nConfig hash is " . Digest::SHA::sha256_hex($raw) . "\n"; 1638 1639 $ret = 0; 1640 1641} elsif ($cmd eq 'lintconfig') { 1642 1643 push @options_spec, qw( 1644 module|m=s 1645 ); 1646 __init(); 1647 1648 my $conf; 1649 if ($ENV{OPENXPKI_CONF_PATH}) { 1650 ## --config or --inst are rendered into ENV in _init 1651 $conf = $ENV{OPENXPKI_CONF_PATH}; 1652 delete $ENV{OPENXPKI_CONF_PATH}; 1653 } else { 1654 $conf = "/etc/openxpki/config.d/"; 1655 } 1656 1657 -e $conf || die "Given config path $conf does not exist!"; 1658 1659 my $c = OpenXPKI::Config::Backend->new( LOCATION => $conf ); 1660 print "Checking config at $conf\n"; 1661 if ($c->get_hash('system')) { 1662 printf "Config ok (%s)\n", $c->checksum(); 1663 $ret = 0; 1664 } else { 1665 # this means the yaml was ok but there is no system node 1666 if (defined $c) { 1667 print "Config was parsed but no system node was found\n"; 1668 } 1669 $ret = 2; 1670 } 1671 1672 if (defined $params{debug}) { 1673 my $hash = $c->get_hash(''); 1674 if ($params{debug}) { 1675 printf "system config at %s:\n", $params{debug}; 1676 my @path = split /\./, $params{debug}; 1677 foreach my $item (@path) { 1678 if (!defined $hash->{$item}) { 1679 print STDERR "No such component ($item)\n"; 1680 $hash = {}; 1681 last; 1682 } 1683 $hash = $hash->{$item}; 1684 } 1685 } 1686 print Dump $hash; 1687 } 1688 1689 foreach my $module (@{$params{module}}) { 1690 my $class = "OpenXPKI::Config::Lint::". ucfirst($module); 1691 eval "use $class;1"; 1692 if ($EVAL_ERROR) { 1693 print "Unable to load lint module $module\n"; 1694 print STDERR $EVAL_ERROR; 1695 } else { 1696 print "Lint $module: "; 1697 my $err = $class->new()->lint($c); 1698 if ($err) { 1699 print "\n$err\n"; 1700 $ret = 3 1701 } else { 1702 print "ok\n"; 1703 } 1704 } 1705 } 1706 1707} elsif ($cmd eq 'version') { 1708 print "Version (core): " . $OpenXPKI::VERSION::VERSION . "\n"; 1709 $ret = 0; 1710 1711} elsif ($cmd eq 'man') { 1712 pod2usage( -verbose => 1 ); 1713 $ret = 0; 1714} 1715 1716 1717}; 1718 1719if (ref $EVAL_ERROR eq "OpenXPKI::Exception") { 1720 print OpenXPKI::i18n::gettext($EVAL_ERROR->message()) ."\n"; 1721 map { print " $_: " . $EVAL_ERROR->params->{$_} ."\n"; } keys %{$EVAL_ERROR->params}; 1722 print "\n"; 1723 exit 1; 1724} elsif (my $eval_err = $EVAL_ERROR) { 1725 die $eval_err; 1726 print "\n"; 1727} elsif ($ret) { 1728 print "\n"; 1729 pod2usage( -verbose => 0 ) if ($ret == 1 || $ret == 255); 1730 exit $ret; 1731} 1732 1733exit 0; 1734 17351; 1736 1737 1738__END__ 1739 1740=head1 NAME 1741 1742openxpkiadm - tool for management operations of OpenXPKI instances 1743 1744=head1 USAGE 1745 1746openxpkiadm COMMAND [SUBCOMMAND] [OPTIONS] 1747 1748 Global options: 1749 --config DIR Location of the configuration repository 1750 optional, defaults to /etc/openxpki/config.d 1751 --instance|i NAME Shortcut to set the config path to 1752 /etc/openxpki/<instance>/config.d 1753 1754 Commands: 1755 help brief help message 1756 man full documentation 1757 version print program version and exit 1758 key Manage keys 1759 certificate Manage certificates 1760 hashpwd Create the (salted) hash / argon2 kcv for a password 1761 alias Manage the token alias table 1762 lintconfig Parse config and shows errors from Config::Merge 1763 buildconfig Create (signed) config blob from config tree 1764 1765=head1 ARGUMENTS 1766 1767Available commands: 1768 1769=head2 initdb 1770 1771Command was removed, use provided sql schema dumps to create database. 1772 1773=head2 key 1774 1775Key management for OpenXPKI Tokens (including issuing CAs and subsystems). 1776 1777Command options: 1778 1779 --realm PKI Realm to operate on 1780 1781=head3 key management subcommands 1782 1783=over 8 1784 1785=item B<list> 1786 1787Shows token key information for the specified realm, including 1788key algorithm, key length and secret splitting information. 1789TODO: Key info not implemented yet! 1790 1791Lists keys together with a status flag, which can be one of the 1792following: 1793 1794 c - token not defined in crypto.token 1795 + - key exists and file is non-empty 1796 0 - key exists but file is empty 1797 ! - key files does not exist (yet) 1798 1799 1800Example: 1801 1802 openxpkiadm key list --realm 'Root CA' 1803 1804=back 1805 1806=head2 certificate 1807 1808Starts a certificate management command and allows to list, install, 1809delete and connect certificates for the configured PKI Realms. 1810 1811 openxpkiadm certificate <subcommand> <options> 1812 1813=head3 certificate management subcommands 1814 1815=over 8 1816 1817=item B<list> 1818 1819Subcommand options (optional): 1820 1821 --realm PKI realm to operate on 1822 --all Show all certificates 1823 -v Show subject and issuer DN as well 1824 -v -v Show chain as well 1825 -v -v -v Show (nearly complete) database entry 1826 -v -v -v -v Show pubkey and certificate data, too 1827 1828Lists certificates present in the database for 1829the specified realm. If --all is not specified, only certificates 1830that have an alias defined for them are listed. --all lists all 1831certificates, regardless of whether they have an alias or not. 1832If --realm is left out, the certificates in all realms are listed 1833The number of -v's increases the verbosity (see above for what is 1834listed in which case). 1835 1836=item B<import> 1837 1838Subcommand options: 1839 1840Mandatory: 1841 --file the PEM file to import from 1842 1843Optional: 1844 --revoked import with status "revoked" 1845 --issuer the identifier of the issuer 1846 --realm PKI realm to import certificate to 1847 1848Force options (use only if you exactly now what you are doing!): 1849 --force-no-chain (only without issuer) 1850 Import even if the chain is incomplete, set NULL as issuer 1851 --force-issuer 1852 Force the issuer setting even if the chain validation fails 1853 --force-certificate-already-exists 1854 Force update for an existing certificate 1855 --force-certificate-ignore-existing 1856 Exit without error if the certificate exists 1857 --force-no-verify 1858 Build the chain but skip cryptographic verification 1859 1860Once again, only use these options if you actually have to (the occasions 1861where this happens should be really, really rare). Note that 1862force-no-chain might result in a wrong issuers assignment if key 1863identifiers or subjects are ambiguous. Consider using explicit issuer in 1864that cases if possible. 1865 1866Adds a certificate to the database. The issuer is usually auto-detected 1867and needs to be given only in rare cases. By default the certificates are 1868imported into the global realm, if you want to add them to a specific one, 1869you need to specify it. Note that a certificate always inherits the realm 1870of its issuer! 1871 1872The command outputs the subject's DN, issuer's DN and the imported realm 1873for you to verify that you imported the correct certificate as well as a 1874unique identifier which can be used to globally reference the certificate 1875(i.e. for configuration or as an issuer). If you don't want to remember 1876the identifier, look into openxpkiadm certificate alias to find out 1877how to create a symbolic name for an identifier. 1878 1879Examples: 1880 1881 openxpkiadm certificate import --file cacert.pem 1882 1883Import a certificate which issuer is not known in the "ServerCA" realm: 1884 1885 openxpkiadm certificate import --file cacert.pem \ 1886 --force-no-chain --realm ServerCA 1887 1888If alias, generation/group or token is given it is used to add an alias 1889after import - this option is deprecated and will be removed, use the 1890alias command with --file instead. 1891 1892=item B<remove> 1893 1894Subcommand options: 1895 1896Mandatory: 1897 --name The alias or identifier of the certificate 1898 1899Optional: 1900 --realm The PKI realm in which the alias is defined 1901 1902Force options (use only if you now what you are doing!): 1903 --force-is-issuer Delete certificate even though it is the 1904 issuer of another certificate in the database 1905 1906Removes a certificate from the database. 1907 1908Example: 1909 1910 openxpkiadm certificate remove --realm 'Root CA' \ 1911 --name 'Root CA 1' 1912 1913=item B<chain> 1914 1915Subcommand options: 1916 1917 Mandatory: 1918 --realm The PKI realm to operate in 1919 --name The alias or identifier of the child 1920 --issuer The alias or identifier of the parent 1921 1922Optional: 1923 --issuer-realm The realm in which the issuer alias 1924 is defined 1925 1926Force options (use only if you now what you are doing!): 1927 --force-certificate-not-found 1928 Ignore that the certificate of the child was not found 1929 in the DB 1930 --force-issuer-certificate-not-found 1931 Ignore that the certificate of the parent was not found 1932 in the DB 1933 1934Once again, only use these options if you actually have to (the 1935occasions where this happens should be really, really rare). 1936 1937Specifies subject/issuer relationship in order to set up certificate 1938chains. The certificates to be connected must already be present in 1939the database (see B<import>). As those connections are already set up 1940during --import, this command exists for changing the issuer if you 1941made an error. It also allows to specify an issuer that does not 1942agree with the information contained in the certificate (but outputs 1943a warning) 1944 1945 Example: 1946 1947openxpkiadm certificate chain --realm 'Root CA' \ 1948 --name 'Subordinate CA 1' --issuer 'root1' 1949 1950=back 1951 1952=head2 alias 1953 1954An alias is a symbolic name for a certificate in a specific realm. 1955OpenXPKI uses aliases to manage the crypto tokens for signer and 1956helper tokens. Several configs options and commands are able to 1957process aliases, too. 1958 1959The selection of functional tokens is done based on the notbefore/ 1960notafter date. To force certain behaviour (e.g time of a ca rollover), 1961you can force a custom notbefore/notafter date on the aliases. 1962 1963Common options: 1964 --realm PKI realm for the alias 1965 --identifier The identifier of the certificate 1966 --notbefore custom notbefore date to set 1967 --notafter custom notafter date to set 1968 accepted formats are epoch or yyyy-mm-dd hh:mm:ss 1969 a literal 0 restores the certificates validity. 1970 1971There are different ways to deal with aliases: 1972 1973=over 1974 1975=item B<list tokens> 1976 1977If you pass a realm but no identifier, you will receive the list of 1978active tokens for all token groups, the current root certificate 1979and, if set, the upcoming root certificate as used by scep I<GetNexCACert>. 1980 1981For items with custom notbefore/notafter settings, the certificate's 1982value is shown in brackets: 1983 1984 upcoming root ca: 1985 Alias : root-2 1986 Identifier: xGBSVo6N-9gpjB8UFll4TS-u-Eo 1987 NotBefore : 2014-01-01 00:00:00 (2013-06-17 13:54:34) 1988 NotAfter : 2016-12-31 23:59:59 (2020-06-17 13:54:34) 1989 1990To show the certificates subject besides the identifier, add --subject. 1991 1992To show a list of all or all active tokens, you can add the filter 1993parameter: 1994 1995 --filter all or --filter active 1996 1997You can also filter by a certain group name with --group <groupname>. 1998 1999Specify --nogroup to list tokens that do not belong to a group. 2000 2001=item B<show/export a single alias> 2002 2003Show the details of a single alias and export the linked certificate to 2004a file. 2005 2006 --alias The full alias (e.g. ca-signer-1) 2007 --subject Print the subject of the certificate 2008 --export Write PEM encoded certificate to <target>. 2009 If target is a directory the alias is used as filename. 2010 If target is "-" the certificate is printed to the terminal 2011 including its textual representation 2012 2013=item B<add functional token with automatic group discovery> 2014 2015Looks up the name of the associated group and finds the next generation 2016index by looking up the present aliases in the group. Recommended. 2017 2018 --token The name of the token type you want to add, 2019 e.g. certsign or datasafe. 2020 --file The filename of the PEM encoded certificate, can be given 2021 instead of --identifier. If the certificate does not exists 2022 in the database it is imported. 2023 2024Example: 2025 2026 openxpkiadm alias --realm server-realm \ 2027 --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \ 2028 --token certsign 2029 2030=item B<add functional token with manual group configuration> 2031 2032The alias is automatically set to <group>-<generation>, e.g. server-ca-1. 2033The generation identifier is increased by one from the latest one found 2034in the same group. 2035 2036 --group The name of the group (e.g. server-ca) 2037 2038Example: 2039 2040 openxpkiadm alias --realm server-realm \ 2041 --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \ 2042 --group server-ca 2043 2044=item B<explicit generation> 2045 2046If you need to force a certain generation identifier, you can skip the 2047autodetection and provide the wanted index: 2048 2049 --generation The numeric index to use for this alias 2050 2051This works with both methods above, token and group. 2052 2053Example: 2054 2055 openxpkiadm alias --realm server-realm \ 2056 --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \ 2057 --group server-ca --generation 42 2058 2059=item B<add non-functional alias> 2060 2061Adds the alias leaving group and generation empty. 2062 2063 --alias The symbolic name for the certificate 2064 2065Example: 2066 2067 openxpkiadm alias --realm server-realm \ 2068 --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \ 2069 --alias my-very-important-certificate 2070 2071=item B<alias key import> 2072 2073This applies as an extended functionality for all commands that create 2074an alias. 2075 2076 --key The filename of a PEM encoded private key. 2077 2078When provided, the system tries to copy the key data contained in the 2079given file to the location defined in the token configuration. The token 2080configuration is read from the OpenXPKI server process via the socket 2081using the System stack to authenticate. Therefore this requires that the 2082daemon is up and allows access to the I<get_token_info> call for the 2083default System user (this configuration is currently hardcoded and can 2084not be changed). 2085 2086For storage type "OPENXPKI" (file based) the key data is written into 2087the given key file, the file is set to 0400 permissions, owned by the 2088user the daemon runs with. The parent folder for the file must exist. 2089 2090For storage type "DATAPOOL" the key blob is loaded into the datapool, 2091encrypted with the current DataVault token. This therefore requires 2092that the DataVault token is already set up and available for encryption. 2093 2094=item B<force option> 2095 2096All alias create commands will fail if the given certificate already 2097exists in the given group. There are two options to ignore existing items. 2098 2099 --force-ignore-existing return and ignore extra settings 2100 --force-update-existing update validity and key for existing alias 2101 2102This will silently return if an alias for the given identifier exists in 2103the given group and no explicit generation was given. If an explicit name 2104or generation was given this only return if the existing alias matches 2105the given name. 2106 2107With --force-ignore-existing any given additional options will not be 2108processed. 2109 2110The command will fail if a key to import is specified but the target 2111already exists. To force overwrite of an existing key use 2112 2113 --force-update-key 2114 2115Please note that this will overwrite the existing file in place! 2116 2117=item B<update alias> 2118 2119Update notebefore/notafter date or set key of an existing alias. 2120 2121 --update Indicates that you want to update an existing entry 2122 --alias You can select the alias by name rather than passing 2123 the identifier. 2124 2125Example: 2126 2127 openxpkiadm alias --update --realm democa \ 2128 --alias ca-signer-1 2129 --notbefore "2014-01-01:00:00:00" 2130 2131This updates notbefore, notafter is not changed. 2132 2133Example: 2134 2135 openxpkiadm alias --update --realm democa \ 2136 --file ca-issuer-1.crt 2137 --key ca-issuer-1.pem 2138 2139Assign the key found in ca-issuer-1.pem to the alias matching the 2140certfificate ca-issuer-1.crt. The command will die if no alias is found 2141for the given certificate or if there is already a key found. In case 2142you want "create or update" use --force-update-existing instead --update. 2143 2144=item B<remove alias> 2145 2146Remove the entry from the alias table. 2147 2148 --remove Indicates that the alias should be removed. 2149 --alias You can select the alias by name rather than passing 2150 the identifier. 2151 2152Example: 2153 2154 openxpkiadm alias --remove --realm server-realm \ 2155 --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \ 2156 2157 openxpkiadm alias --remove --realm server-realm \ 2158 --alias server-ca-1 2159 2160=back 2161 2162=head2 hashpwd 2163 2164Create the hash of a given password to be used with the internal user database. 2165 2166Command options: 2167 2168 --scheme The hashing scheme to use, allowed values are 2169 sshaXXX|shaXXX|smd5|md5|crypt|argon2, default is ssha256 2170 see also OpenXPKI::Server::Authentication::Password 2171 2172 --plain do not hide the password on enter, no retype required 2173 should work with passwords piped to STDIN 2174 2175Prompts for the password and prints the hashed value including the used 2176scheme as defined in RFC2307. 2177 2178Also offers calculation of a token based on the Argon2 KDF. 2179 2180=head2 lintconfig 2181 2182Validate that the config tree is parseable, shows errors from underlying 2183Config::Merge such as YAML ident or quoting errors. 2184 2185Path to the config is read from environment I<OPENXPKI_CONF_PATH> or 2186I<--config> switch on commandline. If both are not set, the default 2187location I</etc/openxpki/config.d> is used. 2188 2189Command options: 2190 2191 -- module Specify extra modules to apply on the config object for 2192 additional checks. 2193 2194 --debug Dump the full config tree as YAML structure 2195 --debug path.to.node Dump the tree below this node as YAML structure 2196 2197=head2 buildconfig 2198 2199Serializes the config tree into a single transportable file that can be 2200signed. 2201 2202Command options: 2203 2204 --output Name of the output file, default is config.oxi 2205 --key Filename of the key to use for signing 2206 --cert Filename of the certificate (together with key) 2207 --chain Filename holding additonal certificates added as chain 2208 --force Force overwrite of existing file 2209 2210=head1 DESCRIPTION 2211 2212B<openxpkiadm> is the administrative frontend for controlling the OpenXPKI 2213installation. 2214 2215=over 8 2216 2217The openxpkiadm script returns a 0 exit value on success, and >0 if an 2218error occurs. 2219 2220=back 2221