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