diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..13a634c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7f520e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +/vendor/ +composer.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d4cf59c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to `SSL converter` will be documented in this file. + +Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. + +## Unreleased +[Compare v0.0.1 - Unreleased](https://github.com/exonet/ssl-converter/compare/v0.0.1...develop) + +## [v0.0.1](https://github.com/exonet/ssl-converter/releases/tag/v0.0.1) - 2019-01-30 +### Added +- Initial release. diff --git a/README.md b/README.md index 3fa9548..932a470 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ -# ssl-converter +# SSL converter PHP package to convert an SSL certificate to various formats (e.g PKC12, PEM). + +## Install +Via Composer + +``` bash +$ composer require exonet/ssl-converter +``` + +## Example usage +The example below shows how combine separate contents of a certificate to a combined PEM string. + - `crt` The certificate (typically the contents of `.crt` file). + - `key` The private key (typically the contents of the `.key` file) + - `ca bundle` The certificate of the intermediate and/or the trusted root certificate + +```php +// Initialise a new SSL converter. +$converter = new Converter(); + +// Setup the plain format class that should be converter. +$plain = new Plain(); +$plain + ->setKey('-----BEGIN PRIVATE KEY----- +... +-----END PRIVATE KEY----- +') + ->setCrt('-----BEGIN CERTIFICATE----- +... +-----END CERTIFICATE----- +') + ->setCaBundle('-----BEGIN CERTIFICATE----- +... +-----END CERTIFICATE----- +'); + +// Convert the plain certificate. +$pem = $converter + ->from($plain) + ->to(new Pem()); + +// Save as zip file: +$pem->asZip('./'); + +// Get an array with the certificate files: +print_r($pem->asFiles()); + +// Get the certificate as string: +print_r($pem->asString()); +``` + +## Change log +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3cfd1b4 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "exonet/ssl-converter", + "description": "PHP package to convert an SSL certificate to various formats (e.g PKC12, PEM).", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Exonet B.V.", + "email": "development@exonet.nl" + } + ], + "require": { + "php": "~7.1", + "ext-openssl": "*", + "ext-zip": "*" + }, + "autoload": { + "psr-4": { + "Exonet\\SslConverter\\": "src" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/src/Converter.php b/src/Converter.php new file mode 100644 index 0000000..c4d2ed8 --- /dev/null +++ b/src/Converter.php @@ -0,0 +1,143 @@ +certificateName = $certificateName; + + return $this; + } + + /** + * Set the format that should be converted. + * + * @param FormatInterface $from The input format to be converted. + * + * @return $this Instance of this class. + */ + public function from(FormatInterface $from) : self + { + $this->from = $from; + + return $this; + } + + /** + * Set the format that should be converted to. + * + * @param FormatInterface $to The output format to convert to. + * + * @return $this Instance of this class + */ + public function to(FormatInterface $to) : self + { + $this->to = $to; + + return $this; + } + + /** + * Convert the input format to the provided output format as a string. + * + * @return string The converted format as a string that can be saved to a file. + * + * @throws Exceptions\InvalidResource When the provided certificate is invalid. + * @throws Exceptions\MissingRequiredInformation When some required certificate data is missing. + */ + public function asString() : string + { + return $this->to + ->setName($this->certificateName) + ->setPlain($this->from->getPlain()) + ->toString(); + } + + /** + * Convert the input format to the provided output format as an array of files. The array key is the file name, the + * array value the file contents. + * + * @return string[] The converted format as an array of files that can be saved to a file. + * + * @throws Exceptions\InvalidResource When the provided certificate is invalid. + * @throws Exceptions\MissingRequiredInformation When some required certificate data is missing. + */ + public function asFiles() : array + { + return $this->to + ->setName($this->certificateName) + ->setPlain($this->from->getPlain()) + ->export(); + } + + /** + * Convert the input format to the provided output format and save the file (or files) as zip. + * + * @param string $path The path where to store the zip file. + * + * @return bool True when the zip is created and stored. + * + * @throws Exceptions\InvalidResource When the provided certificate is invalid. + * @throws Exceptions\MissingRequiredInformation When some required certificate data is missing. + * @throws ZipException When the files can not be zipped. + */ + public function asZip(string $path) : bool + { + $filename = sprintf('%s/%s.zip', rtrim($path, '/'), $this->certificateName); + + if (!is_dir($path) || !is_writable($path)) { + throw new ZipException(sprintf('The directory [%s] is does not exists or is not writable.', $path)); + } + + if (is_file($filename) && !is_writable($filename)) { + throw new ZipException(sprintf('The file [%s] already exists and is not writable.', $filename)); + } + + $files = $this->to + ->setName($this->certificateName) + ->setPlain($this->from->getPlain()) + ->export(); + + $zip = new ZipArchive(); + $zip->open($filename, ZipArchive::CREATE); + foreach ($files as $name => $content) { + $zip->addFromString($name, $content); + } + + if ($zip->status !== 0) { + throw new ZipException($zip->getStatusString()); + } + + $zip->close(); + + return file_exists($filename); + } +} diff --git a/src/Exceptions/InvalidResource.php b/src/Exceptions/InvalidResource.php new file mode 100644 index 0000000..1bc7508 --- /dev/null +++ b/src/Exceptions/InvalidResource.php @@ -0,0 +1,7 @@ +setOptions($options); + } + + /** + * {@inheritdoc} + */ + public function setName(string $name) : FormatInterface + { + $this->name = $name; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getName() : string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function setPlain(Plain $plain) : FormatInterface + { + $this->plainCertificate = $plain; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOptions(array $options) : FormatInterface + { + $this->options = $options; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getOptions() : array + { + return $this->options; + } + + /** + * @inheritdoc + * + * @throws NotImplementedException When this method is not implemented. + */ + public function getPlain() : Plain + { + throw new NotImplementedException('The [getPlain] method is not implemented for this format.'); + } +} diff --git a/src/Formats/FormatInterface.php b/src/Formats/FormatInterface.php new file mode 100644 index 0000000..7fdfa5f --- /dev/null +++ b/src/Formats/FormatInterface.php @@ -0,0 +1,77 @@ +name.'.pem' => $this->toString(), + ]; + } + + /** + * @inheritdoc + */ + public function toString() : string + { + $key = $this->plainCertificate->getKey(); + $crt = $this->plainCertificate->getCrt(); + $caBundle = $this->plainCertificate->getCaBundle(); + + if (!$crt || !$caBundle) { + throw new MissingRequiredInformation('The following fields are required for PEM: CRT, CA Bundle.'); + } + + // If there is a key, prepend the certificate content with the key. + $content = $key ? $key.$crt.$caBundle : $crt.$caBundle; + if (!openssl_x509_read($content)) { + throw new InvalidResource('Invalid certificate provided.'); + } + + return $content; + } +} diff --git a/src/Formats/Pkcs12.php b/src/Formats/Pkcs12.php new file mode 100644 index 0000000..ca7fca8 --- /dev/null +++ b/src/Formats/Pkcs12.php @@ -0,0 +1,40 @@ +name.'.pfx' => $this->toString(), + ]; + } + + /** + * @inheritdoc + */ + public function toString() : string + { + $key = $this->plainCertificate->getKey(); + $crt = $this->plainCertificate->getCrt(); + $caBundle = $this->plainCertificate->getCaBundle(); + $password = $this->options['password'] ?? false; + + if (!$key || !$crt || !$caBundle || !$password) { + throw new MissingRequiredInformation('The following fields are required for PKCS12: key, CRT, CA Bundle, password.'); + } + + if (!openssl_pkcs12_export($crt, $pkc12, $key, $password, ['extracerts' => $caBundle])) { + throw new InvalidResource('Invalid certificate provided.'); + }; + + return $pkc12; + } +} diff --git a/src/Formats/Plain.php b/src/Formats/Plain.php new file mode 100644 index 0000000..880bf49 --- /dev/null +++ b/src/Formats/Plain.php @@ -0,0 +1,121 @@ +name.'.key' => $this->plainCertificate->getKey(), + $this->name.'.crt' => $this->plainCertificate->getCrt(), + $this->name.'.ca-bundle' => $this->plainCertificate->getCaBundle(), + ]; + } + + /** + * @inheritdoc + */ + public function toString() : string + { + return $this->plainCertificate->getCrt().$this->plainCertificate->getKey().$this->plainCertificate->getCaBundle(); + } + + /** + * @inheritdoc + */ + public function getPlain() : self + { + return $this; + } + + /** + * Get the crt. + * + * @return string The crt. + */ + public function getCrt() : ?string + { + return $this->crt; + } + + /** + * Set the crt. + * + * @param string $crt The crt to set + * + * @return $this The current instance of this class. + */ + public function setCrt(string $crt) : self + { + $this->crt = $crt; + + return $this; + } + + /** + * Get the key. + * + * @return string The key. + */ + public function getKey() : ?string + { + return $this->key; + } + + /** + * Set the key. + * + * @param string $key The key. + * + * @return $this The current instance of this class. + */ + public function setKey(string $key) : self + { + $this->key = $key; + + return $this; + } + + /** + * Get the CA bundle. + * + * @return string The CA bundle. + */ + public function getCaBundle() : ?string + { + return $this->caBundle; + } + + /** + * Set the CA bundle. + * + * @param string $caBundle The CA bundle. + * + * @return $this The current instance of this class. + */ + public function setCaBundle(string $caBundle) : self + { + $this->caBundle = $caBundle; + + return $this; + } +}