3126 lines
90 KiB
Perl
3126 lines
90 KiB
Perl
# --
|
|
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
|
|
# --
|
|
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
|
|
# the enclosed file COPYING for license information (GPL). If you
|
|
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
|
|
# --
|
|
|
|
package Kernel::System::Crypt::SMIME;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Kernel::System::VariableCheck qw(:all);
|
|
|
|
our @ObjectDependencies = (
|
|
'Kernel::Config',
|
|
'Kernel::System::Cache',
|
|
'Kernel::System::DB',
|
|
'Kernel::System::FileTemp',
|
|
'Kernel::System::Log',
|
|
'Kernel::System::Main',
|
|
'Kernel::System::CustomerUser',
|
|
'Kernel::System::CheckItem',
|
|
);
|
|
|
|
=head1 NAME
|
|
|
|
Kernel::System::Crypt::SMIME - smime crypt backend lib
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This is a sub module of Kernel::System::Crypt and contains all smime functions.
|
|
|
|
=head1 PUBLIC INTERFACE
|
|
|
|
=cut
|
|
|
|
sub new {
|
|
my ( $Type, %Param ) = @_;
|
|
|
|
# allocate new hash for object
|
|
my $Self = {};
|
|
bless( $Self, $Type );
|
|
|
|
$Self->{Debug} = $Param{Debug} || 0;
|
|
|
|
# check if module is enabled
|
|
return 0 if !$Kernel::OM->Get('Kernel::Config')->Get('SMIME');
|
|
|
|
# call init()
|
|
$Self->_Init();
|
|
|
|
# check working ENV
|
|
return 0 if $Self->Check();
|
|
|
|
return $Self;
|
|
}
|
|
|
|
=head2 Check()
|
|
|
|
check if environment is working
|
|
|
|
my $Message = $CryptObject->Check();
|
|
|
|
=cut
|
|
|
|
sub Check {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
if ( !-e $Self->{Bin} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "No such $Self->{Bin}!",
|
|
);
|
|
return "No such $Self->{Bin}!";
|
|
}
|
|
elsif ( !-x $Self->{Bin} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "$Self->{Bin} not executable!",
|
|
);
|
|
return "$Self->{Bin} not executable!";
|
|
}
|
|
elsif ( !-e $Self->{CertPath} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "No such $Self->{CertPath}!",
|
|
);
|
|
return "No such $Self->{CertPath}!";
|
|
}
|
|
elsif ( !-d $Self->{CertPath} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "No such $Self->{CertPath} directory!",
|
|
);
|
|
return "No such $Self->{CertPath} directory!";
|
|
}
|
|
elsif ( !-w $Self->{CertPath} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "$Self->{CertPath} not writable!",
|
|
);
|
|
return "$Self->{CertPath} not writable!";
|
|
}
|
|
elsif ( !-e $Self->{PrivatePath} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "No such $Self->{PrivatePath}!",
|
|
);
|
|
return "No such $Self->{PrivatePath}!";
|
|
}
|
|
elsif ( !-d $Self->{PrivatePath} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "No such $Self->{PrivatePath} directory!",
|
|
);
|
|
return "No such $Self->{PrivatePath} directory!";
|
|
}
|
|
elsif ( !-w $Self->{PrivatePath} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "$Self->{PrivatePath} not writable!",
|
|
);
|
|
return "$Self->{PrivatePath} not writable!";
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
=head2 Crypt()
|
|
|
|
crypt a message
|
|
|
|
my $Message = $CryptObject->Crypt(
|
|
Message => $Message,
|
|
Certificates => [
|
|
{
|
|
Filename => $CertificateFilename,
|
|
},
|
|
{
|
|
Hash => $CertificateHash,
|
|
Fingerprint => $CertificateFingerprint,
|
|
},
|
|
# ...
|
|
]
|
|
);
|
|
|
|
my $Message = $CryptObject->Crypt(
|
|
Message => $Message,
|
|
Filename => $CertificateFilename,
|
|
);
|
|
|
|
my $Message = $CryptObject->Crypt(
|
|
Message => $Message,
|
|
Hash => $CertificateHash,
|
|
Fingerprint => $CertificateFingerprint,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub Crypt {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
for my $Needed (qw(Message)) {
|
|
if ( !$Param{$Needed} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $Needed!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( $Param{Certificates} && ref $Param{Certificates} ne 'ARRAY' ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Certificates Param: Must be an array reference!",
|
|
);
|
|
}
|
|
|
|
if ( !$Param{Certificates} && !$Param{Filename} && !( $Param{Hash} || $Param{Fingerprint} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => "Need Param: Filename or Hash and Fingerprint!",
|
|
Priority => 'error',
|
|
);
|
|
return;
|
|
}
|
|
|
|
# backwards compatibility
|
|
my @CertificateSearchParams;
|
|
if ( $Param{Certificates} ) {
|
|
@CertificateSearchParams = @{ $Param{Certificates} };
|
|
}
|
|
else {
|
|
my %SearchParam = %Param;
|
|
delete $SearchParam{Message};
|
|
push @CertificateSearchParams, \%SearchParam;
|
|
}
|
|
|
|
# get temp file object
|
|
my $FileTempObject = $Kernel::OM->Get('Kernel::System::FileTemp');
|
|
|
|
my @CertFiles;
|
|
|
|
SEARCHPARAM:
|
|
for my $SearchParam (@CertificateSearchParams) {
|
|
|
|
next SEARCHPARAM if !IsHashRefWithData($SearchParam);
|
|
|
|
my $Certificate = $Self->CertificateGet( %{$SearchParam} );
|
|
my ( $FHCertificate, $CertFile ) = $FileTempObject->TempFile();
|
|
print $FHCertificate $Certificate;
|
|
close $FHCertificate;
|
|
|
|
push @CertFiles, $CertFile;
|
|
}
|
|
|
|
if ( !@CertFiles ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "No certificates found!",
|
|
);
|
|
return;
|
|
}
|
|
|
|
my $CertFileStrg = join ' ', @CertFiles;
|
|
|
|
my ( $FH, $PlainFile ) = $FileTempObject->TempFile();
|
|
print $FH $Param{Message};
|
|
close $FH;
|
|
|
|
my ( $FHCrypted, $CryptedFile ) = $FileTempObject->TempFile();
|
|
close $FHCrypted;
|
|
|
|
my $Options = "smime -encrypt -binary -des3 -in $PlainFile -out $CryptedFile $CertFileStrg";
|
|
my $LogMessage = $Self->_CleanOutput(qx{$Self->{Cmd} $Options 2>&1});
|
|
if ($LogMessage) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Can't encrypt: $LogMessage!"
|
|
);
|
|
return;
|
|
}
|
|
|
|
my $CryptedRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $CryptedFile );
|
|
|
|
return if !$CryptedRef;
|
|
return $$CryptedRef;
|
|
}
|
|
|
|
=head2 Decrypt()
|
|
|
|
decrypt a message and returns a hash (Successful, Message, Data)
|
|
|
|
my %Message = $CryptObject->Decrypt(
|
|
Message => $CryptedMessage,
|
|
Filename => $Filename,
|
|
);
|
|
|
|
my %Message = $CryptObject->Decrypt(
|
|
Message => $CryptedMessage,
|
|
Hash => $Hash,
|
|
Fingerprint => $Fingerprint,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub Decrypt {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
for (qw(Message)) {
|
|
if ( !$Param{$_} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $_!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( !$Param{Filename} && !( $Param{Hash} || $Param{Fingerprint} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => "Need Param: Filename or Hash and Fingerprint!",
|
|
Priority => 'error',
|
|
);
|
|
return;
|
|
}
|
|
|
|
my $Filename = $Param{Filename} || '';
|
|
my $Certificate = $Self->CertificateGet(%Param);
|
|
my %Attributes = $Self->CertificateAttributes(
|
|
Certificate => $Certificate,
|
|
Filename => $Filename,
|
|
);
|
|
my ( $Private, $Secret ) = $Self->PrivateGet(%Attributes);
|
|
|
|
# get temp file object
|
|
my $FileTempObject = $Kernel::OM->Get('Kernel::System::FileTemp');
|
|
|
|
my ( $FHPrivate, $PrivateKeyFile ) = $FileTempObject->TempFile();
|
|
print $FHPrivate $Private;
|
|
close $FHPrivate;
|
|
my ( $FHCertificate, $CertFile ) = $FileTempObject->TempFile();
|
|
print $FHCertificate $Certificate;
|
|
close $FHCertificate;
|
|
my ( $FH, $CryptedFile ) = $FileTempObject->TempFile();
|
|
print $FH $Param{Message};
|
|
close $FH;
|
|
my ( $FHDecrypted, $PlainFile ) = $FileTempObject->TempFile();
|
|
close $FHDecrypted;
|
|
my ( $FHSecret, $SecretFile ) = $FileTempObject->TempFile();
|
|
print $FHSecret $Secret;
|
|
close $FHSecret;
|
|
|
|
my $Options = "smime -decrypt -in $CryptedFile -out $PlainFile -recip $CertFile -inkey $PrivateKeyFile"
|
|
. " -passin file:$SecretFile";
|
|
my $LogMessage = qx{$Self->{Cmd} $Options 2>&1};
|
|
unlink $SecretFile;
|
|
|
|
if (
|
|
$Param{SearchingNeededKey}
|
|
&& $LogMessage =~ m{PKCS7_dataDecode:no recipient matches certificate}
|
|
&& $LogMessage =~ m{PKCS7_decrypt:decrypt error}
|
|
)
|
|
{
|
|
return (
|
|
Successful => 0,
|
|
Message => 'Impossible to decrypt with installed private keys!',
|
|
);
|
|
}
|
|
|
|
if ($LogMessage) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Can't decrypt: $LogMessage!"
|
|
);
|
|
return (
|
|
Successful => 0,
|
|
Message => $LogMessage,
|
|
);
|
|
}
|
|
|
|
my $DecryptedRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $PlainFile );
|
|
if ( !$DecryptedRef ) {
|
|
return (
|
|
Successful => 0,
|
|
Message => "OpenSSL: Can't read $PlainFile!",
|
|
Data => undef,
|
|
);
|
|
|
|
}
|
|
return (
|
|
Successful => 1,
|
|
Message => "OpenSSL: OK",
|
|
Data => $$DecryptedRef,
|
|
);
|
|
}
|
|
|
|
=head2 Sign()
|
|
|
|
sign a message
|
|
|
|
my $Sign = $CryptObject->Sign(
|
|
Message => $Message,
|
|
Filename => $PrivateFilename,
|
|
);
|
|
my $Sign = $CryptObject->Sign(
|
|
Message => $Message,
|
|
Hash => $Hash,
|
|
Fingerprint => $Fingerprint,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub Sign {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
for (qw(Message)) {
|
|
if ( !$Param{$_} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $_!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( !$Param{Filename} && !( $Param{Hash} || $Param{Fingerprint} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => "Need Param: Filename or Hash and Fingerprint!",
|
|
Priority => 'error',
|
|
);
|
|
return;
|
|
}
|
|
|
|
my $Certificate = $Self->CertificateGet(%Param);
|
|
my %Attributes = $Self->CertificateAttributes(
|
|
Certificate => $Certificate,
|
|
Filename => $Param{Filename}
|
|
);
|
|
my ( $Private, $Secret ) = $Self->PrivateGet(%Attributes);
|
|
|
|
# get the related certificates
|
|
my @RelatedCertificates = $Self->SignerCertRelationGet( CertFingerprint => $Attributes{Fingerprint} );
|
|
|
|
# get temp file object
|
|
my $FileTempObject = $Kernel::OM->Get('Kernel::System::FileTemp');
|
|
|
|
my $FHCACertFileActive;
|
|
my ( $FHCACertFile, $CAFileName ) = $FileTempObject->TempFile();
|
|
|
|
my $CertFileCommand = '';
|
|
|
|
# get every related cert
|
|
for my $Cert (@RelatedCertificates) {
|
|
my $CAFilename = $Self->_CertificateFilename(
|
|
Hash => $Cert->{CAHash},
|
|
Fingerprint => $Cert->{CAFingerprint},
|
|
);
|
|
print $FHCACertFile $Self->CertificateGet( Filename => $CAFilename ) . "\n";
|
|
$FHCACertFileActive = 1;
|
|
}
|
|
|
|
if ($FHCACertFileActive) {
|
|
$CertFileCommand = " -certfile $CAFileName ";
|
|
}
|
|
close $FHCACertFile;
|
|
|
|
my ( $FH, $PlainFile ) = $FileTempObject->TempFile();
|
|
print $FH $Param{Message};
|
|
close $FH;
|
|
my ( $FHPrivate, $PrivateKeyFile ) = $FileTempObject->TempFile();
|
|
print $FHPrivate $Private;
|
|
close $FHPrivate;
|
|
my ( $FHCertificate, $CertFile ) = $FileTempObject->TempFile();
|
|
print $FHCertificate $Certificate;
|
|
close $FHCertificate;
|
|
my ( $FHSign, $SignFile ) = $FileTempObject->TempFile();
|
|
close $FHSign;
|
|
my ( $FHSecret, $SecretFile ) = $FileTempObject->TempFile();
|
|
print $FHSecret $Secret;
|
|
close $FHSecret;
|
|
|
|
my $Options = "smime -sign -in $PlainFile -out $SignFile -signer $CertFile -inkey $PrivateKeyFile"
|
|
. " -text -binary -passin file:$SecretFile";
|
|
|
|
# add the certfile parameter
|
|
$Options .= $CertFileCommand;
|
|
|
|
my $LogMessage = $Self->_CleanOutput(qx{$Self->{Cmd} $Options 2>&1});
|
|
unlink $SecretFile;
|
|
if ($LogMessage) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Can't sign: $LogMessage! (Command: $Options)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
my $SignedRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $SignFile );
|
|
|
|
return if !$SignedRef;
|
|
return $$SignedRef;
|
|
|
|
}
|
|
|
|
=head2 Verify()
|
|
|
|
verify a message with signature and returns a hash (Successful, Message, Signers, SignerCertificate)
|
|
|
|
my %Data = $CryptObject->Verify(
|
|
Message => $Message,
|
|
CACert => $PathtoCACert, # the certificates autority that endorse a self
|
|
# signed certificate
|
|
);
|
|
|
|
returns:
|
|
|
|
%Data = (
|
|
SignatureFound => 1, # or 0 if no signature was found
|
|
Successful => 1, # or 0 if the verification process failed
|
|
Message => $Message, # short version of the verification output
|
|
MessageLong => $MessageLong, # full verification output
|
|
Signers => [ # optional, array reference to all signers
|
|
'someone@company.com', # addresses
|
|
],
|
|
SignerCertificate => $SignerCertificate, # the certificate that signs the message
|
|
Content => $Content, # the message content
|
|
);
|
|
|
|
=cut
|
|
|
|
sub Verify {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my %Return;
|
|
my $Message = '';
|
|
my $MessageLong = '';
|
|
my $UsedKey = '';
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Message} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need Message!"
|
|
);
|
|
return;
|
|
}
|
|
|
|
# get temp file object
|
|
my $FileTempObject = $Kernel::OM->Get('Kernel::System::FileTemp');
|
|
|
|
my ( $FH, $SignedFile ) = $FileTempObject->TempFile();
|
|
print $FH $Param{Message};
|
|
close $FH;
|
|
my ( $FHOutput, $VerifiedFile ) = $FileTempObject->TempFile();
|
|
close $FHOutput;
|
|
my ( $FHSigner, $SignerFile ) = $FileTempObject->TempFile();
|
|
close $FHSigner;
|
|
|
|
# path to the cert, when self signed certs
|
|
# specially for openssl 1.0
|
|
my $CertificateOption = '';
|
|
if ( $Param{CACert} ) {
|
|
$CertificateOption = "-CAfile $Param{CACert}";
|
|
}
|
|
|
|
my $Options = "smime -verify -in $SignedFile -out $VerifiedFile -signer $SignerFile "
|
|
. "-CApath $Self->{CertPath} $CertificateOption $SignedFile";
|
|
|
|
my @LogLines = qx{$Self->{Cmd} $Options 2>&1};
|
|
|
|
for my $LogLine (@LogLines) {
|
|
$MessageLong .= $LogLine;
|
|
if ( $LogLine =~ /^\d.*:(.+?):.+?:.+?:$/ || $LogLine =~ /^\d.*:(.+?)$/ ) {
|
|
$Message .= ";$1";
|
|
}
|
|
else {
|
|
$Message .= $LogLine;
|
|
}
|
|
}
|
|
|
|
# get main object
|
|
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
|
|
|
|
my $SignerCertRef = $MainObject->FileRead( Location => $SignerFile );
|
|
my $SignedContentRef = $MainObject->FileRead( Location => $VerifiedFile );
|
|
|
|
# return message
|
|
if ( $Message =~ /Verification successful/i ) {
|
|
|
|
# Determine email address(es) from attributes of signer certificate.
|
|
my %SignerCertAttributes;
|
|
$Self->_FetchAttributesFromCert( $SignerFile, \%SignerCertAttributes );
|
|
my @SignersArray = split( ', ', $SignerCertAttributes{Email} );
|
|
|
|
# Include additional certificate attributes in the message:
|
|
# - signer(s) email address(es)
|
|
# - certificate hash
|
|
# - certificate fingerprint
|
|
# Please see bug#12284 for more information.
|
|
my $MessageSigner = join( ', ', @SignersArray ) . ' : '
|
|
. $SignerCertAttributes{Hash} . ' : '
|
|
. $SignerCertAttributes{Fingerprint};
|
|
|
|
%Return = (
|
|
SignatureFound => 1,
|
|
Successful => 1,
|
|
Message => 'OpenSSL: ' . $Message . ' (' . $MessageSigner . ')',
|
|
MessageLong => 'OpenSSL: ' . $MessageLong . ' (' . $MessageSigner . ')',
|
|
Signers => [@SignersArray],
|
|
SignerCertificate => $$SignerCertRef,
|
|
Content => $$SignedContentRef,
|
|
);
|
|
}
|
|
elsif ( $Message =~ /self signed certificate/i ) {
|
|
%Return = (
|
|
SignatureFound => 1,
|
|
Successful => 0,
|
|
Message =>
|
|
'OpenSSL: self signed certificate, to use it send the \'Certificate\' parameter : '
|
|
. $Message,
|
|
MessageLong =>
|
|
'OpenSSL: self signed certificate, to use it send the \'Certificate\' parameter : '
|
|
. $MessageLong,
|
|
SignerCertificate => $$SignerCertRef,
|
|
Content => $$SignedContentRef,
|
|
);
|
|
}
|
|
|
|
# digest failure means that the content of the email does not match witht he signature
|
|
elsif ( $Message =~ m{digest failure}i ) {
|
|
%Return = (
|
|
SignatureFound => 1,
|
|
Successful => 0,
|
|
Message =>
|
|
'OpenSSL: The signature does not match the message content : ' . $Message,
|
|
MessageLong =>
|
|
'OpenSSL: The signature does not match the message content : ' . $MessageLong,
|
|
SignerCertificate => $$SignerCertRef,
|
|
Content => $$SignedContentRef,
|
|
);
|
|
}
|
|
else {
|
|
%Return = (
|
|
SignatureFound => 0,
|
|
Successful => 0,
|
|
Message => 'OpenSSL: ' . $Message,
|
|
MessageLong => 'OpenSSL: ' . $MessageLong,
|
|
);
|
|
}
|
|
return %Return;
|
|
}
|
|
|
|
=head2 Search()
|
|
|
|
search a certificate or an private key
|
|
|
|
my @Result = $CryptObject->Search(
|
|
Search => 'some text to search',
|
|
);
|
|
|
|
=cut
|
|
|
|
sub Search {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my @Result = $Self->CertificateSearch(%Param);
|
|
@Result = ( @Result, $Self->PrivateSearch(%Param) );
|
|
return @Result;
|
|
}
|
|
|
|
=head2 CertificateSearch()
|
|
|
|
search a local certificate
|
|
|
|
my @Result = $CryptObject->CertificateSearch(
|
|
Search => 'some text to search',
|
|
Valid => 1
|
|
);
|
|
|
|
=cut
|
|
|
|
sub CertificateSearch {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my $Search = $Param{Search} || '';
|
|
$Param{Valid} //= 0;
|
|
|
|
# 1 - Get certificate list
|
|
my @CertList = $Self->CertificateList();
|
|
|
|
my @Result;
|
|
if (@CertList) {
|
|
|
|
# 2 - For the certs in list get its attributes and add them to @Results
|
|
@Result = $Self->_CheckCertificateList(
|
|
CertificateList => \@CertList,
|
|
Search => $Search,
|
|
Valid => $Param{Valid},
|
|
);
|
|
}
|
|
|
|
# 3 - If there are no results already in the system, then check for the certificate in customer data
|
|
if ( !@Result && $Kernel::OM->Get('Kernel::Config')->Get('SMIME::FetchFromCustomer') ) {
|
|
|
|
# Search and add certificates from Customer data if Result from CertList is empty
|
|
if (
|
|
$Search &&
|
|
$Self->FetchFromCustomer(
|
|
Search => $Search,
|
|
)
|
|
)
|
|
{
|
|
# 4 - if found, get its details and add them to the @Results
|
|
@CertList = $Self->CertificateList();
|
|
if (@CertList) {
|
|
@Result = $Self->_CheckCertificateList(
|
|
CertificateList => \@CertList,
|
|
Search => $Search,
|
|
Valid => $Param{Valid},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return @Result;
|
|
}
|
|
|
|
sub _CheckCertificateList {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my @CertList = @{ $Param{CertificateList} };
|
|
my $Search = $Param{Search} || '';
|
|
$Param{Valid} //= 0;
|
|
|
|
my @Result;
|
|
|
|
FILE:
|
|
for my $Filename (@CertList) {
|
|
my $Certificate = $Self->CertificateGet( Filename => $Filename );
|
|
my %Attributes = $Self->CertificateAttributes(
|
|
Certificate => $Certificate,
|
|
Filename => $Filename,
|
|
);
|
|
my $Hit = 0;
|
|
if ($Search) {
|
|
ATTRIBUTE:
|
|
for my $Attribute ( sort keys %Attributes ) {
|
|
if ( $Attributes{$Attribute} =~ m{\Q$Search\E}ixms ) {
|
|
$Hit = 1;
|
|
last ATTRIBUTE;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Hit = 1;
|
|
}
|
|
|
|
$Attributes{Filename} = $Filename;
|
|
|
|
if ($Hit) {
|
|
|
|
my $Expired = 0;
|
|
if ( $Param{Valid} ) {
|
|
$Expired = $Self->KeyExpiredCheck(
|
|
EndDate => $Attributes{EndDate},
|
|
);
|
|
}
|
|
|
|
next FILE if $Expired;
|
|
push @Result, \%Attributes;
|
|
}
|
|
}
|
|
|
|
return @Result;
|
|
}
|
|
|
|
=head2 FetchFromCustomer()
|
|
|
|
add certificates from CustomerUserAttributes to local certificates
|
|
returns an array of filenames of added certificates
|
|
|
|
my @Result = $CryptObject->FetchFromCustomer(
|
|
Search => $SearchEmailAddress,
|
|
);
|
|
|
|
Returns:
|
|
|
|
@Result = ( '6e620dcc.0', '8096d0a9.0', 'c01cdfa2.0' );
|
|
|
|
=cut
|
|
|
|
sub FetchFromCustomer {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Search} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need Search!"
|
|
);
|
|
return;
|
|
}
|
|
|
|
# Check customer users for userSMIMECertificate
|
|
my $CustomerUserObject = $Kernel::OM->Get('Kernel::System::CustomerUser');
|
|
my %CustomerUsers;
|
|
if ( $Param{Search} ) {
|
|
|
|
my $ValidEmail = $Kernel::OM->Get('Kernel::System::CheckItem')->CheckEmail(
|
|
Address => $Param{Search},
|
|
);
|
|
|
|
# If valid email address, only do a PostMasterSearch
|
|
if ($ValidEmail) {
|
|
%CustomerUsers = $CustomerUserObject->CustomerSearch(
|
|
PostMasterSearch => $Param{Search},
|
|
);
|
|
}
|
|
}
|
|
|
|
my @CertFileList;
|
|
|
|
# Check found CustomerUsers
|
|
for my $Login ( sort keys %CustomerUsers ) {
|
|
my %CustomerUser = $CustomerUserObject->CustomerUserDataGet(
|
|
User => $Login,
|
|
);
|
|
|
|
# Add Certificate if available
|
|
if ( $CustomerUser{UserSMIMECertificate} ) {
|
|
|
|
# if don't add, maybe in UnitTests
|
|
return @CertFileList if $Param{DontAdd};
|
|
|
|
# Convert certificate to the correct format (pk7, pk12, pem, der)
|
|
my $Cert = $Self->ConvertCertFormat(
|
|
String => $CustomerUser{UserSMIMECertificate},
|
|
);
|
|
|
|
my %Result = $Self->CertificateAdd(
|
|
Certificate => $Cert,
|
|
);
|
|
if ( $Result{Successful} && $Result{Successful} == 1 ) {
|
|
push @CertFileList, $Result{Filename};
|
|
}
|
|
}
|
|
}
|
|
|
|
return @CertFileList;
|
|
}
|
|
|
|
=head2 ConvertCertFormat()
|
|
|
|
Convert certificate strings into importable C<PEM> format.
|
|
|
|
my $Result = $CryptObject->ConvertCertFormat(
|
|
String => $CertificationString,
|
|
Passphrase => Password for PFX (optional)
|
|
);
|
|
|
|
Returns:
|
|
|
|
$Result =
|
|
"-----BEGIN CERTIFICATE-----
|
|
MIIEXjCCA0agAwIBAgIJAPIBQyBe/HbpMA0GCSqGSIb3DQEBBQUAMHwxCzAJBgNV
|
|
...
|
|
nj2wbQO4KjM12YLUuvahk5se
|
|
-----END CERTIFICATE-----
|
|
";
|
|
|
|
=cut
|
|
|
|
sub ConvertCertFormat {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
if ( !$Param{String} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need String!"
|
|
);
|
|
return;
|
|
}
|
|
my $String = $Param{String};
|
|
my $PassPhrase = $Param{Passphrase} // '';
|
|
|
|
my $FileTempObject = $Kernel::OM->Get('Kernel::System::FileTemp');
|
|
|
|
# Create original certificate file.
|
|
my ( $FileHandle, $TmpCertificate ) = $FileTempObject->TempFile();
|
|
print $FileHandle $String;
|
|
close $FileHandle;
|
|
|
|
# For PEM format no conversion needed.
|
|
my $Options = "x509 -in $TmpCertificate -noout";
|
|
my $ReadError = $Self->_CleanOutput(qx{$Self->{Cmd} $Options 2>&1});
|
|
|
|
return $String if !$ReadError;
|
|
|
|
# Create empty file (to save the converted certificate).
|
|
my ( $FH, $CertFile ) = $FileTempObject->TempFile(
|
|
Suffix => '.pem',
|
|
);
|
|
close $FH;
|
|
|
|
my %OptionsLookup = (
|
|
DER => {
|
|
Read => "x509 -inform der -in $TmpCertificate -noout",
|
|
Convert => "x509 -inform der -in $TmpCertificate -out $CertFile",
|
|
},
|
|
P7B => {
|
|
Read => "pkcs7 -in $TmpCertificate -noout",
|
|
Convert => "pkcs7 -in $TmpCertificate -print_certs -out $CertFile",
|
|
},
|
|
PFX => {
|
|
Read => "pkcs12 -in $TmpCertificate -noout -nomacver -passin pass:'$PassPhrase'",
|
|
Convert =>
|
|
"pkcs12 -in $TmpCertificate -out $CertFile -nomacver -clcerts -nokeys -passin pass:'$PassPhrase'",
|
|
},
|
|
);
|
|
|
|
# Determine the format of the file using OpenSSL.
|
|
my $DetectedFormat;
|
|
FORMAT:
|
|
for my $Format ( sort keys %OptionsLookup ) {
|
|
|
|
# Read the file on each format, if there is any output it means it could not be read.
|
|
next FORMAT if $Self->_CleanOutput(qx{$Self->{Cmd} $OptionsLookup{$Format}->{Read} 2>&1});
|
|
|
|
$DetectedFormat = $Format;
|
|
last FORMAT;
|
|
}
|
|
|
|
if ( !$DetectedFormat ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Certificate could not be read, PassPhrase is invalid or file is corrupted!",
|
|
);
|
|
return;
|
|
}
|
|
|
|
# Convert certificate to PEM.
|
|
my $ConvertError = $Self->_CleanOutput(qx{$Self->{Cmd} $OptionsLookup{$DetectedFormat}->{Convert} 2>&1});
|
|
|
|
if ($ConvertError) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Can't convert certificate from $DetectedFormat to PEM: $ConvertError",
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
# Read converted certificate.
|
|
my $CertFileRefPEM = $Kernel::OM->Get('Kernel::System::Main')->FileRead(
|
|
Location => $CertFile,
|
|
);
|
|
|
|
return ${$CertFileRefPEM};
|
|
}
|
|
|
|
=head2 CertificateAdd()
|
|
|
|
add a certificate to local certificates
|
|
returns result message and new certificate filename
|
|
|
|
my %Result = $CryptObject->CertificateAdd(
|
|
Certificate => $CertificateString,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub CertificateAdd {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Certificate} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need Certificate!'
|
|
);
|
|
return;
|
|
}
|
|
my %Attributes = $Self->CertificateAttributes(
|
|
Certificate => $Param{Certificate},
|
|
);
|
|
my %Result;
|
|
|
|
if ( !$Attributes{Hash} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Can\'t add invalid certificate!'
|
|
);
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => 'Can\'t add invalid certificate!',
|
|
);
|
|
return %Result;
|
|
}
|
|
|
|
# search for certs with same hash
|
|
my @Result = $Self->CertificateSearch(
|
|
Search => $Attributes{Hash},
|
|
);
|
|
|
|
# does the cert already exists?
|
|
for my $CertResult (@Result) {
|
|
if ( $Attributes{Fingerprint} eq $CertResult->{Fingerprint} ) {
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => 'Certificate already installed!',
|
|
);
|
|
return %Result;
|
|
}
|
|
}
|
|
|
|
# get cache object
|
|
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
|
|
|
# look for an available filename
|
|
FILENAME:
|
|
for my $Count ( 0 .. 99 ) {
|
|
if ( -e "$Self->{CertPath}/$Attributes{Hash}.$Count" ) {
|
|
next FILENAME;
|
|
}
|
|
|
|
my $File = "$Self->{CertPath}/$Attributes{Hash}.$Count";
|
|
## no critic
|
|
if ( open( my $OUT, '>', $File ) ) {
|
|
## use critic
|
|
print $OUT $Param{Certificate};
|
|
close($OUT);
|
|
%Result = (
|
|
Successful => 1,
|
|
Message => 'Certificate uploaded',
|
|
Filename => "$Attributes{Hash}.$Count",
|
|
);
|
|
|
|
# delete cache
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Cert',
|
|
);
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Private',
|
|
);
|
|
|
|
return %Result;
|
|
}
|
|
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Can't write $File: $!!"
|
|
);
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => "Can't write $File: $!!",
|
|
);
|
|
return %Result;
|
|
}
|
|
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => "No more available filenames for certificate hash:$Attributes{Hash}!",
|
|
);
|
|
return %Result;
|
|
}
|
|
|
|
=head2 CertificateGet()
|
|
|
|
get a local certificate
|
|
|
|
my $Certificate = $CryptObject->CertificateGet(
|
|
Filename => $CertificateFilename,
|
|
);
|
|
|
|
my $Certificate = $CryptObject->CertificateGet(
|
|
Fingerprint => $Fingerprint,
|
|
Hash => $Hash,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub CertificateGet {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Filename} && !( $Param{Fingerprint} && $Param{Hash} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need Filename or Fingerprint and Hash!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( !$Param{Filename} && ( $Param{Fingerprint} && $Param{Hash} ) ) {
|
|
$Param{Filename} = $Self->_CertificateFilename(%Param);
|
|
return if !$Param{Filename};
|
|
}
|
|
|
|
my $File = "$Self->{CertPath}/$Param{Filename}";
|
|
my $CertificateRef = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $File );
|
|
|
|
return if !$CertificateRef;
|
|
return $$CertificateRef;
|
|
}
|
|
|
|
=head2 CertificateRemove()
|
|
|
|
remove a local certificate
|
|
|
|
$CryptObject->CertificateRemove(
|
|
Filename => $CertificateHash,
|
|
);
|
|
|
|
$CryptObject->CertificateRemove(
|
|
Hash => $CertificateHash,
|
|
Fingerprint => $CertificateHash,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub CertificateRemove {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Filename} && !( $Param{Hash} && $Param{Fingerprint} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need Filename or Hash and Fingerprint!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( !$Param{Filename} && $Param{Hash} && $Param{Fingerprint} ) {
|
|
$Param{Filename} = $Self->_CertificateFilename(%Param);
|
|
return if !$Param{Filename};
|
|
}
|
|
|
|
my %Result;
|
|
|
|
my %CertificateAttributes = $Self->CertificateAttributes(
|
|
Certificate => $Self->CertificateGet( Filename => $Param{Filename} ),
|
|
Filename => $Param{Filename},
|
|
);
|
|
|
|
if (%CertificateAttributes) {
|
|
$Self->SignerCertRelationDelete(
|
|
CAFingerprint => $CertificateAttributes{Fingerprint},
|
|
);
|
|
}
|
|
|
|
# private certificate shouldn't exists if certificate is deleted
|
|
# therefor if exists, first remove private certificate
|
|
# if private delete fails abort certificate removing
|
|
|
|
my ($PrivateExists) = $Self->PrivateGet(
|
|
Filename => $Param{Filename},
|
|
);
|
|
|
|
if ($PrivateExists) {
|
|
my %PrivateResults = $Self->PrivateRemove(
|
|
Filename => $Param{Filename},
|
|
);
|
|
if ( !$PrivateResults{Successful} ) {
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => "Delete certificate aborted, $PrivateResults{Message}: $!!",
|
|
);
|
|
return %Result;
|
|
}
|
|
}
|
|
|
|
my $Message = "Certificate successfully removed";
|
|
my $Success = 1;
|
|
|
|
# remove certificate
|
|
my $Cert = unlink "$Self->{CertPath}/$Param{Filename}";
|
|
if ( !$Cert ) {
|
|
$Message = "Impossible to remove certificate: $Self->{CertPath}/$Param{Filename}: $!!";
|
|
$Success = 0;
|
|
}
|
|
|
|
if ($PrivateExists) {
|
|
$Message .= ". Private certificate successfully deleted";
|
|
}
|
|
|
|
if ($Success) {
|
|
|
|
# get cache object
|
|
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
|
|
|
# delete cache
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Cert',
|
|
);
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Private',
|
|
);
|
|
}
|
|
|
|
%Result = (
|
|
Successful => $Success,
|
|
Message => $Message,
|
|
);
|
|
|
|
return %Result;
|
|
}
|
|
|
|
=head2 CertificateList()
|
|
|
|
get list of local certificates filenames
|
|
|
|
my @CertList = $CryptObject->CertificateList();
|
|
|
|
=cut
|
|
|
|
sub CertificateList {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my @CertList;
|
|
my @Filters;
|
|
for my $Number ( 0 .. 99 ) {
|
|
push @Filters, "*.$Number";
|
|
}
|
|
|
|
my @List = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead(
|
|
Directory => "$Self->{CertPath}",
|
|
Filter => \@Filters,
|
|
);
|
|
|
|
for my $File (@List) {
|
|
$File =~ s{^.*/}{}xms;
|
|
push @CertList, $File;
|
|
}
|
|
|
|
return @CertList;
|
|
}
|
|
|
|
=head2 CertificateAttributes()
|
|
|
|
get certificate attributes
|
|
|
|
my %CertificateAttributes = $CryptObject->CertificateAttributes(
|
|
Certificate => $CertificateString,
|
|
Filename => '12345.1', # optional (useful to use cache)
|
|
);
|
|
|
|
=cut
|
|
|
|
sub CertificateAttributes {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my %Attributes;
|
|
if ( !$Param{Certificate} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need Certificate!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
# get cache object
|
|
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
|
|
|
my $CacheKey;
|
|
if ( defined $Param{Filename} && $Param{Filename} ) {
|
|
|
|
$CacheKey = 'CertAttributes::Filename::' . $Param{Filename};
|
|
|
|
# check cache
|
|
my $Cache = $CacheObject->Get(
|
|
Type => 'SMIME_Cert',
|
|
Key => $CacheKey,
|
|
);
|
|
|
|
# return if cache found,
|
|
return %{$Cache} if ref $Cache eq 'HASH';
|
|
}
|
|
|
|
# get temp file object
|
|
my $FileTempObject = $Kernel::OM->Get('Kernel::System::FileTemp');
|
|
|
|
my ( $FH, $Filename ) = $FileTempObject->TempFile();
|
|
print $FH $Param{Certificate};
|
|
close $FH;
|
|
$Self->_FetchAttributesFromCert( $Filename, \%Attributes );
|
|
if ( $Attributes{Hash} ) {
|
|
my ($Private) = $Self->PrivateGet(%Attributes);
|
|
if ($Private) {
|
|
$Attributes{Private} = 'Yes';
|
|
}
|
|
else {
|
|
$Attributes{Private} = 'No';
|
|
}
|
|
$Attributes{Type} = 'cert';
|
|
}
|
|
|
|
if ($CacheKey) {
|
|
|
|
# set cache
|
|
$CacheObject->Set(
|
|
Type => 'SMIME_Cert',
|
|
Key => $CacheKey,
|
|
Value => \%Attributes,
|
|
TTL => $Self->{CacheTTL},
|
|
);
|
|
}
|
|
|
|
return %Attributes;
|
|
}
|
|
|
|
=head2 CertificateRead()
|
|
|
|
show a local certificate in plain text
|
|
|
|
my $CertificateText = $CryptObject->CertificateRead(
|
|
Filename => $CertificateFilename,
|
|
);
|
|
|
|
my $CertificateText = $CryptObject->CertificateRead(
|
|
Fingerprint => $Fingerprint,
|
|
Hash => $Hash,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub CertificateRead {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Filename} && !( $Param{Fingerprint} && $Param{Hash} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need Filename or Fingerprint and Hash!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( !$Param{Filename} && ( $Param{Fingerprint} && $Param{Hash} ) ) {
|
|
$Param{Filename} = $Self->_CertificateFilename(%Param);
|
|
return if !$Param{Filename};
|
|
}
|
|
|
|
my $File = "$Self->{CertPath}/$Param{Filename}";
|
|
|
|
# check if file exists and can be readed
|
|
if ( !-e $File ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Certificate $File does not exist!"
|
|
);
|
|
return;
|
|
}
|
|
if ( !-r $File ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Can not read certificate $File!"
|
|
);
|
|
return;
|
|
}
|
|
|
|
# set options to retrieve certiciate contents
|
|
my $Options = "x509 -in $File -noout -text";
|
|
|
|
# get the output string
|
|
my $Output = qx{$Self->{Cmd} $Options 2>&1};
|
|
|
|
return $Output;
|
|
}
|
|
|
|
=head2 PrivateSearch()
|
|
|
|
returns private keys
|
|
|
|
my @Result = $CryptObject->PrivateSearch(
|
|
Search => 'some text to search',
|
|
Valid => 1 # optional
|
|
);
|
|
|
|
=cut
|
|
|
|
sub PrivateSearch {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my $Search = $Param{Search} || '';
|
|
$Param{Valid} //= 0;
|
|
my @Result;
|
|
my @Certificates = $Self->CertificateList();
|
|
|
|
FILE:
|
|
for my $File (@Certificates) {
|
|
my $Certificate = $Self->CertificateGet( Filename => $File );
|
|
my %Attributes = $Self->CertificateAttributes(
|
|
Certificate => $Certificate,
|
|
Filename => $File,
|
|
);
|
|
|
|
my $Hit = 0;
|
|
if ($Search) {
|
|
ATTRIBUTE:
|
|
for my $Attribute ( sort keys %Attributes ) {
|
|
if ( $Attributes{$Attribute} =~ m{\Q$Search\E}ixms ) {
|
|
$Hit = 1;
|
|
last ATTRIBUTE;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Hit = 1;
|
|
}
|
|
if ( $Hit && $Attributes{Private} && $Attributes{Private} eq 'Yes' ) {
|
|
$Attributes{Type} = 'key';
|
|
$Attributes{Filename} = $File;
|
|
|
|
my $Expired = 0;
|
|
if ( $Param{Valid} ) {
|
|
$Expired = $Self->KeyExpiredCheck(
|
|
EndDate => $Attributes{EndDate},
|
|
);
|
|
}
|
|
|
|
next FILE if $Expired;
|
|
push @Result, \%Attributes;
|
|
}
|
|
}
|
|
|
|
return @Result;
|
|
}
|
|
|
|
=head2 KeyExpiredCheck()
|
|
|
|
returns if SMIME key is expired
|
|
|
|
my $Valid = $CryptObject->KeyExpiredCheck(
|
|
EndDate => 'May 12 23:50:40 2018 GMT',
|
|
);
|
|
|
|
=cut
|
|
|
|
sub KeyExpiredCheck {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
if ( !$Param{EndDate} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need EndDate!"
|
|
);
|
|
return;
|
|
}
|
|
|
|
my %Months = (
|
|
Jan => '01',
|
|
Feb => '02',
|
|
Mar => '03',
|
|
Apr => '04',
|
|
May => '05',
|
|
Jun => '06',
|
|
Jul => '07',
|
|
Aug => '08',
|
|
Sep => '09',
|
|
Oct => '10',
|
|
Nov => '11',
|
|
Dec => '12',
|
|
);
|
|
|
|
# EndDate is in this format: May 12 23:50:40 2018 GMT
|
|
# It is transformed in supported format for DateTimeObject: 2018-05-12T23:50:40GMT
|
|
if ( $Param{EndDate} =~ /(.+?)\s(.+?)\s(\d\d:\d\d:\d\d)\s(\d\d\d\d)\s(\w+)/ ) {
|
|
my $Day = int($2);
|
|
my $Month = $Months{$1};
|
|
my $Year = $4;
|
|
|
|
if ( $Day < 10 ) {
|
|
$Day = "0$Day";
|
|
}
|
|
|
|
my $EndDateTimeObject = $Kernel::OM->Create(
|
|
'Kernel::System::DateTime',
|
|
ObjectParams => {
|
|
String => "$Year-" . $Month . "-" . $Day . "T$3" . $5,
|
|
},
|
|
);
|
|
|
|
my $CurrentTimeObject = $Kernel::OM->Create(
|
|
'Kernel::System::DateTime',
|
|
);
|
|
|
|
# Check if key is expired.
|
|
if ( $EndDateTimeObject->Compare( DateTimeObject => $CurrentTimeObject ) == -1 ) {
|
|
return 1;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
=head2 PrivateAdd()
|
|
|
|
add private key
|
|
|
|
my %Result = $CryptObject->PrivateAdd(
|
|
Private => $PrivateKeyString,
|
|
Secret => 'Password',
|
|
);
|
|
|
|
=cut
|
|
|
|
sub PrivateAdd {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
for my $Needed (qw(Private Secret)) {
|
|
if ( !$Param{$Needed} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $Needed!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
my %Result;
|
|
|
|
# get private attributes
|
|
my %Attributes = $Self->PrivateAttributes(%Param);
|
|
if ( !$Attributes{Modulus} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'No Private Key!'
|
|
);
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => 'No private key',
|
|
);
|
|
return;
|
|
}
|
|
|
|
# get certificate
|
|
my @Certificates = $Self->CertificateSearch( Search => $Attributes{Modulus} );
|
|
if ( !@Certificates ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need Certificate of Private Key first -$Attributes{Modulus})!",
|
|
);
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => "Need Certificate of Private Key first -$Attributes{Modulus})!",
|
|
);
|
|
return %Result;
|
|
}
|
|
elsif ( $#Certificates > 0 ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Multiple Certificates with the same Modulus, can\'t add Private Key!',
|
|
);
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => 'Multiple Certificates with the same Modulus, can\'t add Private Key!',
|
|
);
|
|
return %Result;
|
|
}
|
|
my %CertificateAttributes = $Self->CertificateAttributes(
|
|
Certificate => $Self->CertificateGet( Filename => $Certificates[0]->{Filename} ),
|
|
Filename => $Certificates[0]->{Filename},
|
|
);
|
|
if ( $CertificateAttributes{Hash} ) {
|
|
my $File = "$Self->{PrivatePath}/$Certificates[0]->{Filename}";
|
|
## no critic
|
|
if ( open( my $PrivKeyFH, '>', "$File" ) ) {
|
|
## use critic
|
|
print $PrivKeyFH $Param{Private};
|
|
close $PrivKeyFH;
|
|
open( my $PassFH, '>', "$File.P" ); ## no critic
|
|
print $PassFH $Param{Secret};
|
|
close $PassFH;
|
|
%Result = (
|
|
Successful => 1,
|
|
Message => 'Private Key uploaded!',
|
|
Filename => $Certificates[0]->{Filename},
|
|
);
|
|
|
|
# get cache object
|
|
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
|
|
|
# delete cache
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Cert',
|
|
);
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Private',
|
|
);
|
|
|
|
return %Result;
|
|
}
|
|
else {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Can't write $File: $!!"
|
|
);
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => "Can't write $File: $!!",
|
|
);
|
|
return %Result;
|
|
}
|
|
}
|
|
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Can\'t add invalid private key!'
|
|
);
|
|
%Result = (
|
|
Successful => 0,
|
|
Message => 'Can\'t add invalid private key!',
|
|
);
|
|
|
|
return %Result;
|
|
}
|
|
|
|
=head2 PrivateGet()
|
|
|
|
get private key
|
|
|
|
my ($PrivateKey, $Secret) = $CryptObject->PrivateGet(
|
|
Filename => $PrivateFilename,
|
|
);
|
|
|
|
my ($PrivateKey, $Secret) = $CryptObject->PrivateGet(
|
|
Hash => $Hash,
|
|
Modulus => $Modulus,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub PrivateGet {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Filename} && !( $Param{Hash} && $Param{Modulus} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need Filename or Hash and Modulus!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( !$Param{Filename} && ( $Param{Hash} && $Param{Modulus} ) ) {
|
|
$Param{Filename} = $Self->_PrivateFilename(
|
|
Hash => $Param{Hash},
|
|
Modulus => $Param{Modulus},
|
|
);
|
|
return if !$Param{Filename};
|
|
}
|
|
|
|
my $File = "$Self->{PrivatePath}/$Param{Filename}";
|
|
|
|
# get main object
|
|
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
|
|
|
|
my $Private;
|
|
if ( -e $File ) {
|
|
$Private = $MainObject->FileRead( Location => $File );
|
|
}
|
|
|
|
return if !$Private;
|
|
|
|
# read secret
|
|
$File = "$Self->{PrivatePath}/$Param{Filename}.P";
|
|
my $Secret = $MainObject->FileRead( Location => $File );
|
|
|
|
return ( $$Private, $$Secret ) if ( $Private && $Secret );
|
|
|
|
return;
|
|
}
|
|
|
|
=head2 PrivateRemove()
|
|
|
|
remove private key
|
|
|
|
$CryptObject->PrivateRemove(
|
|
Filename => $Filename,
|
|
);
|
|
|
|
$CryptObject->PrivateRemove(
|
|
Hash => $Hash,
|
|
Modulus => $Modulus,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub PrivateRemove {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{Filename} && !( $Param{Hash} && $Param{Modulus} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need Filename or Hash and Modulus!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
my %Return;
|
|
if ( !$Param{Filename} && ( $Param{Hash} && $Param{Modulus} ) ) {
|
|
$Param{Filename} = $Self->_PrivateFilename(
|
|
Hash => $Param{Hash},
|
|
Modulus => $Param{Modulus},
|
|
);
|
|
%Return = (
|
|
Successful => 0,
|
|
Message => "Filename not found for hash: $Param{Hash} in: $Self->{PrivatePath}, $!!",
|
|
);
|
|
return %Return if !$Param{Filename};
|
|
}
|
|
|
|
my $SecretDelete = unlink "$Self->{PrivatePath}/$Param{Filename}.P";
|
|
|
|
# abort if secret is not deleted
|
|
if ( !$SecretDelete ) {
|
|
%Return = (
|
|
Successful => 0,
|
|
Message =>
|
|
"Delete private aborted, not possible to delete Secret: $Self->{PrivatePath}/$Param{Filename}.P, $!!",
|
|
);
|
|
return %Return;
|
|
}
|
|
|
|
my $PrivateDelete = unlink "$Self->{PrivatePath}/$Param{Filename}";
|
|
if ($PrivateDelete) {
|
|
|
|
my $Certificate = $Self->CertificateGet(
|
|
Filename => $Param{Filename},
|
|
);
|
|
|
|
# get cert attributes
|
|
my %CertificateAttributes = $Self->CertificateAttributes(
|
|
Certificate => $Certificate,
|
|
Filename => $Param{Filename},
|
|
);
|
|
|
|
$Self->SignerCertRelationDelete(
|
|
CertFingerprint => $CertificateAttributes{Fingerprint},
|
|
);
|
|
|
|
%Return = (
|
|
Successful => 1,
|
|
Message => 'Private key deleted!'
|
|
);
|
|
|
|
# get cache object
|
|
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
|
|
|
# delete cache
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Cert',
|
|
);
|
|
$CacheObject->CleanUp(
|
|
Type => 'SMIME_Private',
|
|
);
|
|
|
|
return %Return;
|
|
}
|
|
|
|
%Return = (
|
|
Successful => 0,
|
|
Message => "Impossible to delete key $Param{Filename} $!!"
|
|
);
|
|
|
|
return %Return;
|
|
}
|
|
|
|
=head2 PrivateList()
|
|
|
|
returns a list of private key hashes
|
|
|
|
my @PrivateList = $CryptObject->PrivateList();
|
|
|
|
=cut
|
|
|
|
sub PrivateList {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my @CertList;
|
|
my @Filters;
|
|
for my $Number ( 0 .. 99 ) {
|
|
push @Filters, "*.$Number";
|
|
}
|
|
|
|
my @List = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead(
|
|
Directory => "$Self->{PrivatePath}",
|
|
Filter => \@Filters,
|
|
);
|
|
|
|
for my $File (@List) {
|
|
$File =~ s{^.*/}{}xms;
|
|
push @CertList, $File;
|
|
}
|
|
|
|
return @CertList;
|
|
|
|
}
|
|
|
|
=head2 PrivateAttributes()
|
|
|
|
returns attributes of private key
|
|
|
|
my %Hash = $CryptObject->PrivateAttributes(
|
|
Private => $PrivateKeyString,
|
|
Secret => 'Password',
|
|
Filename => '12345.1', # optional (useful for cache)
|
|
);
|
|
|
|
=cut
|
|
|
|
sub PrivateAttributes {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
for my $Needed (qw(Private Secret)) {
|
|
if ( !$Param{$Needed} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $Needed!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
# get cache object
|
|
my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache');
|
|
|
|
my $CacheKey;
|
|
if ( defined $Param{Filename} && $Param{Filename} ) {
|
|
|
|
$CacheKey = 'PrivateAttributes::Filename::' . $Param{Filename};
|
|
|
|
# check cache
|
|
my $Cache = $CacheObject->Get(
|
|
Type => 'SMIME_Private',
|
|
Key => $CacheKey,
|
|
);
|
|
|
|
# return if cache found,
|
|
return %{$Cache} if ref $Cache eq 'HASH';
|
|
}
|
|
|
|
# get temp file object
|
|
my $FileTempObject = $Kernel::OM->Get('Kernel::System::FileTemp');
|
|
|
|
my %Attributes;
|
|
my %Option = (
|
|
Modulus => '-modulus',
|
|
);
|
|
my ( $FH, $Filename ) = $FileTempObject->TempFile();
|
|
print $FH $Param{Private};
|
|
close $FH;
|
|
my ( $FHSecret, $SecretFile ) = $FileTempObject->TempFile();
|
|
print $FHSecret $Param{Secret};
|
|
close $FHSecret;
|
|
my $Options = "rsa -in $Filename -noout -modulus -passin file:$SecretFile";
|
|
my $LogMessage = qx{$Self->{Cmd} $Options 2>&1};
|
|
unlink $SecretFile;
|
|
$LogMessage =~ tr{\r\n}{}d;
|
|
$LogMessage =~ s/Modulus=//;
|
|
$Attributes{Modulus} = $LogMessage;
|
|
$Attributes{Type} = 'P';
|
|
|
|
if ($CacheKey) {
|
|
|
|
# set cache
|
|
$CacheObject->Set(
|
|
Type => 'SMIME_Private',
|
|
Key => $CacheKey,
|
|
Value => \%Attributes,
|
|
TTL => $Self->{CacheTTL},
|
|
);
|
|
}
|
|
|
|
return %Attributes;
|
|
}
|
|
|
|
=head2 SignerCertRelationAdd ()
|
|
|
|
add a relation between signer certificate and CA certificate to attach to the signature
|
|
|
|
my $Success = $CryptObject->SignerCertRelationAdd(
|
|
CertFingerprint => $CertFingerprint,
|
|
CAFingerprint => $CAFingerprint,
|
|
UserID => 1,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub SignerCertRelationAdd {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
for my $Needed (qw( CertFingerprint CAFingerprint UserID )) {
|
|
if ( !$Param{$Needed} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $Needed!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( $Param{CertFingerprint} eq $Param{CAFingerprint} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'CertFingerprint must be different to the CAFingerprint param',
|
|
);
|
|
return;
|
|
}
|
|
|
|
# searh certificates by fingerprint
|
|
my @CertResult = $Self->PrivateSearch(
|
|
Search => $Param{CertFingerprint},
|
|
);
|
|
|
|
# results?
|
|
if ( !scalar @CertResult ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => "Wrong CertFingerprint, certificate not found!",
|
|
Priority => 'error',
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
# searh certificates by fingerprint
|
|
my @CAResult = $Self->CertificateSearch(
|
|
Search => $Param{CAFingerprint},
|
|
);
|
|
|
|
# results?
|
|
if ( !scalar @CAResult ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => "Wrong CAFingerprint, certificate not found!",
|
|
Priority => 'error',
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
my $Success = $Kernel::OM->Get('Kernel::System::DB')->Do(
|
|
SQL => 'INSERT INTO smime_signer_cert_relations'
|
|
. ' ( cert_hash, cert_fingerprint, ca_hash, ca_fingerprint, create_time, create_by, change_time, change_by)'
|
|
. ' VALUES (?, ?, ?, ?, current_timestamp, ?, current_timestamp, ?)',
|
|
Bind => [
|
|
\$CertResult[0]->{Hash}, \$CertResult[0]->{Fingerprint}, \$CAResult[0]->{Hash},
|
|
\$CAResult[0]->{Fingerprint},
|
|
\$Param{UserID}, \$Param{UserID},
|
|
],
|
|
);
|
|
|
|
return $Success;
|
|
}
|
|
|
|
=head2 SignerCertRelationGet ()
|
|
|
|
get relation data by ID or by Certificate finger print
|
|
returns data Hash if ID given or Array of all relations if CertFingerprint given
|
|
|
|
my %Data = $CryptObject->SignerCertRelationGet(
|
|
ID => $RelationID,
|
|
);
|
|
|
|
my @Data = $CryptObject->SignerCertRelationGet(
|
|
CertFingerprint => $CertificateFingerprint,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub SignerCertRelationGet {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{ID} && !$Param{CertFingerprint} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Needed ID or CertFingerprint!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
# get database object
|
|
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
|
|
|
# ID
|
|
my %Data;
|
|
my @Data;
|
|
if ( $Param{ID} ) {
|
|
my $Success = $DBObject->Prepare(
|
|
SQL =>
|
|
'SELECT id, cert_hash, cert_fingerprint, ca_hash, ca_fingerprint, create_time, create_by, change_time, change_by'
|
|
. ' FROM smime_signer_cert_relations'
|
|
. ' WHERE id = ? ORDER BY create_time DESC',
|
|
Bind => [ \$Param{ID} ],
|
|
Limit => 1,
|
|
);
|
|
|
|
if ($Success) {
|
|
while ( my @ResultData = $DBObject->FetchrowArray() ) {
|
|
|
|
# format date
|
|
%Data = (
|
|
ID => $ResultData[0],
|
|
CertHash => $ResultData[1],
|
|
CertFingerprint => $ResultData[2],
|
|
CAHash => $ResultData[3],
|
|
CAFingerprint => $ResultData[4],
|
|
Changed => $ResultData[5],
|
|
ChangedBy => $ResultData[6],
|
|
Created => $ResultData[7],
|
|
CreatedBy => $ResultData[8],
|
|
);
|
|
}
|
|
return %Data || '';
|
|
}
|
|
else {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => 'DB error: not possible to get relation!',
|
|
Priority => 'error',
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
my $Success = $DBObject->Prepare(
|
|
SQL =>
|
|
'SELECT id, cert_hash, cert_fingerprint, ca_hash, ca_fingerprint, create_time, create_by, change_time, change_by'
|
|
. ' FROM smime_signer_cert_relations'
|
|
. ' WHERE cert_fingerprint = ? ORDER BY id DESC',
|
|
Bind => [ \$Param{CertFingerprint} ],
|
|
);
|
|
|
|
if ($Success) {
|
|
while ( my @ResultData = $DBObject->FetchrowArray() ) {
|
|
my %ResultData = (
|
|
ID => $ResultData[0],
|
|
CertHash => $ResultData[1],
|
|
CertFingerprint => $ResultData[2],
|
|
CAHash => $ResultData[3],
|
|
CAFingerprint => $ResultData[4],
|
|
Changed => $ResultData[5],
|
|
ChangedBy => $ResultData[6],
|
|
Created => $ResultData[7],
|
|
CreatedBy => $ResultData[8],
|
|
);
|
|
push @Data, \%ResultData;
|
|
}
|
|
return @Data;
|
|
}
|
|
else {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => 'DB error: not possible to get relations!',
|
|
Priority => 'error',
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
=head2 SignerCertRelationExists ()
|
|
|
|
returns the ID if the relation exists
|
|
|
|
my $Result = $CryptObject->SignerCertRelationExists(
|
|
CertFingerprint => $CertificateFingerprint,
|
|
CAFingerprint => $CAFingerprint,
|
|
);
|
|
|
|
my $Result = $CryptObject->SignerCertRelationExists(
|
|
ID => $RelationID,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub SignerCertRelationExists {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{ID} && !( $Param{CertFingerprint} && $Param{CAFingerprint} ) ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need ID or CertFingerprint & CAFingerprint!"
|
|
);
|
|
return;
|
|
}
|
|
|
|
# get database object
|
|
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
|
|
|
if ( $Param{CertFingerprint} && $Param{CAFingerprint} ) {
|
|
my $Data;
|
|
my $Success = $DBObject->Prepare(
|
|
SQL => 'SELECT id FROM smime_signer_cert_relations '
|
|
. 'WHERE cert_fingerprint = ? AND ca_fingerprint = ?',
|
|
Bind => [ \$Param{CertFingerprint}, \$Param{CAFingerprint} ],
|
|
Limit => 1,
|
|
);
|
|
|
|
if ($Success) {
|
|
while ( my @ResultData = $DBObject->FetchrowArray() ) {
|
|
$Data = $ResultData[0];
|
|
}
|
|
return $Data || '';
|
|
}
|
|
else {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => 'DB error: not possible to check relation!',
|
|
Priority => 'error',
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
elsif ( $Param{ID} ) {
|
|
my $Data;
|
|
my $Success = $DBObject->Prepare(
|
|
SQL => 'SELECT id FROM smime_signer_cert_relations '
|
|
. 'WHERE id = ?',
|
|
Bind => [ \$Param{ID}, ],
|
|
Limit => 1,
|
|
);
|
|
|
|
if ($Success) {
|
|
while ( my @ResultData = $DBObject->FetchrowArray() ) {
|
|
$Data = $ResultData[0];
|
|
}
|
|
return $Data || '';
|
|
}
|
|
else {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => 'DB error: not possible to check relation!',
|
|
Priority => 'error',
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
=head2 SignerCertRelationDelete ()
|
|
|
|
returns 1 if success
|
|
|
|
# delete all relations for a cert
|
|
my $Success = $CryptObject->SignerCertRelationDelete (
|
|
CertFingerprint => $CertFingerprint,
|
|
UserID => 1,
|
|
);
|
|
|
|
# delete one relation by ID
|
|
$Success = $CryptObject->SignerCertRelationDelete (
|
|
ID => '45',
|
|
);
|
|
|
|
# delete one relation by CertFingerprint & CAFingerprint
|
|
$Success = $CryptObject->SignerCertRelationDelete (
|
|
CertFingerprint => $CertFingerprint,
|
|
CAFingerprint => $CAFingerprint,
|
|
);
|
|
|
|
# delete one relation by CAFingerprint
|
|
$Success = $CryptObject->SignerCertRelationDelete (
|
|
CAFingerprint => $CAFingerprint,
|
|
);
|
|
|
|
=cut
|
|
|
|
sub SignerCertRelationDelete {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
if ( !$Param{CertFingerprint} && !$Param{ID} && !$Param{CAFingerprint} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => 'Need ID or CertFingerprint or CAFingerprint!'
|
|
);
|
|
return;
|
|
}
|
|
|
|
# get database object
|
|
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
|
|
|
if ( $Param{ID} ) {
|
|
|
|
# delete row
|
|
my $Success = $DBObject->Do(
|
|
SQL => 'DELETE FROM smime_signer_cert_relations '
|
|
. 'WHERE id = ?',
|
|
Bind => [ \$Param{ID} ],
|
|
);
|
|
|
|
if ( !$Success ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message => "DB Error, Not possible to delete relation ID:$Param{ID}!",
|
|
Priority => 'error',
|
|
);
|
|
}
|
|
return $Success;
|
|
}
|
|
elsif ( $Param{CertFingerprint} && $Param{CAFingerprint} ) {
|
|
|
|
# delete one row
|
|
my $Success = $DBObject->Do(
|
|
SQL => 'DELETE FROM smime_signer_cert_relations '
|
|
. 'WHERE cert_fingerprint = ? AND ca_fingerprint = ?',
|
|
Bind => [ \$Param{CertFingerprint}, \$Param{CAFingerprint} ],
|
|
);
|
|
|
|
if ( !$Success ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message =>
|
|
"DB Error, Not possible to delete relation for "
|
|
. "CertFingerprint:$Param{CertFingerprint} and CAFingerprint:$Param{CAFingerprint}!",
|
|
Priority => 'error',
|
|
);
|
|
}
|
|
return $Success;
|
|
}
|
|
elsif ( $Param{CAFingerprint} ) {
|
|
|
|
# delete one row
|
|
my $Success = $DBObject->Do(
|
|
SQL => 'DELETE FROM smime_signer_cert_relations '
|
|
. 'WHERE ca_fingerprint = ?',
|
|
Bind => [ \$Param{CAFingerprint} ],
|
|
);
|
|
|
|
if ( !$Success ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message =>
|
|
"DB Error, Not possible to delete relation for "
|
|
. "CAFingerprint:$Param{CAFingerprint}!",
|
|
Priority => 'error',
|
|
);
|
|
}
|
|
return $Success;
|
|
}
|
|
else {
|
|
|
|
# delete all rows
|
|
my $Success = $DBObject->Do(
|
|
SQL => 'DELETE FROM smime_signer_cert_relations '
|
|
. 'WHERE cert_fingerprint = ?',
|
|
Bind => [ \$Param{CertFingerprint} ],
|
|
);
|
|
|
|
if ( !$Success ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Message =>
|
|
"DB Error, Not possible to delete relations for CertFingerprint:$Param{CertFingerprint}!",
|
|
Priority => 'error',
|
|
);
|
|
}
|
|
return $Success;
|
|
}
|
|
return;
|
|
}
|
|
|
|
=head2 CheckCertPath()
|
|
|
|
Checks and fixes the private secret files that do not have an index. (Needed because this
|
|
changed during the migration from OTRS 3.0 to 3.1.)
|
|
|
|
Checks and fixed certificates, private keys and secrets files to have a correct name
|
|
depending on the current OpenSSL hash algorithm.
|
|
|
|
my $Result = $CryptObject->CheckCertPath ();
|
|
|
|
a result could be:
|
|
|
|
$Result = {
|
|
Success => 1 # or 0 if fails
|
|
Details => $Details # a readable string log of all activities and errors found
|
|
};
|
|
|
|
=cut
|
|
|
|
sub CheckCertPath {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# normalize private secret file names
|
|
#
|
|
# in otrs 3.0 private secret files are stored in format like 12345678.p, from otrs 3.1 this
|
|
# files must be in a format like 12345678.0.p where .0 could be from 0 to 9 depending on the
|
|
# private key file name.
|
|
|
|
my $NormalizeResult = $Self->_NormalizePrivateSecretFiles();
|
|
|
|
if ( !$NormalizeResult->{Success} ) {
|
|
return {
|
|
Success => 0,
|
|
Details => $NormalizeResult->{Details}
|
|
. "\n<red>Error in Normalize Private Secret Files.</red>\n\n",
|
|
ShortDetails => "<red>Error in Normalize Private Secret Files.</red>\n\n",
|
|
};
|
|
}
|
|
|
|
# re-calculate certificates hashes using current openssl
|
|
#
|
|
# from openssl 1.0.0 a new hash algorithm has been implemented, this new hash is not compatible
|
|
# with the old hash all stored certificates names must match current hash
|
|
# all affected certificates, private keys and private secrets has to be renamed
|
|
# all affected relations has to be updated
|
|
my $ReHashSuccess = $Self->_ReHashCertificates();
|
|
|
|
if ( !$ReHashSuccess->{Success} ) {
|
|
return {
|
|
Success => 0,
|
|
Details => $NormalizeResult->{Details} . $ReHashSuccess->{Details}
|
|
. "\n<red>Error in Re-Hash Certificate Files.</red>\n\n",
|
|
ShortDetails => "<red>Error in Re-Hash Certificate Files.</red>\n\n",
|
|
};
|
|
}
|
|
|
|
return {
|
|
Success => 1,
|
|
Details => $NormalizeResult->{Details} . $ReHashSuccess->{Details},
|
|
};
|
|
}
|
|
|
|
=begin Internal:
|
|
|
|
=cut
|
|
|
|
sub _Init {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# get config object
|
|
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
|
|
|
|
$Self->{Bin} = $ConfigObject->Get('SMIME::Bin') || '/usr/bin/openssl';
|
|
$Self->{CertPath} = $ConfigObject->Get('SMIME::CertPath');
|
|
$Self->{PrivatePath} = $ConfigObject->Get('SMIME::PrivatePath');
|
|
|
|
# get the cache TTL (in seconds)
|
|
$Self->{CacheTTL} = int( $ConfigObject->Get('SMIME::CacheTTL') || 86400 );
|
|
|
|
if ( $^O =~ m{mswin}i ) {
|
|
|
|
# take care to deal properly with paths containing whitespace
|
|
$Self->{Cmd} = qq{"$Self->{Bin}"};
|
|
}
|
|
else {
|
|
|
|
# make sure that we are getting POSIX (i.e. english) messages from openssl
|
|
$Self->{Cmd} = "LC_MESSAGES=POSIX $Self->{Bin}";
|
|
}
|
|
|
|
# ensure that there is a random state file that we can write to (otherwise openssl will bail)
|
|
$ENV{RANDFILE} = $ConfigObject->Get('TempDir') . '/.rnd'; ## no critic
|
|
|
|
# prepend RANDFILE declaration to openssl cmd
|
|
$Self->{Cmd} = "HOME=" . $ConfigObject->Get('Home') . " RANDFILE=$ENV{RANDFILE} $Self->{Cmd}";
|
|
|
|
# get the openssl version string, e.g. OpenSSL 0.9.8e 23 Feb 2007
|
|
$Self->{OpenSSLVersionString} = qx{$Self->{Cmd} version};
|
|
|
|
# get the openssl major version, e.g. 1 for version 1.0.0
|
|
if ( $Self->{OpenSSLVersionString} =~ m{ \A (?: (?: Open|Libre)SSL )? \s* ( \d ) }xmsi ) {
|
|
$Self->{OpenSSLMajorVersion} = $1;
|
|
}
|
|
|
|
return $Self;
|
|
}
|
|
|
|
sub _FetchAttributesFromCert {
|
|
my ( $Self, $Filename, $AttributesRef ) = @_;
|
|
|
|
# The hash algorithm used in the -subject_hash and -issuer_hash options before OpenSSL 1.0.0
|
|
# was based on the deprecated MD5 algorithm and the encoding of the distinguished name.
|
|
# In OpenSSL 1.0.0 and later it is based on a canonical version of the DN using SHA1.
|
|
#
|
|
# The older algorithm can be used with -subject_hash_old attribute, but doing this will might
|
|
# cause for openssl 1.0.0 that the -CApath option (e.g. in verify function) will not find the
|
|
# CA files in the path, due that openssl search for the file names based in current algorithm
|
|
#
|
|
# -subject_hash_old was used in otrs in the past (to keep the old hashes style, and perhaps to
|
|
# ease a migration between openssl versions ) but now is not recommended anymore.
|
|
|
|
# testing new solution
|
|
my $OptionString = ' '
|
|
. '-subject_hash '
|
|
. '-issuer '
|
|
. '-fingerprint -sha1 '
|
|
. '-serial '
|
|
. '-subject '
|
|
. '-startdate '
|
|
. '-enddate '
|
|
. '-email '
|
|
. '-modulus '
|
|
. ' ';
|
|
|
|
# call all attributes at same time
|
|
my $Options = "x509 -in $Filename -noout $OptionString";
|
|
|
|
# get the output string
|
|
my $Output = qx{$Self->{Cmd} $Options 2>&1};
|
|
|
|
# filters
|
|
my %Filters = (
|
|
Hash => '(\w{8})',
|
|
Issuer => 'issuer=\s*(.*)',
|
|
Fingerprint => 'SHA1\sFingerprint=(.*)',
|
|
Serial => 'serial=(.*)',
|
|
Subject => 'subject=[ ]*(?:\/)?(.+?)',
|
|
StartDate => 'notBefore=(.*)',
|
|
EndDate => 'notAfter=(.*)',
|
|
Email => '([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4})',
|
|
Modulus => 'Modulus=(.*)',
|
|
);
|
|
|
|
# parse output string
|
|
my @Attributes = split( /\n/, $Output );
|
|
for my $Line (@Attributes) {
|
|
|
|
# clean end spaces
|
|
$Line =~ tr{\r\n}{}d;
|
|
|
|
# look for every attribute by filter
|
|
FILTER:
|
|
for my $Filter ( sort keys %Filters ) {
|
|
next FILTER if $Line !~ m{ \A $Filters{$Filter} \z }xms;
|
|
my $Match = $1 || '';
|
|
|
|
# email filter is allowed to match multiple times for alternate names (SubjectAltName)
|
|
if ( $Filter eq 'Email' ) {
|
|
push @{ $AttributesRef->{$Filter} }, $Match;
|
|
}
|
|
|
|
# all other filters are one-time matches, so we exclude the filter from all remaining lines (performance)
|
|
else {
|
|
$AttributesRef->{$Filter} = $Match;
|
|
delete $Filters{$Filter};
|
|
}
|
|
|
|
last FILTER;
|
|
}
|
|
}
|
|
|
|
# prepare attributes data for use
|
|
if ( ref $AttributesRef->{Email} eq 'ARRAY' ) {
|
|
$AttributesRef->{Email} = join ', ', sort @{ $AttributesRef->{Email} };
|
|
}
|
|
if ( $AttributesRef->{Issuer} ) {
|
|
$AttributesRef->{Issuer} =~ s{=}{= }xmsg;
|
|
}
|
|
if ( $AttributesRef->{Subject} ) {
|
|
$AttributesRef->{Subject} =~ s{\/}{ }xmsg;
|
|
$AttributesRef->{Subject} =~ s{=}{= }xmsg;
|
|
}
|
|
|
|
my %Month = (
|
|
Jan => '01',
|
|
Feb => '02',
|
|
Mar => '03',
|
|
Apr => '04',
|
|
May => '05',
|
|
Jun => '06',
|
|
Jul => '07',
|
|
Aug => '08',
|
|
Sep => '09',
|
|
Oct => '10',
|
|
Nov => '11',
|
|
Dec => '12',
|
|
);
|
|
|
|
for my $DateType ( 'StartDate', 'EndDate' ) {
|
|
if (
|
|
$AttributesRef->{$DateType}
|
|
&&
|
|
$AttributesRef->{$DateType} =~ /(.+?)\s(.+?)\s(\d\d:\d\d:\d\d)\s(\d\d\d\d)/
|
|
)
|
|
{
|
|
my $Day = int($2);
|
|
my $Month = '';
|
|
my $Year = $4;
|
|
|
|
if ( $Day < 10 ) {
|
|
$Day = "0$Day";
|
|
}
|
|
|
|
MONTH_KEY:
|
|
for my $MonthKey ( sort keys %Month ) {
|
|
if ( $AttributesRef->{$DateType} =~ /$MonthKey/i ) {
|
|
$Month = $Month{$MonthKey};
|
|
last MONTH_KEY;
|
|
}
|
|
}
|
|
$AttributesRef->{"Short$DateType"} = "$Year-$Month-$Day";
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub _CleanOutput {
|
|
my ( $Self, $Output ) = @_;
|
|
|
|
# remove spurious warnings that appear on Windows
|
|
if ( $^O =~ m{mswin}i ) {
|
|
$Output =~ s{Loading 'screen' into random state - done\r?\n}{}igms;
|
|
}
|
|
|
|
return $Output;
|
|
}
|
|
|
|
sub _CertificateFilename {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
for my $Needed (qw(Fingerprint Hash)) {
|
|
if ( !$Param{$Needed} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $Needed!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
# get all certificates with hash name
|
|
my @CertList = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead(
|
|
Directory => $Self->{CertPath},
|
|
Filter => "$Param{Hash}.*",
|
|
);
|
|
|
|
# open every file, get attributes and compare fingerprint
|
|
for my $CertFile (@CertList) {
|
|
my %Attributes;
|
|
$Self->_FetchAttributesFromCert( $CertFile, \%Attributes );
|
|
|
|
# exit and return on first finger print found
|
|
if ( $Attributes{Fingerprint} && $Attributes{Fingerprint} eq $Param{Fingerprint} ) {
|
|
$CertFile =~ s{^.*/}{}xms;
|
|
return $CertFile;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _PrivateFilename {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# check needed stuff
|
|
for my $Needed (qw(Hash Modulus)) {
|
|
if ( !$Param{$Needed} ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Need $Needed!"
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
# get main object
|
|
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
|
|
|
|
# get all certificates with hash name
|
|
my @CertList = $MainObject->DirectoryRead(
|
|
Directory => $Self->{PrivatePath},
|
|
Filter => $Param{Hash} . '\.*',
|
|
);
|
|
|
|
# open every file, get attributes and compare modulus
|
|
CERTFILE:
|
|
for my $CertFile (@CertList) {
|
|
my %Attributes;
|
|
next CERTFILE if $CertFile =~ m{\.P}xms;
|
|
|
|
# remove the path and get only the filename (for cache)
|
|
my $CertFilename = $CertFile;
|
|
$CertFilename =~ s{^.*/}{}xms;
|
|
|
|
# open secret
|
|
my $Private = $MainObject->FileRead(
|
|
Location => $CertFile,
|
|
);
|
|
my $Secret = $MainObject->FileRead(
|
|
Location => $CertFile . '.P',
|
|
);
|
|
|
|
%Attributes = $Self->PrivateAttributes(
|
|
Private => $$Private,
|
|
Secret => $$Secret,
|
|
Filename => $CertFilename,
|
|
);
|
|
|
|
# exit and return on first modulus found
|
|
if ( $Attributes{Modulus} && $Attributes{Modulus} eq $Param{Modulus} ) {
|
|
return $CertFilename;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub _NormalizePrivateSecretFiles {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# get main object
|
|
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
|
|
|
|
# get all files that ends with .P from the private directory
|
|
my @List = $MainObject->DirectoryRead(
|
|
Directory => "$Self->{PrivatePath}",
|
|
Filter => '*.P',
|
|
);
|
|
|
|
my $Details = "<yellow>Normalizing private secret files...</yellow>\n"
|
|
. " - Private path: $Self->{PrivatePath}\n\n";
|
|
|
|
# stop if there are no private secrets stored
|
|
if ( scalar @List == 0 ) {
|
|
$Details .= " No private secret files found, nothing to do!... <green>OK</green>\n";
|
|
|
|
return {
|
|
Success => 1,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
my @WrongPrivateSecretList;
|
|
|
|
# exclude the private secret files that has a correct name format
|
|
FILENAME:
|
|
for my $File (@List) {
|
|
$File =~ s{^.*/}{}xms;
|
|
next FILENAME if ( $File =~ m{.+ \. \d \. P}smxi );
|
|
push @WrongPrivateSecretList, $File;
|
|
}
|
|
|
|
# stop if the are no wrong files to normalize
|
|
if ( scalar @WrongPrivateSecretList == 0 ) {
|
|
$Details .= " Stored private secrets found, but they are all correct, nothing to do... <green>OK</green>\n";
|
|
|
|
return {
|
|
Success => 1,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
# check if the file with the correct name already exist in the system
|
|
FILENAME:
|
|
for my $File (@WrongPrivateSecretList) {
|
|
|
|
# build the correct file name
|
|
$File =~ m{(.+) \. P}smxi;
|
|
my $Hash = $1;
|
|
|
|
my $CorrectFile;
|
|
my @UsedPrivateSecretFiles;
|
|
|
|
KEYFILENAME:
|
|
for my $Count ( 0 .. 99 ) {
|
|
my $PrivateKeyFileLocation = "$Self->{PrivatePath}/$Hash.$Count";
|
|
|
|
# get private keys
|
|
if ( -e $PrivateKeyFileLocation ) {
|
|
my $PrivateSecretFileLocation = $PrivateKeyFileLocation . '.P';
|
|
|
|
# check if private secret already exists
|
|
if ( !-e $PrivateSecretFileLocation ) {
|
|
|
|
# use first available
|
|
$CorrectFile = "$Hash.$Count.P";
|
|
last KEYFILENAME;
|
|
}
|
|
else {
|
|
push @UsedPrivateSecretFiles, "$Hash.$Count.P";
|
|
next KEYFILENAME;
|
|
}
|
|
}
|
|
}
|
|
|
|
# if there are no keys for the private secret, the file could not be renamed
|
|
if ( !$CorrectFile && scalar @UsedPrivateSecretFiles == 0 ) {
|
|
$Details .= " Can't rename private secret file $File, because there is no"
|
|
. " private key file for this private secret... <red>Warning</red>\n";
|
|
next FILENAME;
|
|
}
|
|
|
|
my $WrongFileLocation = "$Self->{PrivatePath}/$File";
|
|
|
|
# if an available file name was found
|
|
if ($CorrectFile) {
|
|
my $CorrectFileLocation = "$Self->{PrivatePath}/$CorrectFile";
|
|
if ( !rename $WrongFileLocation, $CorrectFileLocation ) {
|
|
my $Message = "Could not rename private secret file $WrongFileLocation to $CorrectFileLocation!";
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => $Message,
|
|
);
|
|
|
|
$Details .= " $Message\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
$Details .= " Renamed private secret file $File to $CorrectFile ... <green>OK</green>\n";
|
|
next FILENAME;
|
|
}
|
|
|
|
# otherwise try to find if any of the used files has the same content
|
|
$Details .= " Can't rename private secret file: $File\nAll private key files for hash"
|
|
. " $Hash has already a correct private secret filename associated!\n";
|
|
|
|
# get the contents of the wrong private secret file
|
|
my $WrongFileContent = $MainObject->FileRead(
|
|
Location => $WrongFileLocation,
|
|
Result => 'SCALAR',
|
|
);
|
|
|
|
# loop over the found private secret files for the same private key hash
|
|
for my $PrivateSecretFile (@UsedPrivateSecretFiles) {
|
|
my $PrivateSecretFileLocation = "$Self->{PrivatePath}/$PrivateSecretFile";
|
|
|
|
# check if the file contents are the same
|
|
my $PrivateSecretFileContent = $MainObject->FileRead(
|
|
Location => $PrivateSecretFileLocation,
|
|
Result => 'SCALAR',
|
|
);
|
|
|
|
# safe to delete wrong file if contents are are identical
|
|
if ( ${$WrongFileContent} eq ${$PrivateSecretFileContent} ) {
|
|
|
|
$Details
|
|
.= " The content of files $File and $PrivateSecretFile is the same, it is safe to remove $File\n";
|
|
|
|
$Details .= " Remove private secret file $WrongFileLocation from the file system...";
|
|
|
|
# remove file
|
|
my $Success = $MainObject->FileDelete(
|
|
Location => $WrongFileLocation,
|
|
);
|
|
|
|
# return error if file was not deleted
|
|
if ( !$Success ) {
|
|
my $Message = "Could not remove private secret file $WrongFileLocation from the file system!";
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => $Message,
|
|
);
|
|
|
|
$Details .= " <red>Failed</red>\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
# continue to next wrong private secret file
|
|
$Details .= " <green>OK</green>\n";
|
|
|
|
next FILENAME;
|
|
}
|
|
|
|
# otherwise just log that the contents are different, do not delete file
|
|
$Details .= " The content of files $File and $PrivateSecretFile is different\n";
|
|
}
|
|
|
|
# all private secret files has different content, just log this as a waring and continue to
|
|
# the next wrong private secret file
|
|
$Details .= " The private secret file $File has information not stored in any other"
|
|
. " private secret file for hash $Hash\n"
|
|
. " The file will not be deleted... <red>Warning</red>\n";
|
|
next FILENAME;
|
|
}
|
|
|
|
return {
|
|
Success => 1,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
sub _ReHashCertificates {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
# get the list of certificates
|
|
my @CertList = $Self->CertificateList();
|
|
|
|
my $Details = "\n<yellow>Re-Hashing Certificates...</yellow>\n"
|
|
. " - Certificate path: $Self->{CertPath}\n"
|
|
. " - Private path: $Self->{PrivatePath}\n\n";
|
|
|
|
if ( scalar @CertList == 0 ) {
|
|
$Details .= " No certificate files found, nothing to do... <green>OK</green>\n\n";
|
|
|
|
return {
|
|
Success => 1,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
my @WrongCertificatesList;
|
|
|
|
# exclude the certificate files with correct file name
|
|
FILENAME:
|
|
for my $File (@CertList) {
|
|
$File =~ s{^.*/}{}xms;
|
|
|
|
# get certificate attributes with current OpenSSL version
|
|
my $Certificate = $Self->CertificateGet(
|
|
Filename => $File,
|
|
);
|
|
my %CertificateAttributes = $Self->CertificateAttributes(
|
|
Certificate => $Certificate,
|
|
Filename => $File,
|
|
);
|
|
|
|
# split filename into Hash.Index (12345678.0 -> 12345678 / 0)
|
|
$File =~ m{ (.+) \. (\d+) }smx;
|
|
my $Hash = $1;
|
|
my $Index = $2;
|
|
|
|
# get new hash from certificate attributes
|
|
my $NewHash = $CertificateAttributes{Hash};
|
|
my $Fingerprint = $CertificateAttributes{Fingerprint};
|
|
|
|
next FILENAME if $Hash eq $NewHash;
|
|
|
|
push @WrongCertificatesList, {
|
|
Hash => $Hash,
|
|
NewHash => $NewHash,
|
|
Index => $Index,
|
|
Fingerprint => $Fingerprint,
|
|
};
|
|
}
|
|
|
|
# stop if the are no wrong files to re-hash
|
|
if ( scalar @WrongCertificatesList == 0 ) {
|
|
$Details .= " Stored certificates found, but they are all correct, nothing to do... <green>OK</green>\n";
|
|
|
|
return {
|
|
Success => 1,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
# get database object
|
|
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
|
|
|
|
# loop over wrong certificates
|
|
CERTIFICATE:
|
|
for my $WrongCertificate (@WrongCertificatesList) {
|
|
|
|
# recreate the certificate file name
|
|
my $WrongCertificateFile = "$Self->{CertPath}/$WrongCertificate->{Hash}.$WrongCertificate->{Index}";
|
|
|
|
# check if certificate exists
|
|
if ( !-e $WrongCertificateFile ) {
|
|
my $Message = "SMIME certificate $WrongCertificateFile file does not exist!";
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "$Message",
|
|
);
|
|
|
|
$Details .= " $Message\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
# look for an available new filename
|
|
my $NewCertificateFile;
|
|
my $NewPrivateKeyFile;
|
|
my $NewIndex;
|
|
FILENAME:
|
|
for my $Count ( 0 .. 99 ) {
|
|
my $CertTestFile = "$Self->{CertPath}/$WrongCertificate->{NewHash}.$Count";
|
|
if ( -e $CertTestFile ) {
|
|
next FILENAME;
|
|
}
|
|
$NewCertificateFile = $CertTestFile;
|
|
$NewPrivateKeyFile = "$Self->{PrivatePath}/$WrongCertificate->{NewHash}.$Count";
|
|
$NewIndex = $Count;
|
|
last FILENAME;
|
|
}
|
|
|
|
if ( !$NewCertificateFile ) {
|
|
my $Message = "No more available filenames for certificate hash: $WrongCertificate->{NewHash}!";
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => $Message,
|
|
);
|
|
|
|
$Details .= " $Message\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
|
|
}
|
|
|
|
# set wrong private key
|
|
my $WrongPrivateKeyFile = "$Self->{PrivatePath}/$WrongCertificate->{Hash}.$WrongCertificate->{Index}";
|
|
|
|
# check if certificate has a private key and secret
|
|
# if has a private key it must have a private secret
|
|
my $HasPrivateKey;
|
|
my $HasPrivateSecret;
|
|
if ( -e $WrongPrivateKeyFile ) {
|
|
$HasPrivateKey = 1;
|
|
|
|
# check new private key and secret files
|
|
if ( -e $NewPrivateKeyFile ) {
|
|
my $Message = "Filename for private key: $NewPrivateKeyFile is already in use!";
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => $Message,
|
|
);
|
|
|
|
$Details .= " $Message\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
# check private secret
|
|
if ( -e "$WrongPrivateKeyFile.P" ) {
|
|
$HasPrivateSecret = 1;
|
|
|
|
if ( -e "$NewPrivateKeyFile.P" ) {
|
|
my $Message = "Filename for private secret: $NewPrivateKeyFile.P is already in use!";
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => $Message,
|
|
);
|
|
|
|
$Details .= " $Message\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
# rename certificate
|
|
$Details .= " Rename certificate $WrongCertificate->{Hash}.$WrongCertificate->{Index}"
|
|
. " to $WrongCertificate->{NewHash}.$NewIndex...";
|
|
|
|
if ( !rename $WrongCertificateFile, $NewCertificateFile ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Could not rename SMIME certificate file $WrongCertificateFile to $NewCertificateFile!",
|
|
);
|
|
|
|
$Details .= " <red>Failed</red>\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
$Details .= " <green>OK</green>\n";
|
|
|
|
# update certificate relations
|
|
# get relations that have this certificate
|
|
my $DBSuccess = $DBObject->Prepare(
|
|
SQL =>
|
|
'SELECT id, cert_hash, cert_fingerprint, ca_hash, ca_fingerprint'
|
|
. ' FROM smime_signer_cert_relations'
|
|
. ' WHERE cert_hash = ? AND cert_fingerprint =?',
|
|
Bind => [ \$WrongCertificate->{Hash}, \$WrongCertificate->{Fingerprint} ],
|
|
);
|
|
|
|
my @WrongCertRelations;
|
|
|
|
if ($DBSuccess) {
|
|
while ( my @ResultData = $DBObject->FetchrowArray() ) {
|
|
|
|
# format date
|
|
my %Data = (
|
|
ID => $ResultData[0],
|
|
CertHash => $ResultData[1],
|
|
CertFingerprint => $ResultData[2],
|
|
CAHash => $ResultData[3],
|
|
CAFingerprint => $ResultData[4],
|
|
);
|
|
push @WrongCertRelations, \%Data;
|
|
}
|
|
}
|
|
|
|
$Details .= " Get certificate DB relations for $WrongCertificate->{Hash}."
|
|
. "$WrongCertificate->{Index} as certificate\n";
|
|
|
|
# update relations
|
|
if ( scalar @WrongCertRelations > 0 ) {
|
|
for my $WrongRelation (@WrongCertRelations) {
|
|
|
|
my $Success = $DBObject->Do(
|
|
SQL =>
|
|
'UPDATE smime_signer_cert_relations'
|
|
. ' SET cert_hash = ?'
|
|
. ' WHERE id = ? AND cert_fingerprint = ?',
|
|
Bind => [
|
|
\$WrongCertificate->{NewHash},
|
|
\$WrongRelation->{ID}, \$WrongCertificate->{Fingerprint}
|
|
],
|
|
);
|
|
|
|
$Details .= " Updated relation ID: $WrongRelation->{ID} with CA $WrongRelation->{CAHash}...";
|
|
|
|
if ($Success) {
|
|
$Details .= " <green>OK</green>\n";
|
|
}
|
|
else {
|
|
$Details .= " <red>Failed</red>\n";
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Details .= " No wrong relations found, nothing to do... <green>OK</green>\n";
|
|
}
|
|
|
|
# get relations that have this certificate as a CA
|
|
$DBSuccess = $DBObject->Prepare(
|
|
SQL =>
|
|
'SELECT id, cert_hash, cert_fingerprint, ca_hash, ca_fingerprint'
|
|
. ' FROM smime_signer_cert_relations'
|
|
. ' WHERE ca_hash = ? AND ca_fingerprint =?',
|
|
Bind => [ \$WrongCertificate->{Hash}, \$WrongCertificate->{Fingerprint} ],
|
|
);
|
|
|
|
my @WrongCARelations;
|
|
|
|
if ($DBSuccess) {
|
|
while ( my @ResultData = $DBObject->FetchrowArray() ) {
|
|
|
|
# format date
|
|
my %Data = (
|
|
ID => $ResultData[0],
|
|
CertHash => $ResultData[1],
|
|
CertFingerprint => $ResultData[2],
|
|
CAHash => $ResultData[3],
|
|
CAFingerprint => $ResultData[4],
|
|
);
|
|
push @WrongCARelations, \%Data;
|
|
}
|
|
}
|
|
|
|
$Details .= " Get certificate DB relations for $WrongCertificate->{Hash}.$WrongCertificate->{Index} as CA\n";
|
|
|
|
# update relations (CA)
|
|
if ( scalar @WrongCertRelations > 0 ) {
|
|
for my $WrongRelation (@WrongCARelations) {
|
|
|
|
my $Success = $DBObject->Do(
|
|
SQL =>
|
|
'UPDATE smime_signer_cert_relations'
|
|
. ' SET ca_hash = ?'
|
|
. ' WHERE id = ? AND ca_fingerprint = ?',
|
|
Bind => [
|
|
\$WrongCertificate->{NewHash},
|
|
\$WrongRelation->{ID}, \$WrongCertificate->{Fingerprint}
|
|
],
|
|
);
|
|
|
|
$Details
|
|
.= " Updated relation ID: $WrongRelation->{ID} with certificate $WrongRelation->{CertHash}...";
|
|
|
|
if ($Success) {
|
|
$Details .= " <green>OK</green>\n";
|
|
}
|
|
else {
|
|
$Details .= " <red>Failed</red>\n";
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$Details .= " No wrong relations found, nothing to do... <green>OK</green>\n";
|
|
}
|
|
|
|
if ($HasPrivateKey) {
|
|
|
|
# rename private key
|
|
$Details .= " Rename private key $WrongCertificate->{Hash}.$WrongCertificate->{Index} to"
|
|
. " $WrongCertificate->{NewHash}.$NewIndex...";
|
|
|
|
if ( !rename $WrongPrivateKeyFile, $NewPrivateKeyFile ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Could not rename SMIME private key file $WrongPrivateKeyFile to $NewPrivateKeyFile!",
|
|
);
|
|
|
|
$Details .= " <red>Failed</red>\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
$Details .= " <green>OK</green>\n";
|
|
|
|
# rename private secret
|
|
if ($HasPrivateSecret) {
|
|
|
|
$Details .= " Rename private secret $WrongCertificate->{Hash}.$WrongCertificate->{Index}.P to"
|
|
. " $WrongCertificate->{NewHash}.$NewIndex.P...";
|
|
|
|
if ( !rename $WrongPrivateKeyFile . '.P', $NewPrivateKeyFile . '.P' ) {
|
|
$Kernel::OM->Get('Kernel::System::Log')->Log(
|
|
Priority => 'error',
|
|
Message => "Could not rename SMIME private secret file"
|
|
. " $WrongPrivateKeyFile.P to $NewPrivateKeyFile.P!",
|
|
);
|
|
|
|
$Details .= " <red>Failed</red>\n";
|
|
|
|
return {
|
|
Success => 0,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
$Details .= " <green>OK</green>\n";
|
|
}
|
|
else {
|
|
$Details .= " Private key $WrongCertificate->{Hash}.$WrongCertificate->{Index} found,"
|
|
. " but private secret: $WrongCertificate->{Hash}.$WrongCertificate->{Index}.P"
|
|
. " is missing... <red>Warning</red>\n";
|
|
}
|
|
}
|
|
else {
|
|
$Details .= " No Private key found for certificate $WrongCertificate->{Hash}."
|
|
. "$WrongCertificate->{Index}... <green>OK</green>\n";
|
|
}
|
|
}
|
|
return {
|
|
Success => 1,
|
|
Details => $Details,
|
|
};
|
|
}
|
|
|
|
1;
|
|
|
|
=end Internal:
|
|
|
|
=cut
|
|
|
|
=head1 TERMS AND CONDITIONS
|
|
|
|
This software is part of the OTRS project (L<https://otrs.org/>).
|
|
|
|
This software comes with ABSOLUTELY NO WARRANTY. For details, see
|
|
the enclosed file COPYING for license information (GPL). If you
|
|
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
|
|
|
|
=cut
|