diff --git a/config-templates/config.php b/config-templates/config.php index c27decf2ca22267f2a3549033d38d2019bb7055f..77dbc3531504888a030248cb7ce8a4379cfb0fdd 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -56,7 +56,6 @@ $config = [ /* * The following settings are *filesystem paths* which define where * SimpleSAMLphp can find or write the following things: - * - 'certdir': The base directory for certificate and key material. * - 'loggingdir': Where to write logs. * - 'datadir': Storage of general data. * - 'tempdir': Saving temporary files. SimpleSAMLphp will attempt to create @@ -64,11 +63,61 @@ $config = [ * When specified as a relative path, this is relative to the SimpleSAMLphp * root directory. */ - 'certdir' => 'cert/', 'loggingdir' => 'log/', 'datadir' => 'data/', 'tempdir' => '/tmp/simplesaml', + /* + * Certificate and key material can be loaded from different possible + * locations. Currently two locations are supported, the local filesystem + * and the database via pdo using the global database configuration. Locations + * are specified by a URL-link prefix before the file name/path or database + * identifier. + */ + + /* To load a certificate or key from the filesystem, it should be specified + * as 'file://<name>' where <name> is either a relative filename or a fully + * qualified path to a file containing the certificate or key in PEM + * format, such as 'cert.pem' or '/path/to/cert.pem'. If the path is + * relative, it will be searched for in the directory defined by the + * 'certdir' parameter below. When 'certdir' is specified as a relative + * path, it will be interpreted as relative to the SimpleSAMLphp root + * directory. Note that locations with no prefix included will be treated + * as file locations for backwards compatibility. + */ + 'certdir' => 'cert/', + + /* To load a certificate or key from the database, it should be specified + * as 'pdo://<id>' where <id> is the identifier in the database table that + * should be matched. While the certificate and key tables are expected to + * be in the simplesaml database, they are not created or managed by + * simplesaml. The following parameters control how the pdo location + * attempts to retrieve certificates and keys from the database: + * + * - 'cert.pdo.table': name of table where certificates are stored + * - 'cert.pdo.keytable': name of table where keys are stored + * - 'cert.pdo.apply_prefix': whether or not to prepend the database.prefix + * parameter to the table names; if you are using + * database.prefix to separate multiple SSP instances + * in the same database but want to share certificate/key + * data between them, set this to false + * - 'cert.pdo.id_column': name of column to use as identifier + * - 'cert.pdo.data_column': name of column where PEM data is stored + * + * Basically, the query executed will be: + * + * SELECT cert.pdo.data_column FROM cert.pdo.table WHERE cert.pdo.id_column = :id + * + * Defaults are shown below, to change them, uncomment the line and update as + * needed + */ + + //'cert.pdo.table' => 'certificates', + //'cert.pdo.keytable' => 'private_keys', + //'cert.pdo.apply_prefix' => true, + //'cert.pdo.id_column' => 'id', + //'cert.pdo.data_column' => 'data', + /* * Some information about the technical persons running this installation. * The email address will be used as the recipient address for error reports, and diff --git a/docs/simplesamlphp-advancedfeatures.md b/docs/simplesamlphp-advancedfeatures.md index 90df9f49e30d5f9c9ea68205f1ecbdd48f543ae8..73ef877e10ba2ecf12a5f1740833e6913f0914a4 100644 --- a/docs/simplesamlphp-advancedfeatures.md +++ b/docs/simplesamlphp-advancedfeatures.md @@ -90,9 +90,9 @@ Metadata signing SimpleSAMLphp supports signing of the metadata it generates. Metadata signing is configured by four options: - `metadata.sign.enable`: Whether metadata signing should be enabled or not. Set to `TRUE` to enable metadata signing. Defaults to `FALSE`. -- `metadata.sign.privatekey`: Name of the file with the private key which should be used to sign the metadata. This file must exist in in the `cert` directory. +- `metadata.sign.privatekey`: Location of the private key data which should be used to sign the metadata. - `metadata.sign.privatekey_pass`: Passphrase which should be used to open the private key. This parameter is optional, and should be left out if the private key is unencrypted. -- `metadata.sign.certificate`: Name of the file with the certificate which matches the private key. This file must exist in in the `cert` directory. +- `metadata.sign.certificate`: Location of certificate data which matches the private key. - `metadata.sign.algorithm`: The algorithm to use when signing metadata for this entity. Defaults to RSA-SHA256. Possible values: * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` diff --git a/docs/simplesamlphp-changelog.md b/docs/simplesamlphp-changelog.md index 6360f81ddf7141dbd1f31371bb5919c5a445bd0d..0e2b20adb79d303cdf09fc32caae4d6c24b83014 100644 --- a/docs/simplesamlphp-changelog.md +++ b/docs/simplesamlphp-changelog.md @@ -17,6 +17,7 @@ See the upgrade notes for specific information about upgrading. * core:PairwiseID and core:SubjectID authprocs no longer support the 'scope' config-setting. Use 'scopeAttribute' instead to identify the attribute holding the scope. * Accepting unsolicited responses can be disabled by setting `enable_unsolicited` to `false` in the SP authsource. + * Certificates and private keys can now be retrieved from a database ## Version 1.19.1 diff --git a/docs/simplesamlphp-idp.md b/docs/simplesamlphp-idp.md index a4f623fdf1a4bf8983659b9828dbdc62ba7a0505..367cf9e2c6ba3a7740866fafd09f6f0deb300549 100644 --- a/docs/simplesamlphp-idp.md +++ b/docs/simplesamlphp-idp.md @@ -152,7 +152,8 @@ This is a minimal configuration: /* * The private key and certificate to use when signing responses. - * These are stored in the cert-directory. + * These can be stored as files in the cert-directory or retrieved + * from a database. */ 'privatekey' => 'example.org.pem', 'certificate' => 'example.org.crt', diff --git a/docs/simplesamlphp-reference-idp-hosted.md b/docs/simplesamlphp-reference-idp-hosted.md index bd82ad1bc3e60c8d31f511e5858eac8ddca21ab4..ea64aa3b30424d87a294546bd1efb9e4cb907e83 100644 --- a/docs/simplesamlphp-reference-idp-hosted.md +++ b/docs/simplesamlphp-reference-idp-hosted.md @@ -40,8 +40,7 @@ Common options the [authentication processing filter manual](simplesamlphp-authproc). `certificate` -: Certificate file which should be used by this IdP, in PEM format. - The filename is relative to the `cert/`-directory. +: Location of certificate data which should be used by this IdP, in PEM format. `contacts` : Specify contacts in addition to the technical contact configured through config/config.php. @@ -106,8 +105,7 @@ Common options ], `privatekey` -: Name of private key file for this IdP, in PEM format. The filename - is relative to the `cert/`-directory. +: Location of private key data for this IdP, in PEM format. `privatekey_pass` : Passphrase for the private key. Leave this option out if the diff --git a/docs/simplesamlphp-reference-idp-remote.md b/docs/simplesamlphp-reference-idp-remote.md index 8a4fb90b600bbd40e36fa2251c79c3f4991e8009..0fff6e88ceb558256e34b4d389f039eeedad2842 100644 --- a/docs/simplesamlphp-reference-idp-remote.md +++ b/docs/simplesamlphp-reference-idp-remote.md @@ -49,7 +49,7 @@ Options : The base64 encoded certificate for this IdP. This is an alternative to storing the certificate in a file on disk and specifying the filename in the `certificate`-option. `certificate` -: The file with the certificate for this IdP. The path is relative to the `cert`-directory. +: Location of certificate data for this IdP. `description` : A description of this IdP. Will be used by various modules when they need to show a description of the IdP to the user. diff --git a/docs/simplesamlphp-reference-sp-remote.md b/docs/simplesamlphp-reference-sp-remote.md index 77213e64911a2a2a49a4822920a1e0e5e176f1f1..ac44c1b2a0b02e71a9592f70516d74d69d8f868c 100644 --- a/docs/simplesamlphp-reference-sp-remote.md +++ b/docs/simplesamlphp-reference-sp-remote.md @@ -144,7 +144,7 @@ The following options can be set: : The base64 encoded certificate for this SP. This is an alternative to storing the certificate in a file on disk and specifying the filename in the `certificate`-option. `certificate` -: Name of certificate file for this SP. The certificate is used to +: Location of certificate data for this SP. The certificate is used to verify the signature of messages received from the SP (if `redirect.validate`is set to `TRUE`), and to encrypting assertions (if `assertion.encryption` is set to TRUE and `sharedkey` is @@ -221,7 +221,7 @@ The following options can be set: : * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` `signature.privatekey` -: Name of private key file for this IdP, in PEM format. The filename is relative to the cert/-directory. +: Location of private key data for this IdP, in PEM format. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option `privatekey` in the IdP-hosted metadata. `signature.privatekey_pass` @@ -229,7 +229,7 @@ The following options can be set: : Note that this option only is used if `signature.privatekey` is present. `signature.certificate` -: Certificate file included by IdP for KeyInfo within the signature for the SP, in PEM format. The filename is relative to the cert/-directory. +: Location of certificate data included by IdP for KeyInfo within the signature for the SP, in PEM format. : If `signature.privatekey` is present and `signature.certificate` is left blank, X509Certificate will not be included with the signature. `sign.logout` diff --git a/src/SimpleSAML/Configuration.php b/src/SimpleSAML/Configuration.php index 5284fadbedd803f3d39022383f2ac4f713674b9e..614684d66e21799d627b42f4d9a979f3f975aac8 100644 --- a/src/SimpleSAML/Configuration.php +++ b/src/SimpleSAML/Configuration.php @@ -1355,8 +1355,8 @@ class Configuration implements Utils\ClearableState * * @return array Public key data, or empty array if no public key or was found. * - * @throws \Exception If the certificate or public key cannot be loaded from a file. - * @throws \SimpleSAML\Error\Exception If the file does not contain a valid PEM-encoded certificate, or there is no + * @throws \Exception If the certificate or public key cannot be loaded from location. + * @throws \SimpleSAML\Error\Exception If the location does not contain a valid PEM-encoded certificate, or there is no * certificate in the metadata. */ public function getPublicKeys(?string $use = null, bool $required = false, string $prefix = ''): array @@ -1388,15 +1388,14 @@ class Configuration implements Utils\ClearableState ], ]; } elseif ($this->hasValue($prefix . 'certificate')) { - $configUtils = new Utils\Config(); + $location = $this->getString($prefix . 'certificate'); - $file = $this->getString($prefix . 'certificate'); - $file = $configUtils->getCertPath($file); - $data = @file_get_contents($file); + $cryptoUtils = new Utils\Crypto(); + $data = $cryptoUtils->retrieveCertificate($location); - if ($data === false) { + if ($data === null) { throw new Exception( - $this->location . ': Unable to load certificate/public key from file "' . $file . '".' + $this->location . ': Unable to load certificate/public key from location "' . $location . '".' ); } @@ -1404,7 +1403,7 @@ class Configuration implements Utils\ClearableState $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; if (!preg_match($pattern, $data, $matches)) { throw new Error\Exception( - $this->location . ': Could not find PEM encoded certificate in "' . $file . '".' + $this->location . ': Could not find PEM encoded certificate in "' . $location . '".' ); } $certData = preg_replace('/\s+/', '', $matches[1]); diff --git a/src/SimpleSAML/Metadata/SAMLParser.php b/src/SimpleSAML/Metadata/SAMLParser.php index 8e476e1282483e15a18a2392a86d0a7bc717e6c2..5ca445bbc9c816da6c1fad9aaf1c88a604cb9d2d 100644 --- a/src/SimpleSAML/Metadata/SAMLParser.php +++ b/src/SimpleSAML/Metadata/SAMLParser.php @@ -1277,22 +1277,21 @@ class SAMLParser * to do a key rollover. * * @return boolean True if it is possible to check the signature with the certificate, false otherwise. - * @throws \Exception If the certificate file cannot be found. + * @throws \Exception If the certificate location cannot be found. */ public function validateSignature(array $certificates): bool { - $configUtils = new Utils\Config(); + $cryptoUtils = new Utils\Crypto(); - foreach ($certificates as $cert) { - Assert::string($cert); - $certFile = $configUtils->getCertPath($cert); - if (!$this->fileSystem->exists($certFile)) { + foreach ($certificates as $certLocation) { + Assert::string($certLocation); + + $certData = $cryptoUtils->retrieveCertificate($certLocation); + if ($certData === null) { throw new Exception( - 'Could not find certificate file [' . $certFile . '], which is needed to validate signature' + 'Could not find certificate location [' . $certLocation . '], which is needed to validate signature' ); } - $file = new File($certFile); - $certData = $file->getContent(); foreach ($this->validators as $validator) { $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'public']); diff --git a/src/SimpleSAML/Metadata/Signer.php b/src/SimpleSAML/Metadata/Signer.php index 7a35a33dcefc24206c8f1ed2b690c755d45793c1..4cdbfa12a4e7b59f82642e14d8211668b7ff1b0a 100644 --- a/src/SimpleSAML/Metadata/Signer.php +++ b/src/SimpleSAML/Metadata/Signer.php @@ -29,7 +29,7 @@ use function is_string; class Signer { /** - * This functions finds what key & certificate files should be used to sign the metadata + * This functions finds what key & certificate locations should be used to sign the metadata * for the given entity. * * @param \SimpleSAML\Configuration $config Our \SimpleSAML\Configuration instance. @@ -235,7 +235,7 @@ class Signer public static function sign(string $metadataString, array $entityMetadata, string $type): string { $config = Configuration::getInstance(); - $configUtils = new Utils\Config(); + $cryptoUtils = new Utils\Crypto(); // check if metadata signing is enabled if (!self::isMetadataSigningEnabled($config, $entityMetadata, $type)) { @@ -243,27 +243,23 @@ class Signer } // find the key & certificate which should be used to sign the metadata - $keyCertFiles = self::findKeyCert($config, $entityMetadata, $type); + $keyCertLocations = self::findKeyCert($config, $entityMetadata, $type); - $keyFile = $configUtils->getCertPath($keyCertFiles['privatekey']); - $fileSystem = new Filesystem(); - if (!$fileSystem->exists($keyFile)) { + $keyLocation = $keyCertLocations['privatekey']; + $keyData = $cryptoUtils->retrieveKey($keyLocation); + if ($keyData === null) { throw new Exception( - 'Could not find private key file [' . $keyFile . '], which is needed to sign the metadata' + 'Could not find private key location [' . $keyLocation . '], which is needed to sign the metadata' ); } - $key = new File($keyFile); - $keyData = $key->getContent(); - - $certFile = $configUtils->getCertPath($keyCertFiles['certificate']); - $cert = new File($certFile); - if (!$fileSystem->exists($certFile)) { + $certLocation = $keyCertLocations['certificate']; + $certData = $cryptoUtils->retrieveCertificate($certLocation); + if ($certData === null) { throw new Exception( - 'Could not find certificate file [' . $certFile . '], which is needed to sign the metadata' + 'Could not find certificate location [' . $certLocation . '], which is needed to sign the metadata' ); } - $certData = $cert->getContent(); // convert the metadata to a DOM tree try { @@ -276,8 +272,8 @@ class Signer // load the private key $objKey = new XMLSecurityKey($signature_cf['algorithm'], ['type' => 'private']); - if (array_key_exists('privatekey_pass', $keyCertFiles)) { - $objKey->passphrase = $keyCertFiles['privatekey_pass']; + if (array_key_exists('privatekey_pass', $keyCertLocations)) { + $objKey->passphrase = $keyCertLocations['privatekey_pass']; } $objKey->loadKey($keyData, false); diff --git a/src/SimpleSAML/Utils/Crypto.php b/src/SimpleSAML/Utils/Crypto.php index 5d741a4b1df67c80e0d165be5e953a36d587e175..ab55d9c659df409942b9bd2593f102cce907fbd8 100644 --- a/src/SimpleSAML/Utils/Crypto.php +++ b/src/SimpleSAML/Utils/Crypto.php @@ -8,6 +8,7 @@ use SimpleSAML\Assert\Assert; use InvalidArgumentException; use SimpleSAML\Configuration; use SimpleSAML\Error; +use SimpleSAML\Logger; /** * A class for cryptography-related functions. @@ -176,7 +177,7 @@ class Crypto * Load a private key from metadata. * * This function loads a private key from a metadata array. It looks for the following elements: - * - 'privatekey': Name of a private key file in the cert-directory. + * - 'privatekey': Location of a private key * - 'privatekey_pass': Password for the private key. * * It returns an array with the following elements: @@ -188,8 +189,8 @@ class Crypto * missing key will cause an exception. Defaults to false. * @param string $prefix The prefix which should be used when reading from the metadata * array. Defaults to ''. - * @param bool $full_path Whether the filename found in the configuration contains the - * full path to the private key or not. Default to false. + * @param bool $full_path Whether the location found in the configuration contains the + * full path to the private key or not (only relevant for file locations). Default to false. * * @return array|NULL Extracted private key, or NULL if no private key is present. * @throws \InvalidArgumentException If $required is not boolean or $prefix is not a string. @@ -203,8 +204,8 @@ class Crypto string $prefix = '', bool $full_path = false ): ?array { - $file = $metadata->getOptionalString($prefix . 'privatekey', null); - if ($file === null) { + $location = $metadata->getOptionalString($prefix . 'privatekey', null); + if ($location === null) { // no private key found if ($required) { throw new Error\Exception('No private key found in metadata.'); @@ -213,14 +214,10 @@ class Crypto } } - if (!$full_path) { - $configUtils = new Config(); - $file = $configUtils->getCertPath($file); - } + $data = $this->retrieveKey($location, $full_path); - $data = @file_get_contents($file); - if ($data === false) { - throw new Error\Exception('Unable to load private key from file "' . $file . '"'); + if ($data === null) { + throw new Error\Exception('Unable to load private key from location "' . $location . '"'); } $ret = [ @@ -239,7 +236,7 @@ class Crypto * * It will search for the following elements in the metadata: * - 'certData': The certificate as a base64-encoded string. - * - 'certificate': A file with a certificate or public key in PEM-format. + * - 'certificate': Location of a certificate or public key in PEM-format. * * This function will return an array with these elements: * - 'PEM': The public key/certificate in PEM-encoding. @@ -377,4 +374,112 @@ class Crypto } return $hash === $password; } + + /** + * Retrieve a certificate or private key from specified storage location + * + * @param string $data_type Type of data to retrieve, either "certificate" or "private_key" + * @param string $location Location of data to retrieve + * @param bool $full_path Whether the location found in the configuration contains the + * full path to the certificate or private key (only relevant to file locations) + * + * @return string The certificate or private key, or null if not found + * + */ + private function retrieveCertOrKey(string $data_type, string $location, bool $full_path): ?string + { + if (strncmp($location, 'pdo://', 6) === 0) { + # Attempt to load data via pdo from database + + $location = substr($location, 6); + + $globalConfig = Configuration::getInstance(); + $cert_table = $globalConfig->getOptionalString('cert.pdo.table', 'certificates'); + $key_table = $globalConfig->getOptionalString('cert.pdo.keytable', 'private_keys'); + $apply_prefix = $globalConfig->getOptionalBoolean('cert.pdo.apply_prefix', true); + $id_column = $globalConfig->getOptionalString('cert.pdo.id_column', 'id'); + $data_column = $globalConfig->getOptionalString('cert.pdo.data_column', 'data'); + + try { + $db = \SimpleSAML\Database::getInstance(); + } catch (\Exception $e) { + Logger::error('failed to instantiate database: ' . $e->getMessage()); + return(null); + } + + if ($apply_prefix) { + $cert_table = $db->applyPrefix($cert_table); + $key_table = $db->applyPrefix($key_table); + } + + try { + $query = $db->read("select $data_column from " . + ($data_type == 'certificate' ? $cert_table : $key_table) . + " where $id_column = :id", ['id' => $location]); + } catch (\Exception $e) { + Logger::error('failed to query database: ' . $e->getMessage()); + return(null); + } + + $result = $query->fetch(\PDO::FETCH_NUM); + + if ($result) { + return($result[0]); + } + + return(null); + } elseif (strncmp($location, 'file://', 7) === 0) { + # For backwards compatibility, locations without a prefix are assumed to be file locations. + # So just remove prefix and fall through + + $location = substr($location, 7); + } + + # Attempt to load data from file + if (!$full_path) { + $configUtils = new Config(); + $location = $configUtils->getCertPath($location); + } + + $data = @file_get_contents($location); + + if ($data === false) { + Logger::error("failed to read $data_type data from file $location"); + return(null); + } + + return($data); + } + + /** + * Public wrapper around retrieveCertOrKey to retrieve a certificate + * + * @param string $location Location of certificate data to retrieve + * @param bool $full_path Whether the location found in the configuration contains the + * full path to the certificate (only relevant to file locations). + * Default to false. + * + * @return string The certificate or null if not found + * + */ + public function retrieveCertificate(string $location, bool $full_path = false): ?string + { + return $this->retrieveCertOrKey('certificate', $location, $full_path); + } + + /** + * Public wrapper around retrieveCertOrKey to retrieve a private key + * + * @param string $location Location of private key data to retrieve + * @param bool $full_path Whether the location found in the configuration contains the + * full path to the private key (only relevant to file locations). + * Default to false. + * + * @return string The private key or null if not found + * + */ + public function retrieveKey(string $location, bool $full_path = false): ?string + { + return $this->retrieveCertOrKey('private_key', $location, $full_path); + } } diff --git a/src/SimpleSAML/XML/Signer.php b/src/SimpleSAML/XML/Signer.php index 4bc0876507f6c986bc361255ef93a2d0b812e982..17ac5b150aeb3a67dfcbe5aa5b11bd88c8480fc7 100644 --- a/src/SimpleSAML/XML/Signer.php +++ b/src/SimpleSAML/XML/Signer.php @@ -125,32 +125,23 @@ class Signer * * Will throw an exception if unable to load the private key. * - * @param string $file The file which contains the private key. The path is assumed to be relative - * to the cert-directory. + * @param string $location The location which contains the private key * @param string|null $pass The passphrase on the private key. Pass no value or NULL if the private * key is unencrypted. - * @param bool $full_path Whether the filename found in the configuration contains the - * full path to the private key or not. Default to false. + * @param bool $full_path Whether the location found in the configuration contains the + * full path to the private key or not (only relevant to file locations). + * Default to false. * @throws \Exception */ - public function loadPrivateKey(string $file, ?string $pass, bool $full_path = false): void + public function loadPrivateKey(string $location, ?string $pass, bool $full_path = false): void { - if (!$full_path) { - $configUtils = new Utils\Config(); - $keyFile = $configUtils->getCertPath($file); - } else { - $keyFile = $file; - } + $cryptoUtils = new Utils\Crypto(); + $keyData = $cryptoUtils->retrieveKey($location, $full_path); - if (!$this->fileSystem->exists($keyFile)) { - throw new Exception('Could not find private key file "' . $keyFile . '".'); + if ($keyData === null) { + throw new Exception('Could not find private key location "' . $location . '".'); } - $file = new File($keyFile); - $keyData = $file->getContent(); - if ($keyData === false) { - throw new Exception('Unable to read private key file "' . $keyFile . '".'); - } $privatekey = ['PEM' => $keyData]; if ($pass !== null) { @@ -232,31 +223,22 @@ class Signer * Extra certificates will be added to the certificate chain in the order they * are added. * - * @param string $file The file which contains the certificate, relative to the cert-directory. - * @param bool $full_path Whether the filename found in the configuration contains the - * full path to the private key or not. Default to false. + * @param string $location The location which contains the certificate + * @param bool $full_path Whether the location found in the configuration contains the + * full path to the private key or not (only relevant to file locations). + * Default to false. * @throws \Exception */ - public function addCertificate(string $file, bool $full_path = false): void + public function addCertificate(string $location, bool $full_path = false): void { - if (!$full_path) { - $configUtils = new Utils\Config(); - $certFile = $configUtils->getCertPath($file); - } else { - $certFile = $file; - } + $cryptoUtils = new Utils\Crypto(); + $certData = $cryptoUtils->retrieveCertificate($location, $full_path); - if (!$this->fileSystem->exists($certFile)) { - throw new Exception('Could not find extra certificate file "' . $certFile . '".'); - } - - $file = new File($certFile); - $certificate = $file->getContent(); - if ($certificate === false) { - throw new Exception('Unable to read extra certificate file "' . $certFile . '".'); + if ($certData === null) { + throw new Exception('Could not find extra certificate location "' . $location . '".'); } - $this->extraCertificates[] = $certificate; + $this->extraCertificates[] = $certData; }