#!/usr/local/bin/perl -w

use strict;
use warnings;
use English;
use OpenXPKI::Debug;
use OpenXPKI::Control;
use Getopt::Long;
use Pod::Usage;
use Proc::SafeExec;
use POSIX ":sys_wait_h";
use Errno;
use File::Spec;
use File::Basename;
use YAML;
use MIME::Base64;
use Crypt::X509;

# For the password hasher
use IO::Prompt;
use OpenXPKI::Password;


# Config Builder
use Storable qw(freeze);

use OpenXPKI::FileUtils;
use OpenXPKI::Server::Init;
use OpenXPKI::i18n;
use OpenXPKI::Server::Context qw( CTX );
use OpenXPKI::Config::Backend;
use OpenXPKI::Client::Simple;
use Log::Log4perl qw (:easy);

use OpenXPKI::VERSION;

use Data::Dumper;

binmode(STDOUT, ":utf8");

my %params = ( module => [] );
my @options_spec = ('config=s','instance|i=s','verbose','debug|:s');
my $cmd = shift || 'help';
my $ret = 255;


sub certificate_id {

    my $format = $params{format} || 'openxpki';

    my $filename = $params{file};

    if ( !-r $filename ) {
        print STDERR "ERROR: filename '$filename' is not readable\n";
        return 2;
    }

    if ($format eq 'openssl') {
        my @exec = ('openssl','x509','-noout','-hash','-inform','PEM','-in', $filename);
        my ($id, undef) = Proc::SafeExec::backtick(@exec);
        chomp $id;
        print "$id\n";
        return 0;
    } elsif ($format ne 'openxpki') {
        print STDERR "Invalid format - supported formats are openssl|openxpki\n";
        return 1;
    }

    my (undef, $cert_identifier)  = __read_cert_from_file($filename);

    print $cert_identifier;
    print "\n";

    return 0;

 }

sub certificate_import {

    print STDERR "Starting import\n";

    my $extracted_certdata = __read_cert_from_file($params{file});

    my $api_param = { data => $extracted_certdata };

    if ($params{issuer}) {
        $api_param->{issuer} = $params{issuer};
    }

    if ($params{realm}) {
        $api_param->{pki_realm} = $params{realm};
    }

    if ($params{"force-no-chain"}) {
        $api_param->{force_nochain} = 1;
    }

    if ($params{"force-no-verify"}) {
        $api_param->{force_noverify} = 1;
    }

    if ($params{"force-issuer"}) {
        if (!$params{issuer}) {
            die "You need to specify the issuer with --issuer when using --force-issuer\n"
        }
        $api_param->{force_issuer} = 1;
    }

    if ($params{"force-certificate-already-exists"}) {
        $api_param->{update} = 1;
    } elsif ($params{"force-certificate-ignore-existing"}) {
        $api_param->{ignore_existing} = 1;
    }

    if ($params{revoked}) {
        $api_param->{revoked} = 1;
    }

    if ($params{profile}) {
        $api_param->{profile} = $params{profile};
    }

    CTX('dbi')->start_txn();
    my $res = CTX('api2')->import_certificate( %$api_param );

    if (!$res) {
        print "Certificate already in database - ignored\n";
        return 0;
    } else {
        print "Successfully imported certificate into database:\n";
        print "  Subject:    " . $res->{subject} . "\n";
        print "  Issuer:     " . $res->{issuer_dn} . "\n";
        print "  Identifier: " . $res->{identifier} . "\n";
        print "  Realm:      " . ($res->{pki_realm} || 'none'). "\n";
    }
    my $return = 0;

    # directly register alias
    if ($params{alias} || $params{gen} || $params{token} || $params{group}) {
        print "Deprecated - please use openxpkiadm alias with --file option instead";
        if (!$params{realm}) {
            # cert existed so no data in result -> lookup using get_cert
            if (!$res) {
                $res = CTX('api2')->get_cert( identifer => CTX('api2')->get_cert_identifier( cert => $extracted_certdata ) );
            }
            if ($res->{pki_realm}) {
                $params{realm} = $res->{pki_realm};
            # global realm only if no token group is used
            } elsif (!$params{token}) {
                $params{realm} = '_global';
            } else {
                print "*Unable to register alias without realm!*\n";
            }
        }

        if ($res) {
            $params{identifier} = $res->{identifer};
        } else {
            $params{identifier} = CTX('api2')->get_cert_identifier( cert => $extracted_certdata );
        }
        print "\n";
        $return = alias_add();
    } else {
        # alias_add closes the txn already
        CTX('dbi')->commit();
    }

    return $return;
}


sub certificate_remove {

    my $name  = $params{name};
    my $realm = $params{realm};

    my $identifier = $name;

    if ( $realm ) {
        $identifier = __resolve_alias({
            NAME  => $name,
            REALM => $realm,
        });
    } else {
        $realm = '_any';
    }

    # we dont need those checks if force-is-issuer is set
    if (!defined $params{'force-is-issuer'}) {

        # check if certificate is issuer of something
        my $cnt = CTX('api2')->search_cert_count(
            issuer_identifier => $identifier,
            pki_realm => $realm,
        );

        my $is_issuer = 0;
        if ($cnt > 1) {
            # this is definitly an active issuer certificate
            $is_issuer = 1;
        } elsif ($cnt == 1) {
            # might be a self-signed certificate
            my $cert = CTX('api2')->search_cert(
                issuer_identifier => $identifier,
                pki_realm => $realm
            );
            $is_issuer = ( $cert->[0]->{'issuer_identifier'} ne $identifier );
        }

        if ( $is_issuer ) {
            print STDERR "ERROR: Certificate not deleted because it is referenced as the issuer of "
                . $cnt . " certificate(s) in the database.\n";
            return 2;
        }
    }

    my $dbi = CTX('dbi');
    my $certificate = $dbi->select_one(
        from   => 'certificate',
        columns => ['identifier'],
        where => { identifier => $identifier, },
    );
    if ( defined $certificate ) {
        $dbi->delete(
            from => 'certificate',
            where  => { identifier => $identifier, },
        );
        $dbi->commit();
        print "Successfully deleted certificate $name "
          . "(identifier: $identifier) from database.\n";
        return 0;
    }
    else {
        print STDERR "ERROR: Certificate $name "
          . "(identifier: $identifier) not found in database.\n";
        return 2;
    }
}

sub alias_add {

    my %insert_hash = ();
    my ($extracted_certdata, $extracted_keydata);

    $insert_hash{pki_realm} = $params{realm};

    # use file to get identifier
    if (!$params{identifier} && $params{file}) {
        ($extracted_certdata, $params{identifier}) = __read_cert_from_file($params{file});
    }

    my $dbi = CTX('dbi');
    $dbi->start_txn();

    # Import is possible with symbolic token or group name
    if ( $params{token} || $params{group} ) {

        my $group;
        if ($params{group}) {
            $group = $params{group};
        } else {
            $group = ($params{token} eq "root") ? 'root' :
                CTX('config')->get(['realm', $params{realm}, 'crypto','type', $params{token}]);

            if (!$group) {
                print STDERR "There is no token of type $params{token} defined\n";
                return 2;
            }
        }

        # explicit generation
        if ($params{gen} && $params{gen} =~ m{\A \d+ \z}x) {

            # check for duplicate
            my $check_duplicate = $dbi->select_one(
                from   => 'aliases',
                columns => ['*'],
                where => {
                    pki_realm => $params{realm},
                    group_id => $group,
                    generation => $params{gen},
                    # write to self is checked later with force flags
                    identifier => { "!=", $params{identifier} }
                }
            );
            if ($check_duplicate) {
                print STDERR sprintf("A token with generation %01d for group %s already exists:\n",
                    $params{gen}, $params{token});
                print STDERR __alias_print($check_duplicate);
                return 2;
            }

           $insert_hash{generation} = $params{gen};

        # no generation, autodetected
        } else {

            # query aliases to get next generation id
            my $next_generation = $dbi->select_one(
                from   => 'aliases',
                columns => ['*'],
                where => {
                    pki_realm => $params{realm},
                    group_id => $group,
                },
                order_by => '-generation',
            );
            $insert_hash{generation} = ($next_generation->{generation} || 0) + 1;
        }
        $insert_hash{group_id} = $group;
        $insert_hash{alias} = sprintf "%s-%01d", $group, $insert_hash{generation};

    } elsif ( !exists( $params{alias} ) || $params{alias} eq '' ) {
        print STDERR "Please specify an alias with --alias or use --token/--group\n";
        return 1;

    } else {

        $insert_hash{alias} = $params{alias};
    }

    if ( !exists( $params{identifier} ) || $params{identifier} eq '' ) {
        print STDERR "Please specify an identifier with --identifier\n";
        return 1;
    } else {
        $insert_hash{identifier} = $params{identifier};
    }

    # Prevent duplicate entries (each identifier is allowed only once per group)
    my $duplicate_alias = $dbi->select_one(
        from   => 'aliases',
        columns => ['*'],
        where => {
            identifier => $insert_hash{identifier},
            pki_realm => $params{realm},
            group_id => $insert_hash{group_id}
        },
    );

    if ($duplicate_alias) {
        # if no explicit alias or generation was given
        # we ignore this request as the alias is already there
        if (!$params{gen} && !$params{alias}) {
            print "Certificate already registered as alias:\n";
            print __alias_print($duplicate_alias);
            $insert_hash{alias} = $duplicate_alias->{alias};
            return if ($params{"force-ignore-existing"});
        # if the given alias matches the existing one
        } elsif ($insert_hash{alias} eq $duplicate_alias->{alias}) {
            return if ($params{"force-ignore-existing"});
        } else {
            print STDERR "ERROR: certificate exisits in group with a different alias\n";
            print STDERR __alias_print($duplicate_alias);
            return 2;
        }

        if (!$params{"force-update-existing"}) {
            print STDERR "ERROR: certificate already exisits in group\n";
            print STDERR "Alias: " . $duplicate_alias->{alias} . "\n";
            return 2;
        }

    }

    # if a file is given we import it with the ignore flag
    CTX('api2')->import_certificate(
        pki_realm => $params{realm},
        data => $extracted_certdata,
        ignore_existing => 1
    ) if ($extracted_certdata);

    my $alias = __alias_update(\%insert_hash);
    return unless($alias);

    print "Successfully wrote alias:\n";
    print __alias_print( $alias );

    # Check if the alias is for an issuing ca cert -> create root ca alias
    my $cs_group = CTX('config')->get(['realm', $params{realm}, 'crypto', 'type', 'certsign']);
    if ( $cs_group && ($insert_hash{alias} =~ /^$cs_group-(\d+)/ )) {
        print "\nToken is certsign, looking for root...\n";
        my $gen = $1;

        my $chain_ref = CTX('api2')->get_chain( 'start_with' => $insert_hash{identifier} );

        if ($chain_ref->{complete} != 1) {
            print STDERR "ERROR: unable to find root certificate\n";
            return 2;
        }
        my $root_identifier = pop @{$chain_ref->{identifiers}};

        # check if this root is already defined
        my $root_alias = $dbi->select_one(
            from   => 'aliases',
            columns => ['*'],
            where => {
                identifier => $root_identifier,
                pki_realm => $params{realm},
                group_id => 'root'
            },
        );
        if ($root_alias) {
            print "Root ca already in alias table:\n";
        } else {
            print "Creating alias for root ca:\n";
            # Get the notebefore/notafter date
            my $certificate = $dbi->select_one(
                from   => 'certificate',
                columns => ['notbefore' , 'notafter' ],
                where => { identifier => $root_identifier },
            );

            $root_alias = {
                identifier => $root_identifier,
                pki_realm => $params{realm},
                group_id => 'root',
                generation => $gen,
                alias => sprintf ('root-%01d', $gen),
                notbefore => $certificate->{notbefore},
                notafter => $certificate->{notafter},
            };
            $dbi->insert(
                into => 'aliases',
                values  => $root_alias,
            );
        }

        print __alias_print( $root_alias );

    }

    $dbi->commit();

    return 0;

}

sub alias_update {

    my %query_hash = (
        pki_realm =>  $params{realm}
    );

    if ($params{alias}) {
        $query_hash{alias} = $params{alias};
    } elsif ($params{identifier}) {
        $query_hash{identifier} = $params{identifier};
    } elsif ($params{file}) {
        (undef, $query_hash{identifier}) = __read_cert_from_file($params{file});
    } else {
        print STDERR "You must specify either --alias, --identifier or --file\n";
        return 1;
    }

    my $dbi = CTX('dbi');

    my $alias = $dbi->select_one(
        from   => 'aliases',
        columns => ['*'],
        where => \%query_hash,
    );

    if (!$alias) {
        print STDERR "No alias entry found matching your request\n";
        return 2;
    }

    return unless(__alias_update($alias));

    $dbi->commit();

    $alias = $dbi->select_one(
        from   => 'aliases',
        columns => ['*'],
        where => \%query_hash,
    );

    print "Successfully updated alias:\n";
    print __alias_print( $alias );


    return 0;

}

# expects the hash to merge into the db
# adds notbefore, notafter, key from params if set
sub __alias_update {

    my $insert_hash = shift;

    my $dbi = CTX('dbi');
    # query certificate table to check whether --identifer actually exists
    # throws an exception if the cert is not found so no error handling here
    my $certificate = CTX('api2')->get_cert( identifier => $insert_hash->{identifier}, format => "DBINFO" );

    if ($params{notbefore}) {
        if ($params{notbefore} =~ /^\d+$/) {
            $insert_hash->{notbefore} = $params{notbefore};
        } else {
            my $dt;
            eval {
                $dt = OpenXPKI::DateTime::parse_date_utc( $params{notbefore} );
            };
            if ($EVAL_ERROR || !$dt) {
                print STDERR "ERROR: Could not parse notbefore date\n";
                return;
            }

            $insert_hash->{notbefore} = $dt->epoch();

            if ($insert_hash->{notbefore} < $certificate->{notbefore}) {
                print STDERR "ERROR: notbefore exceeds certificate validity\n";
                return;
            }
        }
    } else {
        $insert_hash->{notbefore} = $certificate->{notbefore};
    }


    if ($params{notafter}) {
        if ($params{notafter} =~ /^\d+$/) {
            $insert_hash->{notafter} = $params{notafter};
        } else {
            my $dt;
            eval {
                $dt = OpenXPKI::DateTime::parse_date_utc( $params{notafter} );
            };
            if ($EVAL_ERROR || !$dt) {
                print STDERR "ERROR: Could not parse notafter date\n";
                return;
            }
            $insert_hash->{notafter} = $dt->epoch();

            if ($insert_hash->{notafter} > $certificate->{notafter}) {
                print STDERR "ERROR: notafter exceeds certificate validity\n";
                return;
            }
        }
    } else {
        $insert_hash->{notafter} = $certificate->{notafter};
    }

    #### insert_hash : Dumper($insert_hash)
    $dbi->merge(
        into => 'aliases',
        set  => $insert_hash,
        where => {
            alias =>  $insert_hash->{alias},
            pki_realm => $insert_hash->{pki_realm}
        }
    );

    # check if a key was given
    if ($params{key}) {
        my $filename = $params{key};
        if ( !-r $filename ) {
            print STDERR "ERROR: filename '$filename' is not readable\n";
            return;
        }

        my $key = OpenXPKI::FileUtils->new()->read_file($filename) || '';
        my ($extracted_keydata) = $key =~ m{ \A (-----BEGIN\ ([\w\s]*)PRIVATE\ KEY----- .+? -----END\ \2PRIVATE\ KEY-----) \Z }xms;
        if ( !$extracted_keydata ) {
            print STDERR "ERROR: Could not parse private key data\n";
            return;
        }

        my $client = OpenXPKI::Client::Simple->new({
            config => {
                realm => $insert_hash->{pki_realm},
                socket => CTX('config')->get(['system','server','socket_file']),
            },
            auth => { stack => '_System' },
        });

        my $token = $client->run_command("get_token_info", { alias => $insert_hash->{alias} });
        if (!$token || !$token->{key_store}) {
            print STDERR "ERROR: Unable to get token information!\n";
            return;
        } elsif ($token->{key_store} eq "DATAPOOL") {

            my $check_dv = $client->run_command("get_datavault_status", { check_online => 1 });
            if (!$check_dv) {
                print STDERR "ERROR: You must setup a datavault token before you can import keys into the system!\n";
                return;
            }
            if (!$check_dv->{online}) {
                print STDERR "ERROR: Your datavault token is not online, unable to import key!\n";
                return;
            }

            my $res;
            eval{
                $res = $client->run_command("set_data_pool_entry", {
                    namespace => "sys.crypto.keys",
                    encrypt => 1,
                    force => $params{'force-update-key'},
                    key => $token->{key_name},
                    value => $extracted_keydata,
                });
            };
            if ($res) {
                printf "Successfully wrote key to datapool with key '%s'\n", $token->{key_name};
            } else {
                print STDERR "ERROR: Problems writing key to datapool!\n";
                return;
            }

        } elsif ($token->{key_store} eq "OPENXPKI") {

            # name of the keyfile
            my $keyfile = $token->{key_name};
            if(-e $keyfile && !$params{'force-update-key'}) {
                print STDERR "ERROR: key file '$keyfile' exists, won't override!\n";
                return;
            }
            if (!-d dirname($keyfile)) {
                print STDERR "ERROR: directory for '$keyfile' does not exists, won't create it!\n";
                return;
            }
            open (my $fh, ">", $keyfile) || die "Unable to open $keyfile for writing\n";
            print $fh $extracted_keydata;
            close $fh;

            my $user = CTX('config')->get(['system','server','user']);
            my $uid = getpwnam($user) or die "$user not known";

            my $group = CTX('config')->get(['system','server','user']);
            my $gid = getgrnam($group) or die "$group not known";
            chown ($uid, $gid, $keyfile) || die "Unable to chown $keyfile to $uid/$gid";
            chmod oct("0400"), $keyfile || die "Unable to change mode on $keyfile";
            print "Successfully wrote key to $keyfile\n";
        } else {
            printf STDERR "ERROR: Unsupported key storage method (%s)!\n", $token->{key_store};
            return;
        }
    }

    return $insert_hash;
}

sub __read_cert_from_file {

    my $filename = shift;
    if ( !-r $filename ) {
        print STDERR "ERROR: filename '$filename' is not readable\n";
        exit 2;
    }

    my $FileUtils = OpenXPKI::FileUtils->new();
    my $certdata  = $FileUtils->read_file($filename) || '';

    my ($extracted_certdata) = $certdata =~ m{ \A .* (-----BEGIN\ CERTIFICATE----- .* -----END\ CERTIFICATE-----) .* \z}xms;
    if ( !$extracted_certdata ) {

        # DER encoded?
        my $cert = Crypt::X509->new( cert => $certdata );
        if ($cert->error) {
            print STDERR "ERROR: Could not parse certificate data\n";
            exit 2;
        }
        my $pem = encode_base64($certdata, '');
        chomp $pem;
        $extracted_certdata = "-----BEGIN CERTIFICATE-----\n$pem\n-----END CERTIFICATE-----";
    }

    if (wantarray) {
        return ($extracted_certdata, CTX('api2')->get_cert_identifier( 'cert' => $extracted_certdata ));
    }
    return $extracted_certdata;

}


sub alias_list {

    my $realm = $params{realm} || '_global';
    # get names of groups
    my $groups = CTX('config')->get_hash(['realm',$realm,'crypto','type']);

    my $dbi = CTX('dbi');
    my $alias;
    my $db_alias;
    my $cert;
    my $filter = $params{filter} || 'current';
    my $show_subject = $params{subject} ? 1 :0;

    # Prepare template for where part based on filter
    my $where = { 'pki_realm' => { value => $realm } };
    my $limit = 999;

    if ($filter ne 'all') {
        # not all => active or current
        my $now = time();
        $where->{'notbefore'} = { '<' => $now };
        $where->{'notafter'} = { '>' => $now };

        # Current is only the latest one
        if ($filter ne 'active') {
            $limit = 1;
        }
    }

    # No group list
    if ($params{nogroup}) {
        $where->{group_id} = undef;
        $db_alias = $dbi->select(
            from   => 'aliases',
            columns => ['*'],
            where =>  $where,
            order_by => [ '-alias', '-notbefore' ],
        );

        print "=== alias without group ===\n" if ($db_alias->rows);
        while (my $alias = $db_alias->fetchrow_hashref) {
            print __alias_print( $alias, $show_subject);
        }
        return 0;
    }

    if ($params{group}) {
        $where->{group_id} = $params{group};

    } elsif ($params{token}) {
        my $group = ($params{token} eq "root") ? 'root' : $groups->{$params{token}};
        if (!$group) {
            print STDERR "There is no token of type $params{token} defined\n";
            return 2;
        }
        $where->{group_id} = $group;
    } else {
        $where->{group_id} = { '!=', '' };
    }

    # Load the list of exisiting aliased groups as there can be custom tokens
    # outside the main groups (e.g. alternative scep tokens)
    my $db_results = $dbi->select(
        from  => 'aliases',
        columns => ['*'],
        where => $where,
        order_by => '-notbefore',
    );

    my %anon_groups;
    while (my $entry = $db_results->fetchrow_hashref) {
        $anon_groups{ $entry->{group_id} } = 1;
    }
    # remove root from the list
    delete $anon_groups{'root'} unless($params{group} && $params{group} eq 'root');

    print "=== functional token ===\n" if (!$params{group} && $groups);
    foreach my $type (keys %{$groups}) {

        my $group = $groups->{$type};

        next if ($params{group} && $group ne $params{group});

        print "$group ($type):\n";

        $where->{'group_id'} = $group;

        $db_alias = $dbi->select(
            from   => 'aliases',
            columns => ['*'],
            where =>  $where,
            limit => $limit,
            order_by => [ '-notbefore' ],
        );

        while (my $alias = $db_alias->fetchrow_hashref) {
            print __alias_print( $alias, $show_subject );
        }

        # print empty message if none found and not in group mode
        if (!$db_alias->rows) {
            print __alias_print( undef );
        }

        # unset in anon group list
        delete $anon_groups{$group};

    }

    print "=== anonymous groups ===\n" if (%anon_groups);
    foreach my $group (keys %anon_groups) {

        print "$group:\n";

        $where->{'group_id'} = $group;

        $db_alias = $dbi->select(
            from   => 'aliases',
            columns => ['*'],
            where =>  $where,
            limit => $limit,
            order_by => [ '-notbefore' ],
        );

        while (my $alias = $db_alias->fetchrow_hashref) {
            print __alias_print( $alias, $show_subject );
        }

    }

    # do not proceed in group mode or in global realm
    return 0 if ($params{group} || $realm eq '_global');

    # Check for root ca
    $alias = $dbi->select_one(
        from    => 'aliases',
        columns => ['*'],
        where => {
            'pki_realm' => $realm,
            'group_id' => 'root',
            'notbefore' => { '<' => time() },
            'notafter' => { '>' => time() },
        },
        order_by => '-notbefore'
    );

    print "=== root ca ===\ncurrent root ca:\n";
    print __alias_print( $alias, $show_subject );

    # Check for root ca
    $alias = $dbi->select_one(
        from   => 'aliases',
        columns => ['*'],
        where => {
            'pki_realm' => $realm,
            'group_id' => 'root',
            'notbefore' => { '>', time() },
        },
        order_by => '-notbefore',
    );

    print "upcoming root ca:\n";
    print __alias_print( $alias, $show_subject );

    return 0;


}

sub alias_show {

    my $alias = CTX('dbi')->select_one(
        from   => 'aliases',
        columns => ['*'],
        where => {
            alias =>  $params{alias},
            pki_realm => $params{realm} || '_global',
        },
        limit => 1,
    );

    __alias_print($alias, $params{subject});
    return 2 unless($alias);

    my $file = $params{export} || '';
    if ($file eq "-") {
        print CTX('api2')->get_cert( identifier => $alias->{identifier}, format => 'TXTPEM' );
        print "\n";
    } elsif ($file) {

        my $pem = CTX('api2')->get_cert( identifier => $alias->{identifier}, format => 'PEM' );
        if (-d $file) {
            $file .= sprintf('/%s.pem', $alias->{alias});
        }
        if (-e $file) {
            print "Export location $file exists - override (y/N)?";
            chomp(my $input = <STDIN>);
            if ($input !~ /y/i) {
                print "Aborted!\n";
                return;
            }
        }
        open(my $fh, ">", $file) || die "Unable to write to $file";
        print $fh $pem;
        close ($fh);
        print "Certificate was written to $file\n";
    }

   return 0;

}

sub __alias_print {

    my $alias = shift;
    my $subject = shift;

    if (!$alias || !$alias->{alias}) {
        return "  not set\n\n";
    }

    my $cert = CTX('api2')->get_cert( identifier => $alias->{identifier}, format => 'DBINFO' );

    my @out;
    push @out, "  Alias     : $alias->{alias}\n";
    push @out, "  Identifier: $alias->{identifier}\n";

    push @out, "  Subject   : $cert->{subject}\n" if ($subject);

    push @out, "  NotBefore : " . DateTime->from_epoch( epoch => $alias->{notbefore} )->strftime("%F %T");
    push @out, DateTime->from_epoch( epoch => $cert->{notbefore} )->strftime(" (%F %T)")
        if ($cert && $cert->{notbefore} != $alias->{notbefore});

    push @out, "\n";
    push @out, "  NotAfter  : " . DateTime->from_epoch( epoch => $alias->{notafter} )->strftime("%F %T");

    push @out, DateTime->from_epoch( epoch => $cert->{notafter} )->strftime(" (%F %T)")
        if ($cert && $cert->{notafter} != $alias->{notafter});

    push @out, "\n\n";
    return join "", @out;

}


sub certificate_chain {

    my $cert_name;
    my $issuer_name;
    if ( !exists( $params{name} ) || $params{name} eq '' ) {
        print STDERR "Please specify a certificate name with --name\n";
        return 1;
    }
    else {
        $cert_name = $params{name};
    }
    if ( !exists( $params{issuer} ) || $params{issuer} eq '' ) {
        print STDERR "Please specify an issuer name with --issuer\n";
        return 1;
    }
    else {
        $issuer_name = $params{issuer};
    }

    # maybe the certificate name is an alias, try to resolve it
    my $cert_identifier = __resolve_alias({
        NAME  => $cert_name,
        REALM => $params{realm},
    });

    # check whether the certificate is in the DB
    my $certificate = CTX('dbi')->select_one(
        columns => [ '*' ],
        from => 'certificate',
        where => {
            'identifier' => $cert_identifier,
            pki_realm  => $params{realm}
        }
    );

    if ( !defined $certificate
        && !defined $params{'force-certificate-not-found'} )
    {
        print STDERR "ERROR: Certificate '$cert_name' not found in realm "
          . "$params{realm}.\n";
        return 2;
    }

    my $issuer_identifier;

    # maybe the issuer name is an alias, try resolve it
    my $realm;
    if ( defined $params{'issuer-realm'} ) {
        $realm = $params{'issuer-realm'};
    }
    else {
        $realm = $params{realm};
    }
    $issuer_identifier = __resolve_alias({
        NAME  => $issuer_name,
        REALM => $realm,
    });

    my $dbi = CTX('dbi');
    # check whether the issuer is in the DB
    my $issuer = $dbi->select_one(
        columns => [ '*' ],
        from => 'certificate',
        where => {
            'identifier' => $issuer_identifier},
    );
    if ( !defined $issuer
        && !defined $params{'force-issuer-certificate-not-found'} )
    {
        print STDERR "ERROR: Issuer certificate '$issuer_name' "
          . "(identifier: $issuer_identifier) not found in database.\n";
        return 2;
    }

    # set the issuer_identifier for the given certificate
    $dbi->update(
        table => 'certificate',
        set  => { issuer_identifier => $issuer_identifier },
        where => {
            cert_key => $certificate->{cert_key},
            identifier      => $cert_identifier,
            pki_realm       => $certificate->{pki_realm},
        },
    );
    $dbi->commit();
    print "Successfully set $issuer_name (identifier: $issuer_identifier) "
      . "as issuer of certificate $cert_name (identifier: "
      . "$cert_identifier).\n";

    # TODO: maybe don't warn only, but let the user use --force to
    # specify that he knows what he is doing ...?
    if ( $issuer->{subject_key_identifier} ne
        $certificate->{authority_key_identifier} )
    {
        print STDERR "WARNING: The issuer's subject key identifier "
          . "extension ($issuer->{SUBJECT_KEY_IDENTIFIER}) does not "
          . "match the authority key identifier extension contained "
          . "in the certificate "
          . "($certificate->{authority_key_identifier}). Are you sure "
          . "your chain is correct?\n";
    }
    if ( $issuer->{subject} ne $certificate->{issuer_dn} ) {
        print STDERR "WARNING: The issuer's subject ($issuer->{SUBJECT}) "
          . "does not match the issuer DN contained in the certificate "
          . "($certificate->{ISSUER_DN}). Are you sure your chain is "
          . "correct?\n";
    }
    return 0;

}

sub certificate_list {

    my @realms;
    if ( defined $params{realm} ) {
        push @realms, $params{realm};
    }
    else {
        @realms = CTX('config')->get_keys(['system','realms']);
        push @realms, undef;  # add the magic empty realm
    }

    my $dbi = CTX('dbi');
    foreach my $realm (@realms) {
        if ( defined $realm ) {
            print "\nCertificates in $realm:\n";
        }
        else {
            print "\nCertificates in self-signed pseudo-realm:\n";
        }
        my $certificates;
        if ( defined $params{all} ) {
            $certificates = $dbi->select(
                from   => 'certificate',
                columns => ['*'],
                where => { pki_realm => $realm, },
            );
        }
        else {
            $certificates = $dbi->select(
                from_join   => 'aliases aliases.identifier=certificate.identifier certificate',
                columns => [
                    'aliases.alias',
                    'aliases.identifier',
                    'certificate.subject',
                    'certificate.issuer_dn',
                    'certificate.cert_key',
                    'certificate.issuer_identifier',
                    'certificate.data',
                    'certificate.status',
                    'certificate.subject_key_identifier',
                    'certificate.authority_key_identifier',
                    'certificate.notafter',
                    'certificate.notbefore',
                    'certificate.req_key',
                ],
                where => { 'aliases.pki_realm' => $realm, },
            );
        }

        while (my $cert = $certificates->fetchrow_hashref) {
            my $identifier;
            if ( defined $params{all} ) {    # look up aliases
                $identifier = $cert->{identifier};
                my $status = $cert->{status};
                if ( defined $status && $status eq 'REVOKED' ) {
                    print "\n  Identifier: "
                      . $cert->{identifier}
                      . " (REVOKED)\n";
                }
                else {
                    print "\n  Identifier: " . $cert->{identifier} . "\n";
                }
                my $aliases = $dbi->select(
                    from   => 'aliases',
                    columns => ['*'],
                    where => { identifier => $cert->{identifier}, },
                );
                while (my $alias = $aliases->fetchrow_hashref) {
                    print "    Alias:\n      "
                      . $alias->{alias} . " (in realm: " . $alias->{pki_realm} . ")\n";
                }
            }
            else {
                $identifier = $cert->{'identifier'};
                my $status = $cert->{'status'};
                if ( defined $status && $status eq 'REVOKED' ) {
                    print "\n  Identifier: "
                      . $cert->{'identifier'}
                      . " (REVOKED)\n";
                }
                else {
                    print "\n  Identifier: " . $cert->{'identifier'} . "\n";
                }
                print "    Alias:\n      "
                  . $cert->{'alias'} . "\n";
            }

            my $prefix = '';
            if ( defined $params{v} && $params{v} > 0 ) {

                # show subject and issuer dn
                my $subject   = $cert->{ 'subject' };
                my $issuer_dn = $cert->{ 'issuer_dn' };
                print "    Subject:\n      " . $subject . "\n";
                print "    Issuer DN:\n      " . $issuer_dn . "\n";
            }
            if ( defined $params{v} && $params{v} > 1 ) {

                # show chain
                my $chain = CTX('api2')->get_chain( start_with => $identifier );
                my $chain_str = join( ' -> ', @{ $chain->{identifiers} } );

                print "    Chain:\n      $chain_str";
                if ( $chain->{complete} == 1 ) {
                    print "(complete)\n";
                }
                else {
                    print "(INcomplete!)\n";
                }
            }
            if ( defined $params{v} && $params{v} > 2 ) {

                # show database entry
                my @fields = qw(
                  subject_key_identifier
                  authority_key_identifier
                  cert_key
                  issuer_identifier
                  status
                  notafter
                  notbefore
                  req_key
                );

                if ( $params{v} > 3 ) {
                    push @fields, qw(data);
                }

                foreach my $field (@fields) {
                    my $value;
                    if ( defined $cert->{ $field } ) {
                        $value = $cert->{ $field };
                    }
                    else {
                        $value = 'NULL';
                    }
                    $field =~ s/_/ /g;
                    print "    $field:\n      " . $value . "\n";
                }
            }
        }
    }
    exit 0;
}

sub alias_del {

    my %delete_hash = ();

    $delete_hash{PKI_REALM} = $params{realm};

    if ($params{identifier}) {
        $delete_hash{identifier} = $params{identifier};
    } elsif ($params{alias}) {
        $delete_hash{alias} = $params{alias};
    } else {
        print STDERR "You must specify either --identifier or --alias\n";
        return 1;
    }

    my $dbi = CTX('dbi');
    my $alias = $dbi->select_one(
        from   => 'aliases',
        columns => ['*'],
        where => \%delete_hash,
    );

    if (!$alias) {
        print STDERR "No alias entry found matching your request\n";
        return 2;
    }

    $dbi->delete(
        from => 'aliases',
        where => {
            alias => $alias->{alias},
            pki_realm => $alias->{pki_realm},
        }
    );
    $dbi->commit();

    print "Successfully removed the alias $alias->{alias}:\n";
    print "   Identifier: $alias->{identifier}\n";
    print "   Realm:      $alias->{pki_realm}\n";

    return 0;

}

sub key_list {

    my $dbi = CTX('dbi');
    my $config = CTX('config');
    my $realm = $params{realm};

    # TODO - Improve!
    # We use the alias table to find all keys in the realm
    # For the moment we assume the keys are defined explicit in the config
    # this will change in the future when we allow autodiscovery and default inheritance
    my $token_class = $config->get_hash(['realm',$realm,'crypto','type']);

    foreach my $class (keys %{$token_class}) {
        my $db_alias = $dbi->select(
            from    => 'aliases',
            columns => ['*'],
            where => {
                group_id     => $token_class->{$class},
                pki_realm => $realm,
            },
        );

        print "Keys for token group $token_class->{$class}\n";
        while (my $entry = $db_alias->fetchrow_hashref) {
            my $alias = $entry->{alias};
            my $key = $config->get(['realm',$realm,'crypto','token',$alias,'key']);

            my $status_flag = '?';
            if (!$key) {
                $status_flag = 'c';
            } elsif ( -e $key && ( !-s $key ) ) {
                $status_flag = '0';    # file exists but is of size zero
            } elsif ( -e $key ) {        # file exists and is non-zero
                $status_flag = '+';
            } else {                     # file does not exist (yet)
                $status_flag = '!';
            }
            print '    ' . $status_flag . ' ' . $alias . "\n";
        }
    }

    return 0;
}

sub hash_password {

    print 'Please type your password, end with return: ';

    my $passwd;
    if ($params{plain}) {
        $passwd = prompt;
        chomp $passwd;

    } else {

        $passwd = prompt -echo => '*';

        print "Please re-type your password: ";
        my $retype = prompt -echo => '*';

        chomp $passwd;
        chomp $retype;

        if ($passwd ne $retype) {
            print "Sorry, the passwords do not match\n";
            return 0;
        }
    }

    if (!$passwd) {
        return 0;
    }


    my $hash = OpenXPKI::Password::hash($params{scheme},$passwd);

    if (!$hash) {
        die "Unable to compute hash\n";
    }

    printf "Your hashed password is:\n%s\n", $hash;

    return 0;
}

sub __resolve_alias {

    my $arg_ref = shift;
    my $alias = CTX('dbi')->select_one(
        from => 'aliases',
        columns => [ 'identifier' ],
        where => {
            'alias'     => $arg_ref->{NAME},
            'pki_realm' => $arg_ref->{REALM},
        }
    );

    if (!$alias->{identifier}) {
        # likely it was an identifier already
        return $arg_ref->{NAME};
    }
    return $alias->{identifier};

}

sub __init {

    GetOptions( \%params, @options_spec ) or pod2usage( -verbose => 0 );

    Log::Log4perl->easy_init( {
        level    => defined $params{debug} ? $DEBUG : ( $params{verbose} ? $INFO : $WARN ),
        layout   => '[%p] %m%n'
    });

    if ($params{config}) {
        # we set the ENV here to outrule an external ENV setting and to have
        # it ready for the reload action (needed to find the correct pidfile)
        $ENV{OPENXPKI_CONF_PATH} = $params{config};
    } elsif ($params{instance}) {
        $ENV{OPENXPKI_CONF_PATH} = sprintf '/usr/local/etc/openxpki/%s/config.d', $params{instance};
    }

}

sub __check_realm {

    my $realm = $params{realm};

    if (!$realm) {
         my @realms = CTX('config')->get_keys(['system','realms']);
         if (scalar @realms == 1) {
            $params{realm} = shift @realms;
         } else {
            die "You must specify a realm using --realm\n";
        }
    } elsif (!CTX('config')->exists(['system','realms',$params{realm}])) {
        die  "The realm $realm does not exist!\n";
    }

    return 1;
}


eval {

if ($cmd eq 'initdb') {

    push @options_spec, qw(dry-run force);
    die "initdb is no longer supported!\nPlease use the provided schema dumps to setup your database.";

} elsif ($cmd eq 'certificate') {

    my $subcmd = shift;

    if ($subcmd eq 'id') {

        push @options_spec, qw(
            file=s
            format=s
        );
        __init();

        OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1});

        $ret = certificate_id();

    } elsif ($subcmd eq 'import') {

        push @options_spec, qw(
            realm=s
            file=s
            key=s
            issuer=s
            force-no-chain
            force-issuer
            force-certificate-already-exists
            force-certificate-ignore-existing
            force-no-verify
            revoked

            alias=s
            gen|generation=s
            group=s
            token=s
            notbefore=s
            notafter=s

            profile=s
        );

        # alias, gen, group, token are for alias shortcut
#            force-really-self-signed
#            force-issuer-not-found
#            force-certificate-already-exists


        __init();

        OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1});

        if (!$params{file}) {
            die "You need to specify the certificate to import with --file\n";
        }

        __check_realm() if ($params{realm} && $params{realm} ne '_global');

#       if ((!$params{alias} && $params{group}) || ($params{alias} && !$params{group})) {
#           die "You must always specify both --alias and --group \n";
#       }

        $ret = certificate_import();

    } elsif ($subcmd eq 'remove') {

        push @options_spec, qw(
            realm=s
            name=s
            force-is-issuer
        );
        __init();

        OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1});

        __check_realm() if ($params{realm});

        $ret = certificate_remove();

    } elsif ($subcmd eq 'chain') {

        push @options_spec, qw(
            issuer=s
            issuer-realm=s
            realm=s
            name=s
            force-certificate-not-found
            force-issuer-certificate-not-found
        );
        __init();

        OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1});

        $ret = certificate_chain();

    } elsif ($subcmd eq 'list') {

        push @options_spec, qw(
            realm=s
            all
            v+
        );
        __init();

        OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1});

        __check_realm();

        $ret = certificate_list();
    }


} elsif ($cmd eq 'alias') {

    push @options_spec, qw(
      alias=s
      remove
      update
      notbefore=s
      notafter=s
      realm=s
      gen|generation=s
      group=s
      token=s
      filter=s
      identifier=s
      subject
      nogroup
      export|o=s
      file=s
      key=s
      force-update-existing
      force-ignore-existing
      force-update-key
    );
    __init();

    OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','dbi_log','api2','crypto_layer','dbi'], SILENT => 1, CLI => 1});

    __check_realm() if (!$params{realm} || $params{realm} ne '_global');

    my $subcmd = shift || '';

    if ($params{remove} || $subcmd eq 'remove') {
        $ret = alias_del();
    } elsif ($params{update}  || $subcmd eq 'update') {
        $ret = alias_update();
    } elsif ($params{alias}) {
        $ret = alias_show();
    } elsif ($params{identifier} || $params{file}) {
        $ret = alias_add();
    } else {
        $ret = alias_list();
    }
} elsif ($cmd eq 'key') {

    my $subcmd = shift || '';

    if ($subcmd eq 'list') {

        push @options_spec, qw(
          realm=s
        );
        __init();

        OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','api2','dbi'], SILENT => 1,});

        __check_realm();

        $ret = key_list();

    }
} elsif ($cmd eq 'hashpwd') {

    push @options_spec, qw(
          scheme|s=s
          plain
    );
    __init();
    OpenXPKI::Server::Init::init({TASKS  => ['config_versioned','log','api2','dbi'], SILENT => 1, CLI => 1});

    if (!$params{scheme}) {
        $params{scheme} = 'ssha256';
    } elsif (!OpenXPKI::Password::has_scheme($params{scheme})) {
        die "Unsupported scheme - see perldoc OpenXPKI::Password\n";
    }

    $ret = hash_password();


} elsif ($cmd eq 'buildconfig') {

    push @options_spec, qw(
      output=s
      key=s
      cert=s
      chain=s
      openssl=s
      force
    );
    __init();

    $Storable::canonical = 1;

    delete $ENV{OPENXPKI_CONF_PATH};

    if (!$params{config}) {
        die "You must provide the path to the source config using --config!";
    }

    -e $params{config} || die "Given config path does not exist!";
    my $config = OpenXPKI::Config::Backend->new( LOCATION => $params{config} );
    if (!$config->exists('system')) {
        die "Loaded config does not contain the system node!";
    }

    my $target = $params{output} || 'config.oxi';

    if (-e $target) {
        if (!$params{force}) {
            die "Target $target already exists, please remove first\n";
        }
        unlink $target;
    }

    my $raw = freeze($config->_config());
    open FILE, ">", $target || die "Unable to open/write to $target";
    # Signed config
    if ($params{key} && $params{cert}) {
        my $FileUtils = OpenXPKI::FileUtils->new;
        my $infile = $FileUtils->get_safe_tmpfile({ TMP => "/tmp"});
        $FileUtils->write_file({ FILENAME => $infile, CONTENT => $raw, FORCE => 1 });
        my $outfile = $FileUtils->get_safe_tmpfile({ TMP => "/tmp"});

        my @command = ( $params{openssl} || 'openssl',
            'smime', '-md', 'sha256', '-binary', '-sign', '-nodetach',
            '-outform', 'PEM',
            '-in', $infile,
            '-signer', $params{cert},
            '-inkey', $params{key},
            '-out', $outfile,
        );

        push @command, '-certfile', $params{chain} if ($params{chain});

        my $command = Proc::SafeExec->new({
            exec   => \@command,
            #stdin  => 'new',
            #stdout => 'new',
            #stderr => 'new',
        });
        $command->wait();

        if ($command->exit_status() != 0 || ! -e $outfile) {
            die "Signature creation failed (OpenSSL returned ".$command->exit_status().")";
        }

        my $pkcs7  = $FileUtils->read_file($outfile);
        # replace headers
        $pkcs7 =~ s{-----(BEGIN|END)[^-]+-----}{-----$1 OPENXPKI SIGNED CONFIG V1-----}g;
        print FILE $pkcs7;
    } else {
        print FILE "-----BEGIN OPENXPKI CONFIG V1 -----\n".encode_base64($raw)."-----END OPENXPKI CONFIG V1-----";
    }
    close (FILE);

    print "File written to $target\nConfig hash is " . Digest::SHA::sha256_hex($raw) . "\n";

    $ret = 0;

} elsif ($cmd eq 'lintconfig') {

    push @options_spec, qw(
      module|m=s
    );
    __init();

    my $conf;
    if ($ENV{OPENXPKI_CONF_PATH}) {
        ## --config or --inst are rendered into ENV in _init
        $conf = $ENV{OPENXPKI_CONF_PATH};
        delete $ENV{OPENXPKI_CONF_PATH};
    } else {
        $conf = "/usr/local/etc/openxpki/config.d/";
    }

    -e $conf || die "Given config path $conf does not exist!";

    my $c = OpenXPKI::Config::Backend->new( LOCATION => $conf );
    print "Checking config at $conf\n";
    if ($c->get_hash('system')) {
        printf "Config ok (%s)\n", $c->checksum();
        $ret = 0;
    } else {
        # this means the yaml was ok but there is no system node
        if (defined $c) {
            print "Config was parsed but no system node was found\n";
        }
        $ret = 2;
    }

    if (defined $params{debug}) {
        my $hash = $c->get_hash('');
        if ($params{debug}) {
            printf "system config at %s:\n", $params{debug};
            my @path = split /\./, $params{debug};
            foreach my $item (@path) {
                if (!defined $hash->{$item}) {
                    print STDERR "No such component ($item)\n";
                    $hash = {};
                    last;
                }
                $hash = $hash->{$item};
            }
        }
        print Dump $hash;
    }

    foreach my $module (@{$params{module}}) {
        my $class = "OpenXPKI::Config::Lint::". ucfirst($module);
        eval "use $class;1";
        if ($EVAL_ERROR) {
            print "Unable to load lint module $module\n";
            print STDERR $EVAL_ERROR;
        } else {
            print "Lint $module: ";
            my $err = $class->new()->lint($c);
            if ($err) {
                print "\n$err\n";
                $ret = 3
            } else {
                print "ok\n";
            }
        }
    }

} elsif ($cmd eq 'version') {
    print "Version (core): " . $OpenXPKI::VERSION::VERSION . "\n";
    $ret = 0;

} elsif ($cmd eq 'man') {
    pod2usage( -verbose => 1 );
    $ret = 0;
}


};

if (ref $EVAL_ERROR eq "OpenXPKI::Exception") {
    print  OpenXPKI::i18n::gettext($EVAL_ERROR->message()) ."\n";
    map { print "   $_: " . $EVAL_ERROR->params->{$_} ."\n";  } keys %{$EVAL_ERROR->params};
    print "\n";
    exit 1;
} elsif (my $eval_err = $EVAL_ERROR) {
    die $eval_err;
    print "\n";
} elsif ($ret) {
    print "\n";
    pod2usage( -verbose => 0 ) if ($ret == 1 || $ret == 255);
    exit $ret;
}

exit 0;

1;


__END__

=head1 NAME

openxpkiadm - tool for management operations of OpenXPKI instances

=head1 USAGE

openxpkiadm COMMAND [SUBCOMMAND] [OPTIONS]

 Global options:
   --config DIR          Location of the configuration repository
                         optional, defaults to /usr/local/etc/openxpki/config.d
   --instance|i NAME     Shortcut to set the config path to
                         /usr/local/etc/openxpki/<instance>/config.d

 Commands:
   help               brief help message
   man                full documentation
   version            print program version and exit
   key                Manage keys
   certificate        Manage certificates
   hashpwd            Create the (salted) hash / argon2 kcv for a password
   alias              Manage the token alias table
   lintconfig         Parse config and shows errors from Config::Merge
   buildconfig        Create (signed) config blob from config tree

=head1 ARGUMENTS

Available commands:

=head2 initdb

Command was removed, use provided sql schema dumps to create database.

=head2 key

Key management for OpenXPKI Tokens (including issuing CAs and subsystems).

Command options:

   --realm               PKI Realm to operate on

=head3 key management subcommands

=over 8

=item B<list>

Shows token key information for the specified realm, including
key algorithm, key length and secret splitting information.
TODO: Key info not implemented yet!

Lists keys together with a status flag, which can be one of the
following:

  c - token not defined in crypto.token
  + - key exists and file is non-empty
  0 - key exists but file is empty
  ! - key files does not exist (yet)


Example:

  openxpkiadm key list --realm 'Root CA'

=back

=head2 certificate

Starts a certificate management command and allows to list, install,
delete and connect certificates for the configured PKI Realms.

  openxpkiadm certificate <subcommand> <options>

=head3 certificate management subcommands

=over 8

=item B<list>

Subcommand options (optional):

   --realm                  PKI realm to operate on
   --all                    Show all certificates
   -v                       Show subject and issuer DN as well
   -v -v                    Show chain as well
   -v -v -v                 Show (nearly complete) database entry
   -v -v -v -v              Show pubkey and certificate data, too

Lists certificates present in the database for
the specified realm. If --all is not specified, only certificates
that have an alias defined for them are listed. --all lists all
certificates, regardless of whether they have an alias or not.
If --realm is left out, the certificates in all realms are listed
The number of -v's increases the verbosity (see above for what is
listed in which case).

=item B<import>

Subcommand options:

Mandatory:
  --file                    the PEM file to import from

Optional:
  --revoked                 import with status "revoked"
  --issuer                  the identifier of the issuer
  --realm                   PKI realm to import certificate to

Force options (use only if you exactly now what you are doing!):
  --force-no-chain (only without issuer)
        Import even if the chain is incomplete, set NULL as issuer
  --force-issuer
        Force the issuer setting even if the chain validation fails
  --force-certificate-already-exists
        Force update for an existing certificate
  --force-certificate-ignore-existing
        Exit without error if the certificate exists
  --force-no-verify
        Build the chain but skip cryptographic verification

Once again, only use these options if you actually have to (the occasions
where this happens should be really, really rare). Note that
force-no-chain might result in a wrong issuers assignment if key
identifiers or subjects are ambiguous. Consider using explicit issuer in
that cases if possible.

Adds a certificate to the database. The issuer is usually auto-detected
and needs to be given only in rare cases. By default the certificates are
imported into the global realm, if you want to add them to a specific one,
you need to specify it. Note that a certificate always inherits the realm
of its issuer!

The command outputs the subject's DN, issuer's DN and the imported realm
for you to verify that you imported the correct certificate as well as a
unique identifier which can be used to globally reference the certificate
(i.e. for configuration or as an issuer). If you don't want to remember
the identifier, look into openxpkiadm certificate alias to find out
how to create a symbolic name for an identifier.

Examples:

  openxpkiadm certificate import --file cacert.pem

Import a certificate which issuer is not known in the "ServerCA" realm:

  openxpkiadm certificate import --file cacert.pem \
      --force-no-chain --realm ServerCA

If alias, generation/group or token is given it is used to add an alias
after import - this option is deprecated and will be removed, use the
alias command with --file instead.

=item B<remove>

Subcommand options:

Mandatory:
  --name            The alias or identifier of the certificate

Optional:
  --realm           The PKI realm in which the alias is defined

Force options (use only if you now what you are doing!):
  --force-is-issuer Delete certificate even though it is the
                    issuer of another certificate in the database

Removes a certificate from the database.

Example:

  openxpkiadm certificate remove --realm 'Root CA' \
        --name 'Root CA 1'

=item B<chain>

Subcommand options:

 Mandatory:
  --realm               The PKI realm to operate in
  --name                The alias or identifier of the child
  --issuer              The alias or identifier of the parent

Optional:
  --issuer-realm        The realm in which the issuer alias
                        is defined

Force options (use only if you now what you are doing!):
  --force-certificate-not-found
        Ignore that the certificate of the child was not found
        in the DB
  --force-issuer-certificate-not-found
        Ignore that the certificate of the parent was not found
        in the DB

Once again, only use these options if you actually have to (the
occasions where this happens should be really, really rare).

Specifies subject/issuer relationship in order to set up certificate
chains. The certificates to be connected must already be present in
the database (see B<import>). As those connections are already set up
during --import, this command exists for changing the issuer if you
made an error. It also allows to specify an issuer that does not
agree with the information contained in the certificate (but outputs
a warning)

 Example:

openxpkiadm certificate chain --realm 'Root CA' \
     --name 'Subordinate CA 1' --issuer 'root1'

=back

=head2 alias

An alias is a symbolic name for a certificate in a specific realm.
OpenXPKI uses aliases to manage the crypto tokens for signer and
helper tokens. Several configs options and commands are able to
process aliases, too.

The selection of functional tokens is done based on the notbefore/
notafter date. To force certain behaviour (e.g time of a ca rollover),
you can force a custom notbefore/notafter date on the aliases.

Common options:
    --realm        PKI realm for the alias
    --identifier   The identifier of the certificate
    --notbefore    custom notbefore date to set
    --notafter     custom notafter date to set
                   accepted formats are epoch or yyyy-mm-dd hh:mm:ss
                   a literal 0 restores the certificates validity.

There are different ways to deal with aliases:

=over

=item B<list tokens>

If you pass a realm but no identifier, you will receive the list of
active tokens for all token groups, the current root certificate
and, if set, the upcoming root certificate as used by scep I<GetNexCACert>.

For items with custom notbefore/notafter settings, the certificate's
value is shown in brackets:

    upcoming root ca:
        Alias     : root-2
        Identifier: xGBSVo6N-9gpjB8UFll4TS-u-Eo
        NotBefore : 2014-01-01 00:00:00 (2013-06-17 13:54:34)
        NotAfter  : 2016-12-31 23:59:59 (2020-06-17 13:54:34)

To show the certificates subject besides the identifier, add --subject.

To show a list of all or all active tokens, you can add the filter
parameter:

  --filter all or --filter active

You can also filter by a certain group name with --group <groupname>.

Specify --nogroup to list tokens that do not belong to a group.

=item B<show/export a single alias>

Show the details of a single alias and export the linked certificate to
a file.

  --alias    The full alias (e.g. ca-signer-1)
  --subject  Print the subject of the certificate
  --export   Write PEM encoded certificate to <target>.
             If target is a directory the alias is used as filename.
             If target is "-" the certificate is printed to the terminal
             including its textual representation

=item B<add functional token with automatic group discovery>

Looks up the name of the associated group and finds the next generation
index by looking up the present aliases in the group. Recommended.

  --token  The name of the token type you want to add,
           e.g. certsign or datasafe.
  --file   The filename of the PEM encoded certificate, can be given
           instead  of --identifier. If the certificate does not exists
           in the database it is imported.

Example:

    openxpkiadm alias --realm server-realm \
        --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \
        --token certsign

=item B<add functional token with manual group configuration>

The alias is automatically set to <group>-<generation>, e.g. server-ca-1.
The generation identifier is increased by one from the latest one found
in the same group.

  --group   The name of the group (e.g. server-ca)

Example:

    openxpkiadm alias --realm server-realm \
        --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \
        --group server-ca

=item B<explicit generation>

If you need to force a certain generation identifier, you can skip the
autodetection and provide the wanted index:

    --generation  The numeric index to use for this alias

This works with both methods above, token and group.

Example:

    openxpkiadm alias --realm server-realm \
        --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \
        --group server-ca --generation 42

=item B<add non-functional alias>

Adds the alias leaving group and generation empty.

  --alias               The symbolic name for the certificate

Example:

    openxpkiadm alias --realm server-realm \
        --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \
        --alias my-very-important-certificate

=item B<alias key import>

This applies as an extended functionality for all commands that create
an alias.

  --key    The filename of a PEM encoded private key.

When provided, the system tries to copy the key data contained in the
given file to the location defined in the token configuration. The token
configuration is read from the OpenXPKI server process via the socket
using the System stack to authenticate. Therefore this requires that the
daemon is up and allows access to the I<get_token_info> call for the
default System user (this configuration is currently hardcoded and can
not be changed).

For storage type "OPENXPKI" (file based) the key data is written into
the given key file, the file is set to 0400 permissions, owned by the
user the daemon runs with. The parent folder for the file must exist.

For storage type "DATAPOOL" the key blob is loaded into the datapool,
encrypted with the current DataVault token. This therefore requires
that the DataVault token is already set up and available for encryption.

=item B<force option>

All alias create commands will fail if the given certificate already
exists in the given group. There are two options to ignore existing items.

  --force-ignore-existing  return and ignore extra settings
  --force-update-existing  update validity and key for existing alias

This will silently return if an alias for the given identifier exists in
the given group and no explicit generation was given. If an explicit name
or generation was given this only return if the existing alias matches
the given name.

With --force-ignore-existing any given additional options will not be
processed.

The command will fail if a key to import is specified but the target
already exists. To force overwrite of an existing key use

    --force-update-key

Please note that this will overwrite the existing file in place!

=item B<update alias>

Update notebefore/notafter date or set key of an existing alias.

    --update        Indicates that you want to update an existing entry
    --alias         You can select the alias by name rather than passing
                    the identifier.

Example:

     openxpkiadm alias --update --realm democa \
         --alias ca-signer-1
         --notbefore "2014-01-01:00:00:00"

This updates notbefore, notafter is not changed.

Example:

     openxpkiadm alias --update --realm democa \
         --file ca-issuer-1.crt
         --key ca-issuer-1.pem

Assign the key found in ca-issuer-1.pem to the alias matching the
certfificate ca-issuer-1.crt. The command will die if no alias is found
for the given certificate or if there is already a key found. In case
you want "create or update" use --force-update-existing instead --update.

=item B<remove alias>

Remove the entry from the alias table.

  --remove          Indicates that the alias should be removed.
  --alias           You can select the alias by name rather than passing
                    the identifier.

Example:

    openxpkiadm alias --remove --realm server-realm \
        --identifier rzg0GhTx81ioYGXADfuuIxFd9fw \

    openxpkiadm alias --remove --realm server-realm \
        --alias server-ca-1

=back

=head2 hashpwd

Create the hash of a given password to be used with the internal user database.

Command options:

  --scheme   The hashing scheme to use, allowed values are
             sshaXXX|shaXXX|smd5|md5|crypt|argon2, default is ssha256
             see also OpenXPKI::Server::Authentication::Password

  --plain    do not hide the password on enter, no retype required
             should work with passwords piped to STDIN

Prompts for the password and prints the hashed value including the used
scheme as defined in RFC2307.

Also offers calculation of a token based on the Argon2 KDF.

=head2 lintconfig

Validate that the config tree is parseable, shows errors from underlying
Config::Merge such as YAML ident or quoting errors.

Path to the config is read from environment I<OPENXPKI_CONF_PATH> or
I<--config> switch on commandline. If both are not set, the default
location I</usr/local/etc/openxpki/config.d> is used.

Command options:

  -- module  Specify extra modules to apply on the config object for
             additional checks.

  --debug               Dump the full config tree as YAML structure
  --debug path.to.node  Dump the tree below this node as YAML structure

=head2 buildconfig

Serializes the config tree into a single transportable file that can be
signed.

Command options:

    --output  Name of the output file, default is config.oxi
    --key     Filename of the key to use for signing
    --cert    Filename of the certificate (together with key)
    --chain   Filename holding additonal certificates added as chain
    --force   Force overwrite of existing file

=head1 DESCRIPTION

B<openxpkiadm> is the administrative frontend for controlling the OpenXPKI
installation.

=over 8

The openxpkiadm script returns a 0 exit value on success, and >0 if  an
error occurs.

=back
