diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:31:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:31:28 +0000 |
commit | 067008c5f094ba9606daacbe540f6b929dc124ea (patch) | |
tree | 3092ce2cd8bf1ac6db6c97f4c98c7f71a51c6ac8 /library/X509/Model | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-x509-upstream.tar.xz icingaweb2-module-x509-upstream.zip |
Adding upstream version 1:1.3.2.upstream/1%1.3.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/X509/Model')
-rw-r--r-- | library/X509/Model/Behavior/DERBase64.php | 44 | ||||
-rw-r--r-- | library/X509/Model/Behavior/ExpressionInjector.php | 62 | ||||
-rw-r--r-- | library/X509/Model/Behavior/Ip.php | 39 | ||||
-rw-r--r-- | library/X509/Model/Schema.php | 49 | ||||
-rw-r--r-- | library/X509/Model/X509Certificate.php | 159 | ||||
-rw-r--r-- | library/X509/Model/X509CertificateChain.php | 58 | ||||
-rw-r--r-- | library/X509/Model/X509CertificateChainLink.php | 46 | ||||
-rw-r--r-- | library/X509/Model/X509CertificateSubjectAltName.php | 50 | ||||
-rw-r--r-- | library/X509/Model/X509Dn.php | 51 | ||||
-rw-r--r-- | library/X509/Model/X509Job.php | 73 | ||||
-rw-r--r-- | library/X509/Model/X509JobRun.php | 77 | ||||
-rw-r--r-- | library/X509/Model/X509Schedule.php | 70 | ||||
-rw-r--r-- | library/X509/Model/X509Target.php | 74 |
13 files changed, 852 insertions, 0 deletions
diff --git a/library/X509/Model/Behavior/DERBase64.php b/library/X509/Model/Behavior/DERBase64.php new file mode 100644 index 0000000..f7b7215 --- /dev/null +++ b/library/X509/Model/Behavior/DERBase64.php @@ -0,0 +1,44 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model\Behavior; + +use ipl\Orm\Contract\PropertyBehavior; + +/** + * Support automatically transformation of DER-encoded certificates to PEM and vice versa. + */ +class DERBase64 extends PropertyBehavior +{ + public function fromDb($value, $key, $_) + { + if (! $value) { + return null; + } + + $block = chunk_split(base64_encode($value), 64, "\n"); + + return "-----BEGIN CERTIFICATE-----\n{$block}-----END CERTIFICATE-----"; + } + + public function toDb($value, $key, $_) + { + if (! $value) { + return null; + } + + $lines = explode("\n", $value); + $der = ''; + + foreach ($lines as $line) { + if (strpos($line, '-----') === 0) { + continue; + } + + $der .= base64_decode($line); + } + + return $der; + } +} diff --git a/library/X509/Model/Behavior/ExpressionInjector.php b/library/X509/Model/Behavior/ExpressionInjector.php new file mode 100644 index 0000000..c3fa2cb --- /dev/null +++ b/library/X509/Model/Behavior/ExpressionInjector.php @@ -0,0 +1,62 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model\Behavior; + +use ipl\Orm\Contract\QueryAwareBehavior; +use ipl\Orm\Contract\RewriteFilterBehavior; +use ipl\Orm\Query; +use ipl\Sql\ExpressionInterface; +use ipl\Stdlib\Filter; + +/** + * Support expression columns (which don't really exist in the database, but rather + * resulted e.g. from a `case..when` expression), being used as filter columns + */ +class ExpressionInjector implements RewriteFilterBehavior, QueryAwareBehavior +{ + /** @var array */ + protected $columns; + + /** @var Query */ + protected $query; + + public function __construct(...$columns) + { + $this->columns = $columns; + } + + public function setQuery(Query $query) + { + $this->query = $query; + + return $this; + } + + public function rewriteCondition(Filter\Condition $condition, $relation = null) + { + $columnName = $condition->metaData()->get('columnName'); + if (in_array($columnName, $this->columns, true)) { + $relationPath = $condition->metaData()->get('relationPath'); + if ($relationPath && $relationPath !== $this->query->getModel()->getTableAlias()) { + $subject = $this->query->getResolver()->resolveRelation($relationPath)->getTarget(); + } else { + $subject = $this->query->getModel(); + } + + /** @var ExpressionInterface $column */ + $column = $subject->getColumns()[$columnName]; + $expression = clone $column; + $expression->setColumns($this->query->getResolver()->qualifyColumns( + $this->query->getResolver()->requireAndResolveColumns( + $expression->getColumns(), + $subject + ), + $subject + )); + + $condition->setColumn($this->query->getDb()->getQueryBuilder()->buildExpression($expression)); + } + } +} diff --git a/library/X509/Model/Behavior/Ip.php b/library/X509/Model/Behavior/Ip.php new file mode 100644 index 0000000..79c9e80 --- /dev/null +++ b/library/X509/Model/Behavior/Ip.php @@ -0,0 +1,39 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model\Behavior; + +use ipl\Orm\Behavior\Binary; +use ipl\Orm\Contract\PropertyBehavior; + +/** + * Support automatically transformation of human-readable IP addresses into their respective packed + * binary representation and vice versa. + */ +class Ip extends Binary +{ + public function fromDb($value, $key, $_) + { + $value = parent::fromDb($value, $key, $_); + if ($value === null) { + return null; + } + + $ipv4 = ltrim($value, "\0"); + if (strlen($ipv4) === 4) { + $value = $ipv4; + } + + return inet_ntop($value); + } + + public function toDb($value, $key, $_) + { + if ($value === null || $value === '*' || ! ctype_print($value)) { + return $value; + } + + return parent::toDb(str_pad(inet_pton($value), 16, "\0", STR_PAD_LEFT), $key, $_); + } +} diff --git a/library/X509/Model/Schema.php b/library/X509/Model/Schema.php new file mode 100644 index 0000000..02ec0c0 --- /dev/null +++ b/library/X509/Model/Schema.php @@ -0,0 +1,49 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use DateTime; +use ipl\Orm\Behavior\BoolCast; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; + +/** + * A database model for x509 schema version table + * + * @property int $id Unique identifier of the database schema entries + * @property string $version The current schema version of Icinga Web + * @property DateTime $timestamp The insert/modify time of the schema entry + * @property bool $success Whether the database migration of the current version was successful + * @property ?string $reason The reason why the database migration has failed + */ +class Schema extends Model +{ + public function getTableName(): string + { + return 'x509_schema'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns(): array + { + return [ + 'version', + 'timestamp', + 'success', + 'reason' + ]; + } + + public function createBehaviors(Behaviors $behaviors): void + { + $behaviors->add(new BoolCast(['success'])); + $behaviors->add(new MillisecondTimestamp(['timestamp'])); + } +} diff --git a/library/X509/Model/X509Certificate.php b/library/X509/Model/X509Certificate.php new file mode 100644 index 0000000..63bdf95 --- /dev/null +++ b/library/X509/Model/X509Certificate.php @@ -0,0 +1,159 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use Icinga\Module\X509\Model\Behavior\DERBase64; +use Icinga\Module\X509\Model\Behavior\ExpressionInjector; +use ipl\Orm\Behavior\Binary; +use ipl\Orm\Behavior\BoolCast; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; +use ipl\Sql\Expression; + +class X509Certificate extends Model +{ + public function getTableName() + { + return 'x509_certificate'; + } + + public function getTableAlias(): string + { + return 'certificate'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'subject', + 'subject_hash', + 'issuer', + 'issuer_hash', + 'issuer_certificate_id', + 'version', + 'self_signed', + 'ca', + 'trusted', + 'pubkey_algo', + 'pubkey_bits', + 'signature_algo', + 'signature_hash_algo', + 'valid_from', + 'valid_to', + 'fingerprint', + 'serial', + 'certificate', + 'ctime', + 'mtime', + 'duration' => new Expression('%s - %s', ['valid_to', 'valid_from']) + ]; + } + + public function getColumnDefinitions() + { + return [ + 'subject' => t('Certificate'), + 'issuer' => t('Issuer'), + 'version' => t('Version'), + 'self_signed' => t('Is Self-Signed'), + 'ca' => t('Is Certificate Authority'), + 'trusted' => t('Is Trusted'), + 'pubkey_algo' => t('Public Key Algorithm'), + 'pubkey_bits' => t('Public Key Strength'), + 'signature_algo' => t('Signature Algorithm'), + 'signature_hash_algo' => t('Signature Hash Algorithm'), + 'valid_from' => t('Valid From'), + 'valid_to' => t('Valid To'), + 'duration' => t('Duration'), + 'subject_hash' => t('Subject Hash'), + 'issuer_hash' => t('Issuer Hash'), + ]; + } + + public function getSearchColumns() + { + return ['subject', 'issuer']; + } + + /** + * Get list of allowed columns to be exported + * + * @return string[] + */ + public function getExportableColumns(): array + { + return [ + 'id', + 'subject', + 'issuer', + 'version', + 'self_signed', + 'ca', + 'trusted', + 'pubkey_algo', + 'pubkey_bits', + 'signature_algo', + 'signature_hash_algo', + 'valid_from', + 'valid_to' + ]; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->add(new Binary([ + 'subject_hash', + 'issuer_hash', + 'fingerprint', + 'serial', + 'certificate' + ])); + + $behaviors->add(new DERBase64(['certificate'])); + + $behaviors->add(new BoolCast([ + 'ca', + 'trusted', + 'self_signed' + ])); + + $behaviors->add(new MillisecondTimestamp([ + 'valid_from', + 'valid_to', + 'ctime', + 'mtime', + 'duration' + ])); + + $behaviors->add(new ExpressionInjector('duration')); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('issuer_certificate', static::class) + ->setForeignKey('subject_hash') + ->setCandidateKey('issuer_hash'); + $relations->belongsToMany('chain', X509CertificateChain::class) + ->through(X509CertificateChainLink::class) + ->setForeignKey('certificate_id'); + + $relations->hasMany('certificate', static::class) + ->setForeignKey('issuer_hash') + ->setCandidateKey('subject_hash'); + $relations->hasMany('alt_name', X509CertificateSubjectAltName::class) + ->setJoinType('LEFT'); + $relations->hasMany('dn', X509Dn::class) + ->setForeignKey('hash') + ->setCandidateKey('subject_hash') + ->setJoinType('LEFT'); + } +} diff --git a/library/X509/Model/X509CertificateChain.php b/library/X509/Model/X509CertificateChain.php new file mode 100644 index 0000000..189c38d --- /dev/null +++ b/library/X509/Model/X509CertificateChain.php @@ -0,0 +1,58 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use ipl\Orm\Behavior\BoolCast; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class X509CertificateChain extends Model +{ + public function getTableName() + { + return 'x509_certificate_chain'; + } + + public function getTableAlias(): string + { + return 'chain'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'target_id', + 'length', + 'valid', + 'invalid_reason', + 'ctime' + ]; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->add(new BoolCast(['valid'])); + + $behaviors->add(new MillisecondTimestamp(['ctime'])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('target', X509Target::class) + ->setCandidateKey('id') + ->setForeignKey('latest_certificate_chain_id'); + + $relations->belongsToMany('certificate', X509Certificate::class) + ->through(X509CertificateChainLink::class) + ->setForeignKey('certificate_chain_id'); + } +} diff --git a/library/X509/Model/X509CertificateChainLink.php b/library/X509/Model/X509CertificateChainLink.php new file mode 100644 index 0000000..d093793 --- /dev/null +++ b/library/X509/Model/X509CertificateChainLink.php @@ -0,0 +1,46 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class X509CertificateChainLink extends Model +{ + public function getTableName() + { + return 'x509_certificate_chain_link'; + } + + public function getTableAlias(): string + { + return 'link'; + } + + public function getKeyName() + { + return ['certificate_chain_id', 'certificate_id', 'order']; + } + + public function getColumns() + { + return ['ctime']; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->add(new MillisecondTimestamp(['ctime'])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('certificate', X509Certificate::class) + ->setCandidateKey('certificate_id'); + $relations->belongsTo('chain', X509CertificateChain::class) + ->setCandidateKey('certificate_chain_id'); + } +} diff --git a/library/X509/Model/X509CertificateSubjectAltName.php b/library/X509/Model/X509CertificateSubjectAltName.php new file mode 100644 index 0000000..62aac5c --- /dev/null +++ b/library/X509/Model/X509CertificateSubjectAltName.php @@ -0,0 +1,50 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use ipl\Orm\Behavior\Binary; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class X509CertificateSubjectAltName extends Model +{ + public function getTableName() + { + return 'x509_certificate_subject_alt_name'; + } + + public function getTableAlias(): string + { + return 'alt_name'; + } + + public function getKeyName() + { + return ['certificate_id', 'hash']; + } + + public function getColumns() + { + return [ + 'type', + 'value', + 'ctime' + ]; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->add(new Binary(['hash'])); + + $behaviors->add(new MillisecondTimestamp(['ctime'])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('certificate', X509Certificate::class); + } +} diff --git a/library/X509/Model/X509Dn.php b/library/X509/Model/X509Dn.php new file mode 100644 index 0000000..fa0406f --- /dev/null +++ b/library/X509/Model/X509Dn.php @@ -0,0 +1,51 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use ipl\Orm\Behavior\Binary; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class X509Dn extends Model +{ + public function getTableName() + { + return 'x509_dn'; + } + + public function getTableAlias(): string + { + return 'dn'; + } + + public function getKeyName() + { + return ['hash', 'type', 'order']; + } + + public function getColumns() + { + return [ + 'key', + 'value', + 'ctime' + ]; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->add(new Binary(['hash'])); + + $behaviors->add(new MillisecondTimestamp(['ctime'])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('certificate', X509Certificate::class) + ->setForeignKey('subject_hash'); + } +} diff --git a/library/X509/Model/X509Job.php b/library/X509/Model/X509Job.php new file mode 100644 index 0000000..1b3a855 --- /dev/null +++ b/library/X509/Model/X509Job.php @@ -0,0 +1,73 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use DateTime; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Query; +use ipl\Orm\Relations; + +/** + * A database model for all x509 jobs + * + * @property int $id Unique identifier of this job + * @property string $name The name of this job + * @property string $author The author of this job + * @property string $cidrs The configured cidrs of this job + * @property string $ports The configured ports of this job + * @property ?string $exclude_targets The configured excluded targets of this job + * @property DateTime $ctime The creation time of this job + * @property DateTime $mtime The modification time of this job + * @property Query|X509Schedule $schedule The configured schedules of this job + * @property Query|X509JobRun $job_run Job activities + */ +class X509Job extends Model +{ + public function getTableName(): string + { + return 'x509_job'; + } + + public function getTableAlias(): string + { + return 'job'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns(): array + { + return [ + 'name', + 'author', + 'cidrs', + 'ports', + 'exclude_targets', + 'ctime', + 'mtime' + ]; + } + + public function createBehaviors(Behaviors $behaviors): void + { + $behaviors->add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations): void + { + $relations->hasMany('schedule', X509Schedule::class) + ->setForeignKey('job_id'); + $relations->hasMany('job_run', X509JobRun::class) + ->setForeignKey('job_id'); + } +} diff --git a/library/X509/Model/X509JobRun.php b/library/X509/Model/X509JobRun.php new file mode 100644 index 0000000..d776622 --- /dev/null +++ b/library/X509/Model/X509JobRun.php @@ -0,0 +1,77 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use DateTime; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Query; +use ipl\Orm\Relations; + +/** + * A database model for all x509 job schedules + * + * @property int $id Unique identifier of this job + * @property ?int $job_id The id of the x509 job this job run belongs to + * @property ?int $schedule_id The id of the x509 job schedule this run belongs to + * @property int $total_targets All the x509 targets found by this job run + * @property int $finished_targets All the x509 targets scanned by this job run + * @property DateTime $start_time The start time of this job run + * @property DateTime $end_time The end time of this job run + * @property Query|X509Job $job The x509 job this job run belongs to + * @property Query|X509Schedule $schedule The x509 job schedule this job run belongs to + */ +class X509JobRun extends Model +{ + public function getTableName(): string + { + return 'x509_job_run'; + } + + public function getTableAlias(): string + { + return 'job_run'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns(): array + { + return [ + 'job_id', + 'schedule_id', + 'total_targets', + 'finished_targets', + 'start_time', + 'end_time' + ]; + } + + public function getDefaultSort(): string + { + return 'start_time desc'; + } + + public function createBehaviors(Behaviors $behaviors): void + { + $behaviors->add(new MillisecondTimestamp([ + 'start_time', + 'end_time', + ])); + } + + public function createRelations(Relations $relations): void + { + $relations->belongsTo('job', X509Job::class) + ->setCandidateKey('job_id'); + $relations->belongsTo('schedule', X509Schedule::class) + ->setJoinType('LEFT') + ->setCandidateKey('schedule_id'); + } +} diff --git a/library/X509/Model/X509Schedule.php b/library/X509/Model/X509Schedule.php new file mode 100644 index 0000000..476641a --- /dev/null +++ b/library/X509/Model/X509Schedule.php @@ -0,0 +1,70 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use DateTime; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +/** + * A database model for all x509 job schedules + * + * @property int $id Unique identifier of this job + * @property int $job_id The id of the x509 job this schedule belongs to + * @property string $name The name of this job schedule + * @property string $author The author of this job schedule + * @property string $config The config of this job schedule + * @property DateTime $ctime The creation time of this job + * @property DateTime $mtime The modification time of this job + * @property X509Job $job The x509 job this schedule belongs to + * @property X509JobRun $job_run Schedule activities + */ +class X509Schedule extends Model +{ + public function getTableName(): string + { + return 'x509_schedule'; + } + + public function getTableAlias(): string + { + return 'schedule'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns(): array + { + return [ + 'job_id', + 'name', + 'author', + 'config', + 'ctime', + 'mtime' + ]; + } + + public function createBehaviors(Behaviors $behaviors): void + { + $behaviors->add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations): void + { + $relations->belongsTo('job', X509Job::class) + ->setCandidateKey('job_id'); + $relations->hasMany('job_run', X509JobRun::class) + ->setForeignKey('schedule_id'); + } +} diff --git a/library/X509/Model/X509Target.php b/library/X509/Model/X509Target.php new file mode 100644 index 0000000..7705d57 --- /dev/null +++ b/library/X509/Model/X509Target.php @@ -0,0 +1,74 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2022 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\Model; + +use Icinga\Module\X509\Model\Behavior\Ip; +use ipl\Orm\Behavior\Binary; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class X509Target extends Model +{ + public function getTableName() + { + return 'x509_target'; + } + + public function getTableAlias(): string + { + return 'target'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'ip', + 'port', + 'hostname', + 'latest_certificate_chain_id', + 'last_scan', + 'ctime', + 'mtime' + ]; + } + + public function getColumnDefinitions() + { + return [ + 'hostname' => t('Host Name'), + 'ip' => t('IP'), + 'port' => t('Port') + ]; + } + + public function getSearchColumns() + { + return ['hostname']; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->add(new Ip(['ip'])); + + $behaviors->add(new MillisecondTimestamp([ + 'ctime', + 'mtime', + 'last_scan' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('chain', X509CertificateChain::class) + ->setCandidateKey('latest_certificate_chain_id'); + } +} |