<commit_before>package OpenXPKI::Server::API2::Plugin::Cert::import_certificate;
use OpenXPKI::Server::API2::EasyPlugin;

=head1 NAME

OpenXPKI::Server::API2::Plugin::Cert::import_certificate

=cut

# CPAN modules
use Data::Dumper;

# Project modules
use OpenXPKI::Debug;
use OpenXPKI::Server::Context qw( CTX );
use OpenXPKI::Server::API2::Types;
use OpenXPKI::MooseParams;
use OpenXPKI::Crypt::X509;

use OpenXPKI::Server::Database; # to get AUTO_ID

=head1 COMMANDS

=head2 import_certificate

Parameters:

=over

=item * B<DATA>, certificate data (PEM encoded)

=item * B<PKI_REALM> (optional), set the PKI realm to this value (might be overridden by an
issuer's realm)

=item * B<FORCE_NOCHAIN> (optional), 1 = import certificate even if issuer is
unknown (then I<issuer_identifier> will not be set) or has an incomplete
signature chain.

=item * B<FORCE_ISSUER> (optional), 1 = enforce import even if it has an invalid
signature chain (i.e. verification failed).

=item * B<FORCE_NOVERIFY> (optional), 1 = do not validate signature chain (e.g.
if one of the certificates' CA has expired)

=item * B<REVOKED> (optional), Set to 1 to set the certificate status to "REVOKED"

=item * B<UPDATE> (optional), Do not throw an exception if certificate already exists, update it instead

=back

=cut
command "import_certificate" => {
    data           => { isa => 'PEMCert', required => 1, },
    issuer         => { isa => 'AlphaPunct', },
    pki_realm      => { isa => 'AlphaPunct', },
    force_nochain  => { isa => 'Bool', default => 0, },
    force_issuer   => { isa => 'Bool', default => 0, },
    force_noverify => { isa => 'Bool', default => 0, },
    revoked        => { isa => 'Bool|HashRef', default => 0, },
    update         => { isa => 'Bool', default => 0, },
    attributes     => { isa => 'HashRef'},
} => sub {
    my ($self, $params) = @_;

    if ($params->has_issuer and $params->force_nochain) {
        # TODO Use unique exception id instead of text and output command line specific hints in openxpkiadm
        OpenXPKI::Exception->throw(
            message => 'Option force-no-chain is not allowed with explicit issuer, use force-issuer instead!'
        );
    }

    my $dbi = CTX('dbi');

    my $x509 = OpenXPKI::Crypt::X509->new( $params->data );

    my $cert_identifier = $x509->get_cert_identifier();

    # Check if the certificate is already in the PKI
    my $existing_cert = $dbi->select_one(
        from => 'certificate',
        columns => [ qw( identifier pki_realm status ) ],
        where => { identifier => $cert_identifier },
    );

    OpenXPKI::Exception->throw(
        message => 'Certificate already exists in database',
        params  => {
            identifier => $existing_cert->{identifier},
            pki_realm => $existing_cert->{pki_realm} || '',
            status => $existing_cert->{status},
        },
    ) if ($existing_cert and not $params->update);


    my $cert_hash = {
        status => 'ISSUED',
        identifier => $cert_identifier,
        issuer_dn => $x509->get_issuer,
        cert_key => $x509->get_serial,
        subject => $x509->get_subject,
        subject_key_identifier =>  $x509->get_subject_key_id,
        authority_key_identifier => $x509->get_authority_key_id,
        notbefore => $x509->get_notbefore()->epoch(),
        notafter => $x509->get_notafter()->epoch(),
    };

    $cert_hash->{pki_realm} = $params->pki_realm if $params->has_pki_realm;

    # attach revocation information if provided
    if ($params->has_revoked) {
        if (ref $params->revoked eq 'HASH' && scalar keys %{$params->revoked}) {

            $cert_hash->{status} = 'REVOKED';

            my $r = $params->revoked;
            if (!$r->{revocation_time}) {
                $cert_hash->{revocation_time} = time();
            } elsif ($r->{revocation_time} =~ m{\A\d+\z}) {
                $cert_hash->{revocation_time} = $r->{revocation_time};
            } else {
                OpenXPKI::Exception->throw(
                    message => 'Provided revocation_time has invalid format (epoch expected)',
                );
            }

            if (!$r->{invalidity_time}) {
                # leave null
            } elsif ($r->{invalidity_time} =~ m{\A\d+\z}) {
                $cert_hash->{invalidity_time} = $r->{invalidity_time};
            } else {
                OpenXPKI::Exception->throw(
                    message => 'Provided invalidity_time has invalid format (epoch expected)',
                );
            }

            my $reason_code = $r->{reason_code} || 'unspecified';
            if ($reason_code !~ m{ \A (?: unspecified | keyCompromise | CACompromise | affiliationChanged | superseded | cessationOfOperation | certificateHold | removeFromCRL ) \z }xms) {
                OpenXPKI::Exception->throw(
                    message => 'Provided reason_code is not valid',
                );
            }
            $cert_hash->{reason_code} = $reason_code;

            #  hold_instruction_code not supported yet
        } elsif (ref $params->revoked eq '' && $params->revoked) {
            $cert_hash->{status} = 'REVOKED';
            $cert_hash->{revocation_time} = time();
            $cert_hash->{reason_code} = 'unspecified';
        }
    }


    # Query issuer certificate
    my $issuer_cert = $self->_get_issuer(
        cert            => $x509,
        explicit_issuer => $params->issuer,
        force_nochain   => $params->force_nochain,
    );

    # cert is self signed
    if ($issuer_cert and $issuer_cert eq "SELF") {
        $cert_hash->{issuer_identifier} = $cert_identifier;
    }
    # cert has known issuer
    elsif ($issuer_cert) {
        my $valid;
        #
        # No verfication requested ?
        #
        if ($params->force_noverify) {
            CTX('log')->system()->warn("Importing certificate without chain verification! $cert_identifier / " . $x509->get_subject);
            CTX('log')->audit('system')->warn('certificate import without chain validation', {
                certid    => $cert_identifier,
                key       => $x509->get_subject_key_id(),
            });
            $valid = 1;
        }
        else {
            $valid = $self->_is_issuer_valid(
                cert           => $x509,
                issuer_cert    => $issuer_cert,
                force_nochain  => $params->force_nochain,
            );
        }

        if (!$valid) {
            # force the invalid issuer
            if ($params->force_issuer) {
                CTX('log')->system->warn("Importing certificate with invalid chain with force! $cert_identifier / " . $x509->get_subject());
                CTX('log')->audit('system')->warn('certificate import without chain validation', {
                    certid    => $cert_identifier,
                    key       => $x509->get_subject_key_id(),
                });
            } else {
                OpenXPKI::Exception->throw(
                    message => 'Unable to build certificate chain',
                    params  => { issuer_identifier => $issuer_cert->{identifier}, issuer_subject => $issuer_cert->{subject} },
                );
            }
        }

        $cert_hash->{issuer_identifier} = $issuer_cert->{identifier};
        # if the issuer is in a realm, it forces the entity into the same one
        $cert_hash->{pki_realm} = $issuer_cert->{pki_realm} if $issuer_cert->{pki_realm};
    }


    $dbi->merge(<commit_msg>Fix missing PEM data when importing certificates<commit_after>package OpenXPKI::Server::API2::Plugin::Cert::import_certificate;
use OpenXPKI::Server::API2::EasyPlugin;

=head1 NAME

OpenXPKI::Server::API2::Plugin::Cert::import_certificate

=cut

# CPAN modules
use Data::Dumper;

# Project modules
use OpenXPKI::Debug;
use OpenXPKI::Server::Context qw( CTX );
use OpenXPKI::Server::API2::Types;
use OpenXPKI::MooseParams;
use OpenXPKI::Crypt::X509;

use OpenXPKI::Server::Database; # to get AUTO_ID

=head1 COMMANDS

=head2 import_certificate

Parameters:

=over
=item * C<XXX> I<Bool> - XXX. Default: XXX

=item * C<data> I<Str> - certificate data (PEM encoded)

=item * C<pki_realm> I<Str> - set the PKI realm to this value (optional, might be
overridden by an issuer's realm)

=item * C<force_nochain> I<Str> - 1 = import certificate even if issuer is
unknown (then I<issuer_identifier> will not be set) or has an incomplete
signature chain. Default: 0

=item * C<force_issuer> I<Bool> - 1 = enforce import even if it has an invalid
signature chain (i.e. verification failed). Default: 0

=item * C<force_noverify> I<Bool> - 1 = do not validate signature chain (e.g.
if one of the certificates' CAs has expired). Default: 0

=item * C<revoked> I<Bool> - set to 1 to set the certificate status to
I<REVOKED>. Default: 0

=item * C<update> I<Bool> - do not throw an exception if certificate already
exists, update it instead. Default: 0

=back

=cut
command "import_certificate" => {
    data           => { isa => 'PEMCert', required => 1, },
    issuer         => { isa => 'AlphaPunct', },
    pki_realm      => { isa => 'AlphaPunct', },
    force_nochain  => { isa => 'Bool', default => 0, },
    force_issuer   => { isa => 'Bool', default => 0, },
    force_noverify => { isa => 'Bool', default => 0, },
    revoked        => { isa => 'Bool|HashRef', default => 0, },
    update         => { isa => 'Bool', default => 0, },
    attributes     => { isa => 'HashRef'},
} => sub {
    my ($self, $params) = @_;

    if ($params->has_issuer and $params->force_nochain) {
        # TODO Use unique exception id instead of text and output command line specific hints in openxpkiadm
        OpenXPKI::Exception->throw(
            message => 'Option force-no-chain is not allowed with explicit issuer, use force-issuer instead!'
        );
    }

    my $dbi = CTX('dbi');

    my $x509 = OpenXPKI::Crypt::X509->new( $params->data );

    my $cert_identifier = $x509->get_cert_identifier();

    # Check if the certificate is already in the PKI
    my $existing_cert = $dbi->select_one(
        from => 'certificate',
        columns => [ qw( identifier pki_realm status ) ],
        where => { identifier => $cert_identifier },
    );

    OpenXPKI::Exception->throw(
        message => 'Certificate already exists in database',
        params  => {
            identifier => $existing_cert->{identifier},
            pki_realm => $existing_cert->{pki_realm} || '',
            status => $existing_cert->{status},
        },
    ) if ($existing_cert and not $params->update);


    my $cert_hash = {
        status => 'ISSUED',
        identifier => $cert_identifier,
        data => $x509->pem,
        # FIXME public_key is missing !
        issuer_dn => $x509->get_issuer,
        cert_key => $x509->get_serial,
        subject => $x509->get_subject,
        subject_key_identifier =>  $x509->get_subject_key_id,
        authority_key_identifier => $x509->get_authority_key_id,
        notbefore => $x509->get_notbefore()->epoch(),
        notafter => $x509->get_notafter()->epoch(),
    };

    $cert_hash->{pki_realm} = $params->pki_realm if $params->has_pki_realm;

    # attach revocation information if provided
    if ($params->has_revoked) {
        if (ref $params->revoked eq 'HASH' && scalar keys %{$params->revoked}) {

            $cert_hash->{status} = 'REVOKED';

            my $r = $params->revoked;
            if (!$r->{revocation_time}) {
                $cert_hash->{revocation_time} = time();
            } elsif ($r->{revocation_time} =~ m{\A\d+\z}) {
                $cert_hash->{revocation_time} = $r->{revocation_time};
            } else {
                OpenXPKI::Exception->throw(
                    message => 'Provided revocation_time has invalid format (epoch expected)',
                );
            }

            if (!$r->{invalidity_time}) {
                # leave null
            } elsif ($r->{invalidity_time} =~ m{\A\d+\z}) {
                $cert_hash->{invalidity_time} = $r->{invalidity_time};
            } else {
                OpenXPKI::Exception->throw(
                    message => 'Provided invalidity_time has invalid format (epoch expected)',
                );
            }

            my $reason_code = $r->{reason_code} || 'unspecified';
            if ($reason_code !~ m{ \A (?: unspecified | keyCompromise | CACompromise | affiliationChanged | superseded | cessationOfOperation | certificateHold | removeFromCRL ) \z }xms) {
                OpenXPKI::Exception->throw(
                    message => 'Provided reason_code is not valid',
                );
            }
            $cert_hash->{reason_code} = $reason_code;

            #  hold_instruction_code not supported yet
        } elsif (ref $params->revoked eq '' && $params->revoked) {
            $cert_hash->{status} = 'REVOKED';
            $cert_hash->{revocation_time} = time();
            $cert_hash->{reason_code} = 'unspecified';
        }
    }


    # Query issuer certificate
    my $issuer_cert = $self->_get_issuer(
        cert            => $x509,
        explicit_issuer => $params->issuer,
        force_nochain   => $params->force_nochain,
    );

    # cert is self signed
    if ($issuer_cert and $issuer_cert eq "SELF") {
        $cert_hash->{issuer_identifier} = $cert_identifier;
    }
    # cert has known issuer
    elsif ($issuer_cert) {
        my $valid;
        #
        # No verfication requested ?
        #
        if ($params->force_noverify) {
            CTX('log')->system()->warn("Importing certificate without chain verification! $cert_identifier / " . $x509->get_subject);
            CTX('log')->audit('system')->warn('certificate import without chain validation', {
                certid    => $cert_identifier,
                key       => $x509->get_subject_key_id(),
            });
            $valid = 1;
        }
        else {
            $valid = $self->_is_issuer_valid(
                cert           => $x509,
                issuer_cert    => $issuer_cert,
                force_nochain  => $params->force_nochain,
            );
        }

        if (!$valid) {
            # force the invalid issuer
            if ($params->force_issuer) {
                CTX('log')->system->warn("Forced import of certificate with invalid! $cert_identifier / " . $x509->get_subject());
                CTX('log')->audit('system')->warn('certificate import without chain validation', {
                    certid    => $cert_identifier,
                    key       => $x509->get_subject_key_id(),
                });
            } else {
                OpenXPKI::Exception->throw(
                    message => 'Unable to build certificate chain',
                    params  => { issuer_identifier => $issuer_cert->{identifier}, issuer_subject => $issuer_cert->{subject} },
                );
            }
        }

        $cert_hash->{issuer_identifier} = $issuer_cert->{identifier};
        # if the issuer is in a realm, it forces the entity into the same one
        $cert_hash->{pki_realm} = $issuer_cert->{pki_realm} if $issuer_cert->{pki_realm};
    }


    $dbi->merge(
{'fraction_non_alphanumeric': 0.13747023021963484, 'fraction_numerical': 0.009658639851812648, 'mean_word_length': 2.279887177261879, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 309, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4230235', 'n_tokens_mistral': 4791, 'n_tokens_neox': 4559, 'n_words': 1080}